import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";

import type { SupabaseClient } from "@supabase/supabase-js";

import type { EstadoPublico, Fixado, MensagemDaSala, Oferta, PapelNaSala, PessoaNoPalco, TipoDeTrilha } from "../evento.ts";
import { TIPOS_DE_TRILHA } from "../evento.ts";
import { capacidadeResolvida, ehDaEquipe, lerOferta, momentoDaSala, type SalaParaRegras } from "../regras.ts";
import { avisarSala } from "../sinal.ts";

/*
 * A sala da live no servidor: ler, montar o estado que todo mundo vê, e
 * conferir quem está pedindo cada coisa.
 */

export type Admin = SupabaseClient;

export type SalaDoBanco = SalaParaRegras & {
  id: string;
  user_id: string;
  product_id: string | null;
  titulo: string;
  acesso: "paga" | "cadastro" | "aberta";
  vender_durante: boolean;
  limite_vagas: number | null;
  chat: "aberto" | "perguntas" | "desligado";
  gravar: boolean;
  gravacao_para: "compradores" | "ninguem";
  gravacao_modo: "palco" | "sala";
  categoria: "venda" | "reuniao";
  codigo: string;
  vaga: "gratis" | "pacote";
  vaga_compra_id: string | null;
  itens: unknown;
  vitrine: boolean;
  produtos_vitrine: string[];
  apresentador_nome: string | null;
  apresentador_bio: string | null;
  apresentador_foto: string | null;
  iniciada_em: string | null;
  encerrada_em: string | null;
  limite_pessoas: number | null;
  pico_pessoas: number;
  canal: string;
  convite_hash: string | null;
  estado: Record<string, unknown>;
  gravacao_status: "nenhuma" | "gravando" | "enviando" | "pronta" | "falhou" | "apagada";
  gravacao_chave: string | null;
  gravacao_envio: string | null;
  gravacao_partes: Array<{ numero: number; etag: string; tamanho?: number }>;
  gravacao_tipo: string | null;
  gravacao_bytes: number | null;
  gravacao_segundos: number | null;
};

export const CAMPOS_DA_SALA =
  "id,user_id,product_id,titulo,comeca_em,duracao_min,espera_min,acesso,vender_durante,limite_vagas,participacao,cameras_alunos,chat,gravar,gravacao_para,gravacao_modo,categoria,codigo,vaga,vaga_compra_id,itens,vitrine,produtos_vitrine,apresentador_nome,apresentador_bio,apresentador_foto,status,iniciada_em,encerrada_em,limite_pessoas,pico_pessoas,canal,convite_hash,estado,gravacao_status,gravacao_chave,gravacao_envio,gravacao_partes,gravacao_tipo,gravacao_bytes,gravacao_segundos";

export class ErroDaLive extends Error {
  readonly status: number;
  readonly extra?: Record<string, unknown>;
  constructor(mensagem: string, status = 400, extra?: Record<string, unknown>) {
    super(mensagem);
    this.status = status;
    this.extra = extra;
  }
}

const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

export function uuidValido(valor: unknown): valor is string {
  return typeof valor === "string" && UUID.test(valor);
}

export async function lerSala(admin: Admin, salaId: string): Promise<SalaDoBanco | null> {
  if (!uuidValido(salaId)) return null;
  const { data } = await admin.from("live_salas").select(CAMPOS_DA_SALA).eq("id", salaId).maybeSingle();
  return (data as SalaDoBanco | null) ?? null;
}

export async function exigirSala(admin: Admin, salaId: string): Promise<SalaDoBanco> {
  const sala = await lerSala(admin, salaId);
  if (!sala) throw new ErroDaLive("Esta live não existe mais.", 404);
  /* Só existe sala de loja liberada: quem cria é o painel, que confere a liberação. */
  return sala;
}

/* ------------------------------------------------------------------ chaves */

export function resumo(texto: string): string {
  return createHash("sha256").update(texto, "utf8").digest("hex");
}

function iguais(a: string, b: string): boolean {
  const x = Buffer.from(a, "utf8");
  const y = Buffer.from(b, "utf8");
  return x.length === y.length && timingSafeEqual(x, y);
}

