import { AsyncLocalStorage } from "node:async_hooks";
import { createPrivateKey, createPublicKey, generateKeyPairSync, sign, type KeyObject } from "node:crypto";

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

import { montarEvento, type EventoDaSala } from "./evento.ts";

/*
 * O "alto-falante" da sala: o servidor avisa todo mundo (bate-papo, palco,
 * mão levantada, oferta) pelo broadcast do Supabase Realtime.
 *
 * Qualquer pessoa que conheça o nome do canal consegue mandar mensagem nele,
 * então TODO aviso do servidor sai assinado (ECDSA P-256). O navegador só
 * acredita no que confere com a chave pública que recebeu ao entrar; o resto
 * (as reações que voam na tela) é inofensivo e vai sem assinatura.
 *
 * O par de chaves nasce sozinho na primeira vez e fica em `live_chaves`, que
 * só a chave de serviço lê.
 */

type Chaves = { privada: KeyObject; publica: JsonWebKey };

const memoria = globalThis as unknown as { __chavesDaLive?: Chaves };

export async function chavesDaLive(admin: SupabaseClient): Promise<Chaves> {
  if (memoria.__chavesDaLive) return memoria.__chavesDaLive;
  const ler = async () => {
    const { data } = await admin.from("live_chaves").select("privada,publica").eq("id", 1).maybeSingle();
    return data as { privada: string; publica: JsonWebKey } | null;
  };
  let linha = await ler();
  if (!linha) {
    const par = generateKeyPairSync("ec", { namedCurve: "prime256v1" });
    const privada = par.privateKey.export({ format: "pem", type: "pkcs8" }).toString();
    const publica = par.publicKey.export({ format: "jwk" }) as JsonWebKey;
    /* Duas entradas ao mesmo tempo: a segunda não sobrescreve, lê a que ficou. */
    await admin.from("live_chaves").upsert({ id: 1, privada, publica }, { onConflict: "id", ignoreDuplicates: true });
    linha = await ler();
    if (!linha) throw new Error("live: não foi possível guardar as chaves");
  }
  const privada = createPrivateKey(linha.privada);
  /* A pública sai da privada: se alguém mexer só na coluna pública, não engana ninguém. */
  const publica = createPublicKey(privada).export({ format: "jwk" }) as JsonWebKey;
  const chaves = { privada, publica: { kty: publica.kty, crv: publica.crv, x: publica.x, y: publica.y } };
  memoria.__chavesDaLive = chaves;
  return chaves;
}

export function assinar(privada: KeyObject, texto: string): string {
  return sign("sha256", Buffer.from(texto, "utf8"), { key: privada, dsaEncoding: "ieee-p1363" }).toString("base64");
}

/* O nome do tópico no Realtime para o canal secreto da sala. */
export function topicoDaSala(canal: string): string {
  return `live-${canal}`;
}

/* O aviso assinado, do jeito que viaja no canal. */
export type AvisoAssinado = { d: string; s: string };

/*
 * Os avisos assinados de cada pedido: a rota devolve junto da resposta e quem
 * clicou repassa na hora pelo canal que já está aberto no navegador (chega em
 * milissegundos). O envio pelo servidor segue por trás, como garantia; a tela
 * de quem recebe os dois ignora o repetido.
 */
const coletor = new AsyncLocalStorage<AvisoAssinado[]>();

export function comAvisos<T>(executar: () => Promise<T>): Promise<{ resultado: T; avisos: AvisoAssinado[] }> {
  const avisos: AvisoAssinado[] = [];
  return coletor.run(avisos, async () => ({ resultado: await executar(), avisos }));
}

/*
 * Manda um ou mais avisos assinados para a sala. Só espera a assinatura: o
 * envio pela Supabase segue sozinho (às vezes passa de 8 s). Falhar aqui nunca
 * derruba a rota: o banco já guardou, e a tela confere de novo de tempos em tempos.
 */
export async function avisarSala(admin: SupabaseClient, sala: { id: string; canal: string }, eventos: EventoDaSala[]): Promise<void> {
  const url = process.env.NEXT_PUBLIC_SUPABASE_URL ?? process.env.SUPABASE_URL;
  const chave = process.env.SUPABASE_SERVICE_ROLE_KEY;
  if (!url || !chave || !eventos.length) return;
  let assinados: AvisoAssinado[];
  try {
    const { privada } = await chavesDaLive(admin);
    assinados = eventos.map((evento) => {
      const d = montarEvento({ ...evento, sala: sala.id });
      return { d, s: assinar(privada, d) };
    });
  } catch (erro) {
    console.error("[live] aviso para a sala não foi assinado", erro instanceof Error ? erro.message : erro);
    return;
  }
  coletor.getStore()?.push(...assinados);
  const topico = topicoDaSala(sala.canal);
  void fetch(`${url.replace(/\/+$/, "")}/realtime/v1/api/broadcast`, {
    method: "POST",
    headers: { apikey: chave.trim(), authorization: `Bearer ${chave.trim()}`, "content-type": "application/json" },
    body: JSON.stringify({ messages: assinados.map((payload) => ({ topic: topico, event: "sala", payload, private: false })) }),
    cache: "no-store",
    signal: AbortSignal.timeout(20000),
  }).catch((erro) => console.error("[live] aviso para a sala não saiu", erro instanceof Error ? erro.message : erro));
}

/* Assina um texto qualquer com a chave da sala (a licença da caneta). */
export async function assinarTexto(admin: SupabaseClient, texto: string): Promise<string> {
  const { privada } = await chavesDaLive(admin);
  return assinar(privada, texto);
}
