/*
 * KtaloguLive: a ponte com o Cloudflare Realtime (SFU e TURN).
 *
 * O navegador nunca fala com a Cloudflare usando a chave: ele manda a oferta
 * WebRTC para as nossas rotas, que conferem quem é, se pode publicar aquela
 * trilha, e só então repassam para o SFU com o segredo do app.
 *
 * Variáveis (só no servidor da loja):
 *   CLOUDFLARE_REALTIME_APP_ID, CLOUDFLARE_REALTIME_APP_SECRET   — o app do SFU
 *   CLOUDFLARE_TURN_KEY_ID, CLOUDFLARE_TURN_KEY_TOKEN             — o TURN (opcional)
 *
 * Sem as duas primeiras, `configuracaoDoSfu()` devolve null e a sala mostra
 * que a transmissão ainda não está disponível (nada quebra).
 */

const BASE = "https://rtc.live.cloudflare.com/v1";

export type ConfiguracaoDoSfu = { appId: string; segredo: string };

export function configuracaoDoSfu(env: Record<string, string | undefined> = process.env): ConfiguracaoDoSfu | null {
  const appId = String(env.CLOUDFLARE_REALTIME_APP_ID || "").trim();
  const segredo = String(env.CLOUDFLARE_REALTIME_APP_SECRET || "").trim();
  if (!appId || !segredo || !/^[a-z0-9]{8,64}$/i.test(appId)) return null;
  return { appId, segredo };
}

export type DescricaoDaSessao = { type: "offer" | "answer"; sdp: string };

export type TrilhaLocal = { location: "local"; mid: string; trackName: string };
export type TrilhaRemota = { location: "remote"; sessionId: string; trackName: string; mid?: string };

export type RespostaDasTrilhas = {
  sessionDescription?: DescricaoDaSessao;
  requiresImmediateRenegotiation?: boolean;
  tracks?: Array<{
    mid?: string;
    trackName?: string;
    sessionId?: string;
    error?: { errorCode?: string; errorDescription?: string };
    errorCode?: string;
    errorDescription?: string;
  }>;
  errorCode?: string;
  errorDescription?: string;
};

export class ErroDoSfu extends Error {
  readonly status: number;
  constructor(mensagem: string, status: number) {
    super(mensagem);
    this.status = status;
  }
}

async function chamar<T>(cfg: ConfiguracaoDoSfu, metodo: string, caminho: string, corpo?: unknown, prazoMs = 15000): Promise<T> {
  const resposta = await fetch(`${BASE}/apps/${cfg.appId}${caminho}`, {
    method: metodo,
    headers: { authorization: `Bearer ${cfg.segredo}`, "content-type": "application/json" },
    body: corpo === undefined ? undefined : JSON.stringify(corpo),
    cache: "no-store",
    signal: AbortSignal.timeout(prazoMs),
  }).catch((erro) => {
    /* A Cloudflare não respondeu a tempo: a negociação pode ter ficado pela metade, a tela abre outra conexão. */
    throw new ErroDoSfu(`sfu sem resposta: ${erro instanceof Error ? erro.name : "erro"}`, 504);
  });
  const texto = await resposta.text().catch(() => "");
  let json: unknown = null;
  try {
    json = texto ? JSON.parse(texto) : null;
  } catch {
    json = null;
  }
  if (!resposta.ok) {
    const detalhe = json && typeof json === "object" ? String((json as Record<string, unknown>).errorDescription || "") : "";
    throw new ErroDoSfu(`sfu ${resposta.status}${detalhe ? `: ${detalhe.slice(0, 160)}` : ""}`, resposta.status);
  }
  const lido = (json ?? {}) as T & { errorCode?: string; errorDescription?: string };
  if (lido && typeof lido === "object" && lido.errorCode) {
    throw new ErroDoSfu(`sfu: ${lido.errorCode} ${String(lido.errorDescription || "").slice(0, 160)}`, 502);
  }
  return lido;
}