/*
 * O link de quem apresenta: o painel assina "sala + prazo" com a chave do
 * servidor (a mesma conta nos dois apps) e a loja confere. Não fica no banco.
 */
function segredoDoApresentador(): Buffer | null {
  const chave = String(process.env.SUPABASE_SERVICE_ROLE_KEY || "").trim();
  return chave ? createHash("sha256").update(`ktalogu-live-apresentar:${chave}`).digest() : null;
}

export function chaveDoApresentadorValida(salaId: string, chave: unknown, agora = Date.now()): boolean {
  if (typeof chave !== "string" || !/^\d{10,14}\.[A-Za-z0-9_-]{20,64}$/.test(chave)) return false;
  const segredo = segredoDoApresentador();
  if (!segredo) return false;
  const [prazo, assinatura] = chave.split(".");
  if (Number(prazo) < agora) return false;
  const esperada = createHmac("sha256", segredo).update(`${salaId}.${prazo}`).digest("base64url");
  return iguais(esperada, assinatura);
}

export function conviteValido(sala: Pick<SalaDoBanco, "convite_hash">, convite: unknown): boolean {
  if (typeof convite !== "string" || !/^[a-f0-9]{40,80}$/.test(convite) || !sala.convite_hash) return false;
  return iguais(resumo(convite), sala.convite_hash);
}

/* ------------------------------------------------------------ participantes */

export type Participante = {
  id: string;
  sala_id: string;
  papel: PapelNaSala;
  nome: string;
  email: string | null;
  foto: string | null;
  client_id: string | null;
  empresa_id: string | null;
  /* A sessão do SFU por onde a pessoa manda e a por onde recebe (duas conexões). */
  sfu_sessao: string | null;
  sfu_sessao_recebe: string | null;
  trilhas: Partial<Record<TipoDeTrilha, { mid: string; nome: string }>>;
  pode_falar: boolean;
  mao_em: string | null;
  bloqueado: boolean;
  visto_em: string;
  saiu_em: string | null;
};

export const CAMPOS_DO_PARTICIPANTE =
  "id,sala_id,papel,nome,email,foto,client_id,empresa_id,sfu_sessao,sfu_sessao_recebe,trilhas,pode_falar,mao_em,bloqueado,visto_em,saiu_em";

/* Sem sinal por este tempo, a pessoa saiu (fechou a aba, caiu a internet). */
export const ONLINE_MS = 45 * 1000;

export async function pessoasOnline(admin: Admin, salaId: string): Promise<Participante[]> {
  const desde = new Date(Date.now() - ONLINE_MS).toISOString();
  const { data } = await admin
    .from("live_participantes")
    .select(CAMPOS_DO_PARTICIPANTE)
    .eq("sala_id", salaId)
    .is("saiu_em", null)
    .eq("bloqueado", false)
    .gte("visto_em", desde)
    .order("entrou_em", { ascending: true })
    .limit(3000);
  return (data ?? []) as Participante[];
}

export function contarPublico(pessoas: Participante[]): number {
  return pessoas.filter((p) => !ehDaEquipe(p.papel)).length;
}

/* Lê a credencial "id.token" que a tela manda em cada pedido. */
export async function participanteDaCredencial(admin: Admin, salaId: string, credencial: unknown): Promise<Participante | null> {
  if (typeof credencial !== "string") return null;
  const [id, token] = credencial.split(".");
  if (!uuidValido(id) || !token || !/^[a-f0-9]{64}$/.test(token)) return null;
  const { data } = await admin
    .from("live_participantes")
    .select(`${CAMPOS_DO_PARTICIPANTE},token_hash`)
    .eq("id", id)
    .eq("sala_id", salaId)
    .maybeSingle();
  const linha = data as (Participante & { token_hash: string }) | null;
  if (!linha || !iguais(resumo(token), linha.token_hash)) return null;
  const { token_hash: _ignorado, ...participante } = linha;
  void _ignorado;
  return participante;
}

