/**
 * Channel builders + subscribe authorization for live notifications.
 *
 * Channel name conventions:
 *   chat:thread:<id>       — chat thread (participants only — backed by chat_thread_participants)
 *   presence:thread:<id>   — presence indicators for a thread (same authorization)
 *   live:deal:<id>         — deal viewer count / slot updates
 *   live:groupdeal:<id>    — group-deal slot count
 *   live:vendor:<id>       — vendor-scoped live updates
 *   live:admin:<adminId>   — admin-scoped channel (admin ID match required)
 *   live:admin:system      — global admin broadcast
 *   live:user:<userId>     — per-user channel
 *   live:ticket:<id>       — support ticket channel
 *   live:case:<id>         — transaction case channel
 *
 * NOTE: chat:thread / presence:thread authorization backed by chat_thread_participants
 * table (landed migration 0065). canSubscribe queries isParticipant() with CHAT_TTL_MS cache.
 */
import type { DrizzleClient } from '@/server/db/client.js';
import { isParticipant } from '@/server/db/queries/chat-thread-participants.js';
import { eq } from 'drizzle-orm';
import { supportTickets, transactionCases, vendors } from '@/server/db/schema.js';
import { asCaseId } from '@/server/platform-seams/ids.js';

export type Role = 'user' | 'vendor' | 'admin';
export type ChannelName = string;
export type Db = DrizzleClient;

// ─── Channel name builders ────────────────────────────────────────────────────

export function chatThreadCh(id: string): ChannelName {
  return `chat:thread:${id}`;
}
export function presenceThreadCh(id: string): ChannelName {
  return `presence:thread:${id}`;
}
export function liveDealCh(id: string): ChannelName {
  return `live:deal:${id}`;
}
export function liveGroupDealCh(id: string): ChannelName {
  return `live:groupdeal:${id}`;
}
export function liveVendorCh(id: string): ChannelName {
  return `live:vendor:${id}`;
}
export function liveAdminCh(adminId: string): ChannelName {
  return `live:admin:${adminId}`;
}
export function liveUserCh(userId: string): ChannelName {
  return `live:user:${userId}`;
}
export function liveTicketCh(id: string): ChannelName {
  return `live:ticket:${id}`;
}
export function liveCaseCh(id: string): ChannelName {
  return `live:case:${id}`;
}

// ─── Channel parsing ──────────────────────────────────────────────────────────

export type ParsedChannel =
  | { family: 'chat:thread'; id: string }
  | { family: 'presence:thread'; id: string }
  | { family: 'live:deal'; id: string }
  | { family: 'live:groupdeal'; id: string }
  | { family: 'live:vendor'; id: string }
  | { family: 'live:admin'; id: string }
  | { family: 'live:admin:system' }
  | { family: 'live:user'; id: string }
  | { family: 'live:ticket'; id: string }
  | { family: 'live:case'; id: string };

const KNOWN_FAMILIES = new Set([
  'chat:thread',
  'presence:thread',
  'live:deal',
  'live:groupdeal',
  'live:vendor',
  'live:admin',
  'live:user',
  'live:ticket',
  'live:case',
]);

export function parseChannel(ch: ChannelName): ParsedChannel | null {
  if (ch === 'live:admin:system') return { family: 'live:admin:system' };
  const parts = ch.split(':');
  if (parts.length < 3) return null;
  // family = all but last segment, id = last segment
  const id = parts[parts.length - 1] as string;
  const family = parts.slice(0, -1).join(':');
  if (!KNOWN_FAMILIES.has(family)) return null;
  return { family, id } as ParsedChannel;
}

// ─── Authorization cache ──────────────────────────────────────────────────────

type CacheEntry = { ok: boolean; until: number };
const authCache = new Map<string, CacheEntry>();

const CHAT_TTL_MS = 60_000;
const ADMIN_TTL_MS = 10_000;

function cacheGet(key: string): boolean | null {
  const e = authCache.get(key);
  if (!e) return null;
  if (Date.now() > e.until) {
    authCache.delete(key);
    return null;
  }
  return e.ok;
}
function cachePut(key: string, ok: boolean, ttlMs: number): void {
  authCache.set(key, { ok, until: Date.now() + ttlMs });
}

// ─── canSubscribe ─────────────────────────────────────────────────────────────

/**
 * Determine whether `userId` with `role` may subscribe to `ch`.
 *
 * @param db   - DrizzleClient (used for DB-backed checks)
 * @param userId - "anon:<sid>" for anonymous sessions
 * @param ch   - Channel name
 * @param role - Derived role from JWT claims
 */
export async function canSubscribe(
  db: Db,
  userId: string,
  ch: ChannelName,
  role: Role,
): Promise<boolean> {
  // The global notification channel is always allowed.
  if (ch === 'notif') return true;

  const p = parseChannel(ch);
  if (!p) return false;

  const key = `${userId}|${ch}|${role}`;
  const cached = cacheGet(key);
  if (cached !== null) return cached;

  switch (p.family) {
    case 'live:admin:system': {
      const result = role === 'admin';
      cachePut(key, result, ADMIN_TTL_MS);
      return result;
    }
    case 'live:admin': {
      const result = role === 'admin' && p.id === userId;
      cachePut(key, result, ADMIN_TTL_MS);
      return result;
    }
    case 'live:user': {
      // Never cached — identity is stable within session.
      return p.id === userId;
    }
    case 'live:deal':
    case 'live:groupdeal':
    case 'live:vendor': {
      // Public channels — open to all authenticated sessions.
      return true;
    }
    case 'live:ticket': {
      const result = await isTicketParticipant(db, userId, p.id);
      cachePut(key, result, CHAT_TTL_MS);
      return result;
    }
    case 'live:case': {
      const result = await isCaseParticipant(db, userId, p.id);
      cachePut(key, result, CHAT_TTL_MS);
      return result;
    }
    case 'chat:thread':
    case 'presence:thread': {
      const result = await isParticipant(db, p.id, userId);
      cachePut(key, result, CHAT_TTL_MS);
      return result;
    }
  }
}

// ─── Private helpers ──────────────────────────────────────────────────────────

async function isTicketParticipant(db: Db, userId: string, ticketId: string): Promise<boolean> {
  const rows = await db
    .select({ openerId: supportTickets.openerId })
    .from(supportTickets)
    .where(eq(supportTickets.id, ticketId))
    .limit(1);
  if (rows.length === 0) return false;
  // The opener of the ticket is the only non-admin participant.
  // Admins are already gated above in canSubscribe (live:admin check).
  return rows[0]!.openerId === userId;
}

async function isCaseParticipant(db: Db, userId: string, caseId: string): Promise<boolean> {
  const rows = await db
    .select({
      customerId: transactionCases.customerId,
      vendorOwnerUserId: vendors.ownerUserId,
    })
    .from(transactionCases)
    .innerJoin(vendors, eq(transactionCases.vendorId, vendors.id))
    .where(eq(transactionCases.id, asCaseId(caseId)))
    .limit(1);
  if (rows.length === 0) return false;
  const row = rows[0]!;
  return row.customerId === userId || row.vendorOwnerUserId === userId;
}