/* Uma sessão = uma conexão WebRTC de um navegador. */
export async function novaSessao(cfg: ConfiguracaoDoSfu): Promise<string> {
  const r = await chamar<{ sessionId?: string }>(cfg, "POST", "/sessions/new");
  const id = String(r.sessionId || "");
  if (!/^[a-zA-Z0-9_-]{8,128}$/.test(id)) throw new ErroDoSfu("sfu: sessão sem id", 502);
  return id;
}

/* Publica trilhas do navegador (com a oferta dele) ou puxa trilhas de outra sessão. */
export function novasTrilhas(
  cfg: ConfiguracaoDoSfu,
  sessao: string,
  corpo: { sessionDescription?: DescricaoDaSessao; tracks: Array<TrilhaLocal | TrilhaRemota> },
  prazoMs?: number,
) {
  return chamar<RespostaDasTrilhas>(cfg, "POST", `/sessions/${encodeURIComponent(sessao)}/tracks/new`, corpo, prazoMs);
}

export function renegociar(cfg: ConfiguracaoDoSfu, sessao: string, descricao: DescricaoDaSessao) {
  return chamar<RespostaDasTrilhas>(cfg, "PUT", `/sessions/${encodeURIComponent(sessao)}/renegotiate`, {
    sessionDescription: descricao,
  });
}

export function fecharTrilhas(
  cfg: ConfiguracaoDoSfu,
  sessao: string,
  corpo: { tracks: Array<{ mid: string }>; sessionDescription?: DescricaoDaSessao; force: boolean },
) {
  return chamar<RespostaDasTrilhas>(cfg, "PUT", `/sessions/${encodeURIComponent(sessao)}/tracks/close`, corpo);
}

export type ServidorIce = { urls: string | string[]; username?: string; credential?: string };

const STUN_PADRAO: ServidorIce[] = [{ urls: "stun:stun.cloudflare.com:3478" }];

/* As credenciais do TURN vencem; guardamos por pouco tempo para não pedir a cada entrada. */
const memoria = globalThis as unknown as { __iceDaLive?: { servidores: ServidorIce[]; venceEm: number } };

export async function servidoresIce(env: Record<string, string | undefined> = process.env): Promise<ServidorIce[]> {
  const chave = String(env.CLOUDFLARE_TURN_KEY_ID || "").trim();
  const token = String(env.CLOUDFLARE_TURN_KEY_TOKEN || "").trim();
  if (!chave || !token || !/^[a-z0-9]{8,64}$/i.test(chave)) return STUN_PADRAO;
  const guardado = memoria.__iceDaLive;
  if (guardado && guardado.venceEm > Date.now()) return guardado.servidores;
  try {
    const resposta = await fetch(`${BASE}/turn/keys/${chave}/credentials/generate-ice-servers`, {
      method: "POST",
      headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
      body: JSON.stringify({ ttl: 6 * 3600 }),
      cache: "no-store",
      signal: AbortSignal.timeout(8000),
    });
    if (!resposta.ok) return STUN_PADRAO;
    const json = (await resposta.json().catch(() => null)) as { iceServers?: ServidorIce | ServidorIce[] } | null;
    const lista = json?.iceServers ? (Array.isArray(json.iceServers) ? json.iceServers : [json.iceServers]) : [];
    /* A porta 53 costuma travar no navegador (a própria Cloudflare avisa). */
    const servidores = lista
      .map((s) => {
        const urls = (Array.isArray(s.urls) ? s.urls : [s.urls]).filter((u) => typeof u === "string" && !/:53(\?|$)/.test(u));
        return { ...s, urls };
      })
      .filter((s) => s.urls.length > 0);
    if (!servidores.length) return STUN_PADRAO;
    /* Credencial vale 6 h; renova com folga, depois de 4 h. */
    memoria.__iceDaLive = { servidores, venceEm: Date.now() + 4 * 3600 * 1000 };
    return servidores;
  } catch {
    return STUN_PADRAO;
  }
}