export async function exigirParticipante(admin: Admin, sala: Pick<SalaDoBanco, "id">, req: Request): Promise<Participante> {
  const participante = await participanteDaCredencial(admin, sala.id, req.headers.get("x-live-quem"));
  if (!participante) throw new ErroDaLive("Entre na sala de novo.", 401, { motivo: "sem_credencial" });
  if (participante.bloqueado) throw new ErroDaLive("A loja tirou você desta live.", 403, { motivo: "bloqueado" });
  if (participante.saiu_em) throw new ErroDaLive("Você entrou nesta live em outro lugar.", 409, { motivo: "outro_lugar" });
  return participante;
}

export function exigirEquipe(participante: Participante) {
  if (!ehDaEquipe(participante.papel)) throw new ErroDaLive("Só quem apresenta pode fazer isso.", 403);
}

export function novoToken(): string {
  return randomBytes(32).toString("hex");
}

/* ------------------------------------------------------------------ estado */

function lerFixado(bruto: unknown): Fixado | null {
  if (!bruto || typeof bruto !== "object") return null;
  const f = bruto as Record<string, unknown>;
  const texto = typeof f.texto === "string" ? f.texto.trim().slice(0, 240) : "";
  if (!texto) return null;
  const link = typeof f.link === "string" && /^https:\/\//.test(f.link) ? f.link.slice(0, 1000) : null;
  const rotulo = typeof f.rotuloDoLink === "string" ? f.rotuloDoLink.trim().slice(0, 40) || null : null;
  return { texto, link, rotuloDoLink: link ? rotulo : null };
}

function ofertaValendo(bruto: unknown, agora: number): Oferta | null {
  const oferta = lerOferta(bruto);
  if (!oferta) return null;
  if (oferta.ate && new Date(oferta.ate).getTime() < agora) return null;
  return oferta;
}

export function montarEstado(sala: SalaDoBanco, pessoas: Participante[], agora = Date.now()): EstadoPublico {
  const momento = momentoDaSala(sala, agora);
  /* Na reunião todo mundo aparece (como no Meet); na live, só quem apresenta e quem liga algo. */
  const palco: PessoaNoPalco[] = pessoas
    .filter((p) => sala.categoria === "reuniao" || ehDaEquipe(p.papel) || Object.keys(p.trilhas ?? {}).length > 0)
    .slice(0, 60)
    .map((p) => {
      const trilhas: Partial<Record<TipoDeTrilha, string>> = {};
      for (const tipo of TIPOS_DE_TRILHA) {
        const t = p.trilhas?.[tipo];
        if (t?.nome) trilhas[tipo] = t.nome;
      }
      return { id: p.id, nome: p.nome, papel: p.papel, foto: p.foto, trilhas };
    });
  const estado = sala.estado ?? {};
  return {
    v: agora,
    status: momento === "encerrada" ? "encerrada" : sala.status === "cancelada" ? "cancelada" : sala.status,
    iniciadaEm: sala.iniciada_em,
    encerradaEm: sala.encerrada_em,
    palco,
    falando: pessoas.filter((p) => p.pode_falar && !ehDaEquipe(p.papel)).map((p) => p.id),
    maos: pessoas
      .filter((p) => p.mao_em && !ehDaEquipe(p.papel))
      .sort((a, b) => String(a.mao_em).localeCompare(String(b.mao_em)))
      .slice(0, 50)
      .map((p) => ({ id: p.id, nome: p.nome, em: String(p.mao_em) })),
    oferta: ofertaValendo(estado.oferta, agora),
    foco: ofertaValendo(estado.foco, agora),
    fixado: lerFixado(estado.fixado),
    /* Live que acabou não está gravando, mesmo que o arquivo não tenha fechado direito. */
    gravando: sala.gravacao_status === "gravando" && momento !== "encerrada" && sala.status !== "cancelada",
    pessoas: contarPublico(pessoas),
    chat: sala.chat,
  };
}

