import { randomUUID } from "node:crypto";

import { NextResponse } from "next/server";

import { abrirEnvio, cancelarEnvio, enderecoDaParte, fecharEnvio, fichaDoObjeto, configuracaoDoR2, partesRecebidas } from "@/lib/digitais/r2";
import { conferirPartes, partesParaFechar, resumoDasPartes } from "@/lib/live/gravacao-partes";
import { pedidoInternoValido } from "@/lib/live/interno";
import { createAdminClient } from "@/lib/supabase-server";

export const runtime = "nodejs";

const SEM_CACHE = { "cache-control": "no-store" };
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

function resposta(corpo: unknown, status = 200) {
  return NextResponse.json(corpo, { status, headers: SEM_CACHE });
}

type Sala = {
  id: string;
  user_id: string;
  titulo: string;
  gravar: boolean;
  gravacao_status: string;
  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;
};

const TIPOS: Record<string, string> = { "video/mp4": "mp4", "video/webm": "webm" };

/* O id do arquivo que está sendo gravado (a 2ª metade da chave): é com ele que o navegador diz de qual gravação é a parte. */
function idDaGravacao(chave: string | null): string | null {
  return chave ? chave.split("/")[1] ?? null : null;
}

/*
 * A gravação da live chega da loja (127.0.0.1, pedido assinado): só o painel
 * tem a chave que escreve no depósito. Cada parte vai para o R2 por um
 * endereço assinado; no fim, o arquivo vira um arquivo da loja (aparece em
 * "Arquivos e espaço") ligado à live pelo `meta.live`.
 */