/* Quantas pessoas ficaram no último aviso de cada sala (para não avisar a cada sinal). */
const memoria = globalThis as unknown as { __pessoasDaLive?: Map<string, { pessoas: number; em: number }> };
const ultimosAvisos = (memoria.__pessoasDaLive ??= new Map());

/* O último estado montado de cada sala (o sinal de vida devolve sem ir ao banco). */
const memoriaDoEstado = globalThis as unknown as { __estadoDaLive?: Map<string, EstadoPublico> };
const estados = (memoriaDoEstado.__estadoDaLive ??= new Map());

function guardarEstado(salaId: string, estado: EstadoPublico) {
  estados.set(salaId, estado);
  if (estados.size > 500) estados.delete(estados.keys().next().value as string);
}

/* O estado de agora: o guardado se tem menos de 8 s, senão monta de novo (sem avisar). */
export async function estadoAtual(admin: Admin, salaId: string): Promise<EstadoPublico | null> {
  const guardado = estados.get(salaId);
  if (guardado && Date.now() - guardado.v < 8000) return guardado;
  const [sala, pessoas] = await Promise.all([lerSala(admin, salaId), pessoasOnline(admin, salaId)]);
  if (!sala) return null;
  const estado = montarEstado(sala, pessoas);
  guardarEstado(salaId, estado);
  return estado;
}

/* Lê tudo de novo e manda o estado para a sala. Devolve o estado montado. */
export async function publicarEstado(admin: Admin, salaId: string): Promise<EstadoPublico | null> {
  /* Em paralelo: o banco fica longe, cada ida conta. */
  const [sala, pessoas] = await Promise.all([lerSala(admin, salaId), pessoasOnline(admin, salaId)]);
  if (!sala) return null;
  const estado = montarEstado(sala, pessoas);
  guardarEstado(salaId, estado);
  ultimosAvisos.set(salaId, { pessoas: estado.pessoas, em: Date.now() });
  if (ultimosAvisos.size > 500) ultimosAvisos.delete(ultimosAvisos.keys().next().value as string);
  /* O aviso sai primeiro; o recorde de pessoas é anotado ao mesmo tempo. */
  /* O aviso segue sozinho: a resposta de quem clicou não espera a Supabase. */
  await avisarSala(admin, { id: sala.id, canal: sala.canal }, [{ t: "estado", estado }]);
  await Promise.all([
    estado.pessoas > (sala.pico_pessoas ?? 0)
      ? admin.from("live_salas").update({ pico_pessoas: estado.pessoas }).eq("id", salaId).lt("pico_pessoas", estado.pessoas)
      : Promise.resolve(null),
  ]);
  return estado;
}

/* O sinal de vida mudou a contagem? Avisa no máximo a cada 15 s. */
export async function talvezAvisarPessoas(admin: Admin, salaId: string): Promise<void> {
  const ultimo = ultimosAvisos.get(salaId);
  if (ultimo && Date.now() - ultimo.em < 15000) return;
  const desde = new Date(Date.now() - ONLINE_MS).toISOString();
  const { count } = await admin
    .from("live_participantes")
    .select("id", { count: "exact", head: true })
    .eq("sala_id", salaId)
    .is("saiu_em", null)
    .eq("bloqueado", false)
    .in("papel", ["aluno", "visitante"])
    .gte("visto_em", desde);
  if (ultimo && (count ?? 0) === ultimo.pessoas) {
    ultimosAvisos.set(salaId, { pessoas: ultimo.pessoas, em: Date.now() });
    return;
  }
  await publicarEstado(admin, salaId);
}

/* Muda o que está na tela (oferta, foco, fixado) e avisa. */
export async function mudarEstado(admin: Admin, sala: SalaDoBanco, mudancas: Record<string, unknown>) {
  const novo = { ...(sala.estado ?? {}), ...mudancas };
  for (const [chave, valor] of Object.entries(novo)) if (valor === null || valor === undefined) delete novo[chave];
  /* Grava e lê as pessoas ao mesmo tempo; o estado novo sai da sala que já temos (sem ler de novo). */
  const [, pessoas] = await Promise.all([
    admin.from("live_salas").update({ estado: novo, atualizado_em: new Date().toISOString() }).eq("id", sala.id),
    pessoasOnline(admin, sala.id),
  ]);
  const estado = montarEstado({ ...sala, estado: novo }, pessoas);
  guardarEstado(sala.id, estado);
  await avisarSala(admin, { id: sala.id, canal: sala.canal }, [{ t: "estado", estado }]);
  return estado;
}

/* ---------------------------------------------------------------- mensagens */

export function mensagemParaTela(linha: {
  id: string;
  participante_id: string | null;
  nome: string;
  papel: string;
  foto: string | null;
  texto: string;
  pergunta: boolean;
  criado_em: string;
}): MensagemDaSala {
  return {
    id: linha.id,
    autor: linha.participante_id ?? "",
    nome: linha.nome,
    papel: (["apresentador", "convidado", "aluno", "visitante"].includes(linha.papel) ? linha.papel : "visitante") as PapelNaSala,
    foto: linha.foto,
    texto: linha.texto,
    pergunta: linha.pergunta,
    em: linha.criado_em,
  };
}

/*
 * As mensagens que esta pessoa pode ver: no modo "só perguntas", a pergunta
 * de um aluno aparece só para quem apresenta e para quem perguntou.
 */
export async function mensagensRecentes(
  admin: Admin,
  sala: SalaDoBanco,
  quem: Participante,
  desde?: string | null,
): Promise<MensagemDaSala[]> {
  let consulta = admin
    .from("live_mensagens")
    .select("id,participante_id,nome,papel,foto,texto,pergunta,criado_em")
    .eq("sala_id", sala.id)
    .is("apagada_em", null);
  if (desde && Number.isFinite(new Date(desde).getTime())) consulta = consulta.gt("criado_em", desde);
  if (!ehDaEquipe(quem.papel)) consulta = consulta.or(`pergunta.eq.false,participante_id.eq.${quem.id}`);
  const { data } = await consulta.order("criado_em", { ascending: false }).limit(80);
  return ((data ?? []) as Array<Parameters<typeof mensagemParaTela>[0]>).reverse().map(mensagemParaTela);
}

/*
 * A sessão do SFU desta pessoa. A Cloudflare derruba sessão que não negocia
 * logo, então ela nasce aqui, na primeira vez que a tela manda ou recebe
 * vídeo (e não ao entrar na sala).
 */
export type LadoDaConexao = "envio" | "recebe";

export function ladoDaConexao(valor: unknown): LadoDaConexao {
  return valor === "recebe" ? "recebe" : "envio";
}

export async function sessaoDoSfu(admin: Admin, eu: Participante, criar: () => Promise<string>, lado: LadoDaConexao = "envio"): Promise<string> {
  const atual = lado === "recebe" ? eu.sfu_sessao_recebe : eu.sfu_sessao;
  if (atual) return atual;
  const sessao = await criar();
  if (lado === "recebe") {
    await admin.from("live_participantes").update({ sfu_sessao_recebe: sessao }).eq("id", eu.id);
    eu.sfu_sessao_recebe = sessao;
  } else {
    await admin.from("live_participantes").update({ sfu_sessao: sessao }).eq("id", eu.id);
    eu.sfu_sessao = sessao;
  }
  return sessao;
}

/* Fecha no SFU as trilhas que alguém manda (microfone de quem foi silenciado, tela de quem perdeu a vez). */
export async function cortarTrilhas(pessoa: Participante, tipos: TipoDeTrilha[], fechar: (sessao: string, mids: string[]) => Promise<unknown>) {
  const trilhas = { ...(pessoa.trilhas ?? {}) };
  const mids = tipos.map((t) => trilhas[t]?.mid).filter((m): m is string => Boolean(m));
  if (pessoa.sfu_sessao && mids.length) await fechar(pessoa.sfu_sessao, mids).catch(() => null);
  for (const t of tipos) delete trilhas[t];
  return trilhas;
}

/* -------------------------------------------------------------- capacidade */