export async function POST(req: Request) {
  if (!pedidoInternoValido(req)) return resposta({ error: "Pedido recusado." }, 401);
  const cfg = configuracaoDoR2();
  if (!cfg) return resposta({ error: "O depósito de arquivos ainda não está ligado." }, 503);

  const url = new URL(req.url);
  const salaId = url.searchParams.get("sala") || "";
  const acao = url.searchParams.get("acao") || "";
  if (!UUID.test(salaId)) return resposta({ error: "Sala inválida." }, 400);

  const admin = createAdminClient();
  const { data } = await admin
    .from("live_salas")
    .select("id,user_id,titulo,gravar,gravacao_status,gravacao_chave,gravacao_envio,gravacao_partes,gravacao_tipo,gravacao_bytes,gravacao_segundos")
    .eq("id", salaId)
    .maybeSingle();
  const sala = data as Sala | null;
  if (!sala) return resposta({ error: "Esta live não existe mais." }, 404);
  /* Só abrir exige a gravação ligada; fechar e cancelar o que já está aberto sempre pode. */
  if (!sala.gravar && acao === "abrir") return resposta({ error: "Esta live não é gravada." }, 403);
  const agora = new Date().toISOString();

  try {
    if (acao === "abrir") {
      const corpo = (await req.json().catch(() => null)) as { tipo?: unknown } | null;
      const tipo = typeof corpo?.tipo === "string" && TIPOS[corpo.tipo.split(";")[0]] ? corpo.tipo.split(";")[0] : "video/mp4";
      const anterior = sala.gravacao_envio && sala.gravacao_chave ? { chave: sala.gravacao_chave, envio: sala.gravacao_envio } : null;
      const chave = `${sala.user_id}/${randomUUID()}`;
      const envio = await abrirEnvio(cfg, chave, tipo);
      /*
       * Troca só se ninguém trocou antes: dois "abrir" no mesmo segundo (17/10)
       * deixaram dois gravadores mandando partes para o MESMO envio, e o R2
       * recusou juntar. Quem perde desiste do envio que acabou de abrir.
       */
      const troca = admin
        .from("live_salas")
        .update({ gravacao_status: "gravando", gravacao_chave: chave, gravacao_envio: envio, gravacao_partes: [], gravacao_tipo: tipo, atualizado_em: agora })
        .eq("id", sala.id);
      const { data: trocou, error: erroDaTroca } = await (anterior ? troca.eq("gravacao_envio", anterior.envio) : troca.is("gravacao_envio", null)).select("id");
      if (erroDaTroca || !trocou?.length) {
        await cancelarEnvio(cfg, chave, envio).catch(() => null);
        return resposta({ error: "Outra gravação desta live começou agora.", motivo: "outra_gravacao" }, 409);
      }
      if (anterior) {
        if (sala.gravacao_status === "falhou") {
          /* Não fechou no fim: as partes ficam no depósito (o R2 desfaz sozinho em 7 dias) para dar para recuperar. */
          console.error(
            "[live] gravação que não fechou ficou guardada",
            JSON.stringify({ sala: sala.id, chave: anterior.chave, envio: anterior.envio, ...resumoDasPartes(sala.gravacao_partes ?? []) }),
          );
        } else {
          /* Um arquivo que ficou aberto (a aba fechou no meio) não tem como ser
             aproveitado: o fim do MP4 nunca chegou. Libera as partes. */
          await cancelarEnvio(cfg, anterior.chave, anterior.envio).catch(() => null);
        }
      }
      return resposta({ ok: true, gravacao: idDaGravacao(chave) });
    }

    /* De qual gravação o navegador está falando (a tela antiga, ainda aberta em algum navegador, não manda: vale a aberta). */
    const daGravacao = url.searchParams.get("g");
    if (!sala.gravacao_envio || !sala.gravacao_chave) {
      /* Fechar de novo o que já fechou (a resposta se perdeu no caminho): deu certo. */
      if (acao === "fechar" && daGravacao && UUID.test(daGravacao)) {
        const { data: arquivo } = await admin.from("digital_files").select("id,tamanho_bytes").eq("id", daGravacao).eq("user_id", sala.user_id).maybeSingle();
        if (arquivo) return resposta({ ok: true, jaFechada: true, tamanho: Number((arquivo as { tamanho_bytes?: number }).tamanho_bytes ?? 0) });
      }
      if (acao === "cancelar") return resposta({ ok: true });
      return resposta({ error: "Nenhuma gravação aberta.", motivo: "sem_gravacao" }, 409);
    }
    if (daGravacao && daGravacao !== idDaGravacao(sala.gravacao_chave)) {
      /* Um gravador velho (outra aba, ou a tela que recomeçou) não mexe na gravação de agora. */
      if (acao === "cancelar") return resposta({ ok: true });
      return resposta({ error: "Esta gravação foi substituída por outra.", motivo: "substituida" }, 409);
    }

    if (acao === "parte") {
      const n = Number(url.searchParams.get("n"));
      if (!Number.isInteger(n) || n < 1 || n > 10000) return resposta({ error: "Parte inválida." }, 400);
      const bytes = await req.arrayBuffer();
      if (!bytes.byteLength || bytes.byteLength > 10 * 1024 * 1024) return resposta({ error: "Parte inválida." }, 413);
      const envio = await fetch(enderecoDaParte(cfg, sala.gravacao_chave, sala.gravacao_envio, n, 900), {
        method: "PUT",
        body: bytes,
        cache: "no-store",
        signal: AbortSignal.timeout(110000),
      });
      const etag = envio.headers.get("etag");
      if (!envio.ok || !etag) return resposta({ error: "A parte não chegou ao depósito." }, 502);
      /* Leio de novo antes de gravar: a lista pode ter mudado desde a leitura de cima. */
      const { data: atual } = await admin.from("live_salas").select("gravacao_partes,gravacao_envio").eq("id", sala.id).maybeSingle();
      const linha = atual as { gravacao_partes?: Sala["gravacao_partes"]; gravacao_envio?: string | null } | null;
      if (linha?.gravacao_envio !== sala.gravacao_envio) return resposta({ error: "A gravação foi fechada." }, 409);
      const partes = (linha?.gravacao_partes ?? []).filter((p) => p.numero !== n);
      partes.push({ numero: n, etag, tamanho: bytes.byteLength });
      partes.sort((a, b) => a.numero - b.numero);
      await admin.from("live_salas").update({ gravacao_partes: partes }).eq("id", sala.id).eq("gravacao_envio", sala.gravacao_envio);
      return resposta({ ok: true, partes: partes.length });
    }

    if (acao === "fechar") {
      const corpo = (await req.json().catch(() => null)) as { segundos?: unknown } | null;
      const segundos = Math.max(0, Math.min(24 * 3600, Math.round(Number(corpo?.segundos) || 0)));
      const doBanco = (sala.gravacao_partes ?? []).slice().sort((a, b) => a.numero - b.numero);
      if (!doBanco.length) {
        await cancelarEnvio(cfg, sala.gravacao_chave, sala.gravacao_envio).catch(() => null);
        await admin.from("live_salas").update({ gravacao_status: "nenhuma", gravacao_chave: null, gravacao_envio: null, gravacao_partes: [] }).eq("id", sala.id);
        return resposta({ ok: true, vazia: true });
      }
      /* O depósito é a verdade: as etiquetas e os tamanhos que ele guardou (o banco pode ter ficado para trás). */
      const chaveAberta = sala.gravacao_chave;
      const envioAberto = sala.gravacao_envio;
      const doDeposito = await partesRecebidas(cfg, chaveAberta, envioAberto).catch((erro) => {
        console.error("[live] gravação: listar as partes", erro instanceof Error ? erro.message : erro);
        return null;
      });
      const { partes, fonte } = partesParaFechar(doBanco, doDeposito);
      const naoFechou = async (motivo: string) => {
        /* Nada se apaga: a sala guarda o envio e as partes, para tentar de novo ou recuperar. */
        console.error(
          "[live] gravação não fechou",
          JSON.stringify({ sala: sala.id, chave: chaveAberta, envio: envioAberto, motivo, fonte, noBanco: doBanco.length, ...resumoDasPartes(partes) }),
        );
        await admin
          .from("live_salas")
          .update({ gravacao_status: "falhou", gravacao_partes: partes, atualizado_em: agora })
          .eq("id", sala.id)
          .eq("gravacao_envio", envioAberto);
        return resposta({ error: "A gravação não fechou. As partes ficaram guardadas para recuperar.", motivo: "nao_fechou", guardada: true }, 502);
      };
      const conferencia = conferirPartes(partes, partes.every((p) => typeof p.tamanho === "number"));
      if (!conferencia.ok) return naoFechou(conferencia.motivo);
      try {
        await fecharEnvio(cfg, chaveAberta, envioAberto, partes.map((p) => ({ numero: p.numero, etag: p.etag })));
      } catch (erro) {
        return naoFechou(erro instanceof Error ? erro.message : String(erro));
      }
      const ficha = await fichaDoObjeto(cfg, sala.gravacao_chave);
      const tamanho = ficha?.tamanho ?? partes.reduce((soma, p) => soma + (p.tamanho ?? 0), 0);
      const extensao = TIPOS[sala.gravacao_tipo ?? "video/mp4"] ?? "mp4";
      const { count } = await admin
        .from("digital_files")
        .select("id", { count: "exact", head: true })
        .eq("user_id", sala.user_id)
        .eq("meta->>live", sala.id);
      const parte = (count ?? 0) + 1;
      const nome = `Gravação · ${sala.titulo}${parte > 1 ? ` (parte ${parte})` : ""}.${extensao}`.slice(0, 200);
      const { error } = await admin.from("digital_files").insert({
        id: sala.gravacao_chave.split("/")[1],
        user_id: sala.user_id,
        storage_key: sala.gravacao_chave,
        nome,
        extensao,
        mime: sala.gravacao_tipo ?? "video/mp4",
        categoria: "video",
        tamanho_bytes: Math.max(1, tamanho),
        status: "pronto",
        meta: { live: sala.id, parte, segundos },
      });
      if (error) {
        console.error("[live] gravação: ficha do arquivo", error.message);
        return resposta({ error: "A gravação foi guardada, mas não entrou na lista de arquivos." }, 500);
      }
      await admin
        .from("live_salas")
        .update({
          gravacao_status: "pronta",
          gravacao_chave: null,
          gravacao_envio: null,
          gravacao_partes: [],
          gravacao_bytes: Number(sala.gravacao_bytes ?? 0) + tamanho,
          gravacao_segundos: Number(sala.gravacao_segundos ?? 0) + segundos,
          atualizado_em: agora,
        })
        .eq("id", sala.id);
      return resposta({ ok: true, parte, tamanho });
    }

    if (acao === "cancelar") {
      /* A que não fechou no fim fica guardada para recuperar: um cancelar não a desfaz. */
      if (sala.gravacao_status === "falhou") return resposta({ ok: true, guardada: true });
      await cancelarEnvio(cfg, sala.gravacao_chave, sala.gravacao_envio).catch(() => null);
      const { count } = await admin
        .from("digital_files")
        .select("id", { count: "exact", head: true })
        .eq("user_id", sala.user_id)
        .eq("meta->>live", sala.id);
      await admin
        .from("live_salas")
        .update({ gravacao_status: (count ?? 0) > 0 ? "pronta" : "nenhuma", gravacao_chave: null, gravacao_envio: null, gravacao_partes: [] })
        .eq("id", sala.id);
      return resposta({ ok: true });
    }

    return resposta({ error: "Ação desconhecida." }, 400);
  } catch (erro) {
    console.error("[live] gravação", acao, erro instanceof Error ? erro.message : erro);
    return resposta({ error: "O depósito não respondeu. Tentamos de novo." }, 502);
  }
}