/*
 * Quantas pessoas cabem (Infinity = sem limite). A cortesia da empresa vale
 * antes de tudo, em qualquer momento da sala; sem ela, depois de ir ao ar, o
 * que a vaga reservou; antes, o que a vaga escolhida vai dar (para a sala de
 * espera não lotar além). A conta fica em capacidadeResolvida (regras.ts).
 */
export async function capacidadeDaSala(admin: Admin, sala: SalaDoBanco): Promise<number> {
  const pacote = sala.vaga === "pacote" && sala.vaga_compra_id ? sala.vaga_compra_id : null;
  const [cortesia, compra, regras] = await Promise.all([
    admin.from("live_cortesias").select("pessoas").eq("empresa_id", sala.user_id).maybeSingle(),
    !sala.limite_pessoas && pacote ? admin.from("live_compras").select("pessoas").eq("id", pacote).maybeSingle() : null,
    !sala.limite_pessoas ? admin.from("live_regras").select("gratis_pessoas").eq("id", 1).maybeSingle() : null,
  ]);
  const daCortesia = !cortesia.error && cortesia.data ? (cortesia.data as { pessoas: number | null }) : null;
  return capacidadeResolvida({
    cortesia: daCortesia ? { pessoas: daCortesia.pessoas ?? null } : null,
    limiteDaSala: sala.limite_pessoas,
    pessoasDoPacote: (compra?.data as { pessoas?: number } | null | undefined)?.pessoas ?? null,
    gratisPessoas: (regras?.data as { gratis_pessoas?: number } | null | undefined)?.gratis_pessoas ?? null,
  });
}

/* ----------------------------------------------------------------- produto */

type ProdutoDaVitrine = {
  id: string;
  name: string;
  price: number | null;
  price_on_request?: boolean | null;
  image_url: string | null;
  active: boolean | null;
  show_in_catalog?: boolean | null;
};

const CAMPOS_DO_PRODUTO = "id,name,price,price_on_request,image_url,active,show_in_catalog";

function produtoParaOferta(produto: ProdutoDaVitrine, slug: string): Oferta | null {
  if (produto.active === false || produto.show_in_catalog === false) return null;
  /* Preço sob consulta não aparece: o botão leva à página, que explica. */
  const preco = !produto.price_on_request && Number(produto.price) > 0 ? Number(produto.price) : null;
  return lerOferta({
    id: `p-${produto.id}`,
    tipo: "produto",
    titulo: produto.name,
    detalhe: null,
    imagem: produto.image_url,
    preco,
    precoDe: null,
    destino: `/${slug}/p/${produto.id}`,
  });
}

/* O produto da loja como item de liveshop (para o "produto em foco"). */
export async function produtoComoOferta(admin: Admin, sala: SalaDoBanco, produtoId: string, slug: string): Promise<Oferta | null> {
  if (!uuidValido(produtoId)) return null;
  const { data } = await admin.from("products").select(CAMPOS_DO_PRODUTO).eq("id", produtoId).eq("user_id", sala.user_id).maybeSingle();
  return data ? produtoParaOferta(data as ProdutoDaVitrine, slug) : null;
}

/* Os produtos do liveshop, na ordem que a loja escolheu. */
export async function produtosDaVitrine(admin: Admin, sala: SalaDoBanco, slug: string): Promise<Oferta[]> {
  const ids = (sala.produtos_vitrine ?? []).filter(uuidValido).slice(0, 40);
  if (!ids.length) return [];
  const { data } = await admin.from("products").select(CAMPOS_DO_PRODUTO).eq("user_id", sala.user_id).in("id", ids);
  const porId = new Map(((data ?? []) as ProdutoDaVitrine[]).map((p) => [p.id, p]));
  return ids
    .map((id) => porId.get(id))
    .map((p) => (p ? produtoParaOferta(p, slug) : null))
    .filter((o): o is Oferta => Boolean(o));
}

/* O slug da loja (para os links da sala). */
export async function slugDaLoja(admin: Admin, lojaId: string): Promise<string> {
  const { data } = await admin.from("profiles").select("slug").eq("id", lojaId).maybeSingle();
  return String((data as { slug?: string | null } | null)?.slug || "");
}
