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

import { avisarLoja } from "@/lib/loja/avisar-loja";
import { conferirConteudo, conferirExtensao, tipoDaExtensao } from "@/lib/digitais/formatos";
import { limitesDaLoja, partesDoArquivo, TAMANHO_DA_PARTE } from "@/lib/digitais/limites";
import { extensaoDe, nomeLimpo, tamanhoLegivel } from "@/lib/digitais/nome-do-arquivo";
import {
  abrirEnvio,
  apagarObjeto,
  cancelarEnvio,
  configuracaoDoR2,
  enderecoDaParte,
  enderecoParaBaixar,
  fecharEnvio,
  fichaDoObjeto,
  lerTrecho,
  partesRecebidas,
} from "@/lib/digitais/r2";
import { VIDEO_DO_PRODUTO } from "@/lib/limites-do-video-do-produto";

import { lerArquivoDaLoja, metaConferida } from "../arquivos";
import { ErroDigital, type ContextoDigital } from "../contexto";
import { arquivoProntoNosCursos, tirarArquivoDosCursos, usosNosCursos } from "../cursos";
import { atualizarOQueRecebe } from "../produtos";

/*
 * O que as rotas de arquivos fazem (app/api/digitais/arquivos/**), fora
 * delas. O painel, o assistente (MCP) e a página de envio do assistente
 * (/enviar/<código>, sem login: o código manda) usam as mesmas funções, com
 * as mesmas conferências de formato, espaço e conteúdo real.
 */

/* Validade dos endereços das partes: dá para enviar 2 GB numa internet lenta. */
const VALIDADE_DAS_PARTES_S = 6 * 60 * 60;
/* Dias que um arquivo apagado espera antes de sair do depósito. */
const LIXEIRA_DIAS = 30;

type LinhaDoArquivo = {
  id: string;
  nome: string;
  extensao: string;
  categoria: string;
  tamanho_bytes: number;
  status: string;
  motivo: string | null;
  criado_em: string;
  meta: Record<string, unknown> | null;
  digital_deliverables: Array<{ id: string; apagado_em: string | null; product_id: string; products: { name: string } | null }> | null;
};

/* A biblioteca de arquivos e o espaço usado. */
export async function listarArquivos({ admin, lojaId, situacao, pode, liberado }: ContextoDigital) {
  const [arquivos, espaco, comVideo] = await Promise.all([
    admin
      .from("digital_files")
      .select("id,nome,extensao,categoria,tamanho_bytes,status,motivo,criado_em,meta,digital_deliverables(id,apagado_em,product_id,products(name))")
      .eq("user_id", lojaId)
      .in("status", ["enviando", "conferindo", "pronto", "recusado"])
      /* Sem Produtos digitais, a empresa só tem os vídeos dos produtos. */
      .in("categoria", liberado ? ["video", "pdf", "imagem", "compactado", "audio", "documento", "outro"] : ["video"])
      .order("criado_em", { ascending: false })
      .limit(500),
    admin.rpc("espaco_digital_da_loja", { p_loja: lojaId }),
    /* Os produtos com vídeo enviado na galeria (o máximo é lib/limites-do-video-do-produto.ts). */
    admin.from("products").select("id").eq("user_id", lojaId).not("video_file_id", "is", null).limit(200),
  ]);
  if (arquivos.error) throw new ErroDigital("Não foi possível ler os arquivos.", 500);
  const limites = limitesDaLoja(situacao);

  /* O que está nos cursos (vídeo de aula ou material) também é "usado". */
  const ids = ((arquivos.data ?? []) as Array<{ id: string }>).map((a) => a.id);
  const nosCursos = new Map<string, Map<string, string>>();
  if (ids.length) {
    const [aulas, materiais, produtosComVideo] = await Promise.all([
      admin
        .from("digital_course_lessons")
        .select("video_file_id,product_id,products(name)")
        .eq("user_id", lojaId)
        .in("video_file_id", ids)
        .is("apagado_em", null),
      admin
        .from("digital_course_materials")
        .select("file_id,digital_course_lessons!inner(product_id,apagado_em,products(name))")
        .eq("user_id", lojaId)
        .in("file_id", ids)
        .is("digital_course_lessons.apagado_em", null),
      /* O vídeo da galeria de um produto da loja (migration 183). */
      admin.from("products").select("id,name,video_file_id").eq("user_id", lojaId).in("video_file_id", ids),
    ]);
    const anotar = (arquivoId: string | null, produtoId: string | undefined, nome: string | undefined) => {
      if (!arquivoId || !produtoId) return;
      const lista = nosCursos.get(arquivoId) ?? new Map<string, string>();
      lista.set(produtoId, nome ?? "Conteúdo em vídeo");
      nosCursos.set(arquivoId, lista);
    };
    for (const linha of (aulas.data ?? []) as unknown as Array<{ video_file_id: string; product_id: string; products: { name: string } | null }>) {
      anotar(linha.video_file_id, linha.product_id, linha.products?.name);
    }
    for (const linha of (materiais.data ?? []) as unknown as Array<{
      file_id: string;
      digital_course_lessons: { product_id: string; products: { name: string } | null } | null;
    }>) {
      anotar(linha.file_id, linha.digital_course_lessons?.product_id, linha.digital_course_lessons?.products?.name);
    }
    for (const linha of (produtosComVideo.data ?? []) as Array<{ id: string; name: string | null; video_file_id: string }>) {
      anotar(linha.video_file_id, linha.id, linha.name ?? "Produto");
    }
  }
  const bruto = (espaco.data ?? {}) as { usado?: number; arquivos?: number; por_categoria?: Record<string, { bytes: number; arquivos: number }> };
  return {
    podeEnviar: pode("digital.files.write") || pode("catalog.products.write") || pode("products.catalog.write"),
    produtosComVideo: ((comVideo.data ?? []) as Array<{ id: string }>).map((p) => p.id),
    deposito: configuracaoDoR2() !== null,
    espaco: {
      usado: Number(bruto.usado ?? 0),
      arquivos: Number(bruto.arquivos ?? 0),
      porCategoria: bruto.por_categoria ?? {},
      ...limites,
    },
    arquivos: ((arquivos.data ?? []) as unknown as LinhaDoArquivo[]).map((linha) => {
      const usos = (linha.digital_deliverables ?? []).filter((d) => !d.apagado_em);
      const cursos = nosCursos.get(linha.id) ?? new Map<string, string>();
      return {
        id: linha.id,
        nome: linha.nome,
        extensao: linha.extensao,
        categoria: linha.categoria,
        tamanho: Number(linha.tamanho_bytes) || 0,
        tamanhoLegivel: tamanhoLegivel(Number(linha.tamanho_bytes) || 0),
        status: linha.status,
        motivo: linha.motivo,
        criadoEm: linha.criado_em,
        duracao: typeof linha.meta?.duracao === "number" ? linha.meta.duracao : null,
        paginas: typeof linha.meta?.paginas === "number" ? linha.meta.paginas : null,
        usadoEm: Array.from(
          new Set([...usos.map((d) => d.products?.name), ...cursos.values()].filter((nome): nome is string => Boolean(nome)))
        ),
        produtos: Array.from(new Set([...usos.map((d) => d.product_id), ...cursos.keys()])),
      };
    }),
  };
}

export type QuemEnvia = {
  admin: SupabaseClient;
  lojaId: string;
  /* A assinatura da empresa ("teste" tem espaço menor). */
  situacao: string;
  /* Produtos digitais liberados (sem eles, só o vídeo do produto). */
  liberado: boolean;
  criadoPor: string;
};

/* Abre o envio de um arquivo: confere formato e espaço, reserva o tamanho e
   devolve um endereço assinado para cada parte. O arquivo vai direto do
   navegador para o depósito. `soVideo`: a página de envio do assistente
   pediu um vídeo (o resto é recusado antes de reservar espaço). */
export async function abrirEnvioDeArquivo(quem: QuemEnvia, bruto: unknown, opcoes: { soVideo?: boolean } = {}) {
  const { admin, lojaId, situacao, liberado } = quem;
  const cfg = configuracaoDoR2();
  if (!cfg) throw new ErroDigital("O envio de arquivos ainda está sendo ligado. Tente mais tarde.", 503);

  const corpo = bruto as { nome?: unknown; tamanho?: unknown } | null;
  const nome = nomeLimpo(typeof corpo?.nome === "string" ? corpo.nome : "");
  const tamanho = Math.floor(Number(corpo?.tamanho));
  if (!Number.isFinite(tamanho) || tamanho <= 0) throw new ErroDigital("O arquivo está vazio.", 400);

  const extensao = extensaoDe(nome);
  const conferencia = conferirExtensao(extensao);
  if (!conferencia.ok) throw new ErroDigital(conferencia.motivo, 400);
  if (opcoes.soVideo && conferencia.categoria !== "video") throw new ErroDigital("Aqui só entra vídeo (MP4, MOV, M4V ou WEBM).", 400);
  /* Sem Produtos digitais, só entra o vídeo do produto (lib/limites-do-video-do-produto.ts). */
  if (!liberado) {
    if (conferencia.categoria !== "video") throw new ErroDigital("Aqui só entra o vídeo do produto (MP4, MOV ou WEBM).", 400);
    if (tamanho > VIDEO_DO_PRODUTO.bytes) throw new ErroDigital("O vídeo do produto pode ter até 50 MB.", 400);
  }

  const limites = limitesDaLoja(situacao);
  const { data, error } = await admin.rpc("abrir_arquivo_digital", {
    p_loja: lojaId,
    p_nome: nome,
    p_extensao: extensao,
    p_mime: tipoDaExtensao(extensao),
    p_categoria: conferencia.categoria,
    p_tamanho: tamanho,
    p_limite_total: limites.total,
    p_limite_arquivo: limites.porArquivo,
    p_criado_por: quem.criadoPor,
  });
  if (error) throw new ErroDigital("Não foi possível começar o envio.", 500);
  const reserva = data as { ok: boolean; motivo?: string; id?: string; storage_key?: string; usado?: number; limite?: number };
  if (!reserva.ok || !reserva.id || !reserva.storage_key) {
    if (reserva.motivo === "arquivo_grande") {
      throw new ErroDigital(`Este arquivo passa de ${tamanhoLegivel(limites.porArquivo)}. Divida em partes menores (vários vídeos curtos, por exemplo).`, 400);
    }
    if (reserva.motivo === "sem_espaco") {
      const sobra = Math.max(0, limites.total - Number(reserva.usado ?? 0));
      throw new ErroDigital(`Não cabe: sobram ${tamanhoLegivel(sobra)} dos ${tamanhoLegivel(limites.total)} do seu espaço. Apague arquivos sem uso para liberar.`, 400);
    }
    throw new ErroDigital("Não foi possível começar o envio.", 400);
  }

  try {
    const idDoEnvio = await abrirEnvio(cfg, reserva.storage_key, tipoDaExtensao(extensao));
    const { error: erroAoGuardar } = await admin
      .from("digital_files")
      .update({ envio_id: idDoEnvio, tamanho_da_parte: TAMANHO_DA_PARTE, atualizado_em: new Date().toISOString() })
      .eq("id", reserva.id)
      .eq("user_id", lojaId);
    if (erroAoGuardar) throw erroAoGuardar;
    const total = partesDoArquivo(tamanho);
    return {
      ok: true,
      id: reserva.id,
      tamanhoDaParte: TAMANHO_DA_PARTE,
      partes: Array.from({ length: total }, (_, i) => ({
        numero: i + 1,
        url: enderecoDaParte(cfg, reserva.storage_key!, idDoEnvio, i + 1, VALIDADE_DAS_PARTES_S),
      })),
    };
  } catch (erro) {
    /* Não abriu no depósito: devolve o espaço reservado. */
    await admin.from("digital_files").update({ status: "recusado", motivo: "O depósito não respondeu." }).eq("id", reserva.id).eq("user_id", lojaId);
    console.error("[digitais] abrir envio", erro);
    throw new ErroDigital("O depósito de arquivos não respondeu. Tente de novo em instantes.", 502);
  }
}

/* Endereço para assistir o vídeo no painel (a prévia no cadastro do produto). Vale uma hora. */
export async function enderecoParaAssistirNoPainel({ admin, lojaId }: ContextoDigital, id: string) {
  const arquivo = await lerArquivoDaLoja(admin, lojaId, id);
  const cfg = configuracaoDoR2();
  if (!cfg || arquivo.status !== "pronto") return { url: null, status: arquivo.status, nome: arquivo.nome };
  return {
    url: enderecoParaBaixar(cfg, arquivo.storage_key, { nome: arquivo.nome, abrirNoNavegador: true, segundos: 60 * 60 }),
    status: arquivo.status,
    nome: arquivo.nome,
  };
}

/*
 * Cancela um envio ou apaga um arquivo. Arquivo que está num produto, ou que
 * alguém comprou, não sai: primeiro tira do produto (quem comprou continua
 * baixando enquanto tiver acesso).
 */
export async function apagarArquivoDaBiblioteca(admin: SupabaseClient, lojaId: string, id: string) {
  const arquivo = await lerArquivoDaLoja(admin, lojaId, id);
  const agora = new Date();

  if (arquivo.status === "enviando" || arquivo.status === "conferindo") {
    const cfg = configuracaoDoR2();
    if (cfg && arquivo.envio_id) await cancelarEnvio(cfg, arquivo.storage_key, arquivo.envio_id).catch(() => undefined);
    await admin
      .from("digital_files")
      .update({ status: "apagado", envio_id: null, apagado_em: agora.toISOString(), atualizado_em: agora.toISOString() })
      .eq("id", id)
      .eq("user_id", lojaId);
    /* O envio cancelado sai também dos produtos em que já tinha entrado
       (ninguém recebeu um arquivo que nunca terminou de chegar). */
    const { data: ligados } = await admin.from("digital_deliverables").select("id,product_id").eq("file_id", id).eq("user_id", lojaId);
    const lista = (ligados ?? []) as Array<{ id: string; product_id: string }>;
    for (const item of lista) {
      const { error } = await admin.from("digital_deliverables").delete().eq("id", item.id).eq("user_id", lojaId);
      if (error) {
        await admin
          .from("digital_deliverables")
          .update({ ativo: false, apagado_em: agora.toISOString(), atualizado_em: agora.toISOString() })
          .eq("id", item.id)
          .eq("user_id", lojaId);
      }
    }
    for (const produtoId of new Set(lista.map((item) => item.product_id))) {
      await atualizarOQueRecebe(admin, lojaId, produtoId).catch(() => undefined);
    }
    await tirarArquivoDosCursos(admin, lojaId, id).catch(() => undefined);
    return { ok: true, cancelado: true };
  }

  /* Num curso (vídeo de aula, material ou apresentação), não sai. */
  const nosCursos = await usosNosCursos(admin, lojaId, id);
  if (nosCursos.nomes.length) {
    throw new ErroDigital(`Este arquivo está no conteúdo em vídeo ${nosCursos.nomes.join(", ")}. Tire de lá antes de apagar.`, 409);
  }

  /* O vídeo da galeria de um produto da loja também segura o arquivo. */
  const { data: naGaleria } = await admin.from("products").select("name").eq("user_id", lojaId).eq("video_file_id", id).limit(5);
  const produtosComVideo = ((naGaleria ?? []) as Array<{ name: string | null }>).map((p) => p.name).filter(Boolean);
  if (produtosComVideo.length) {
    throw new ErroDigital(`Este vídeo está na galeria de ${produtosComVideo.join(", ")}. Tire de lá antes de apagar.`, 409);
  }

  const { data: usos } = await admin
    .from("digital_deliverables")
    .select("id,apagado_em,products(name)")
    .eq("file_id", id)
    .eq("user_id", lojaId);
  const lista = (usos ?? []) as unknown as Array<{ id: string; apagado_em: string | null; products: { name: string } | null }>;
  const noProduto = lista.filter((u) => !u.apagado_em);
  if (noProduto.length) {
    const nomes = Array.from(new Set(noProduto.map((u) => u.products?.name).filter(Boolean))).join(", ");
    throw new ErroDigital(`Este arquivo está em ${nomes || "um produto"}. Tire de lá antes de apagar.`, 409);
  }
  if (lista.length) {
    const { count } = await admin
      .from("digital_entitlements")
      .select("id", { count: "exact", head: true })
      .in("deliverable_id", lista.map((u) => u.id))
      .in("status", ["ativo", "agendado", "aguardando_arquivo", "pausado"]);
    if ((count ?? 0) > 0) throw new ErroDigital("Quem comprou ainda baixa este arquivo. Ele fica guardado enquanto o acesso valer.", 409);
  }

  const remover = new Date(agora.getTime() + LIXEIRA_DIAS * 24 * 60 * 60 * 1000).toISOString();
  const { error } = await admin
    .from("digital_files")
    .update({ status: "apagado", apagado_em: agora.toISOString(), remover_do_deposito_em: remover, atualizado_em: agora.toISOString() })
    .eq("id", id)
    .eq("user_id", lojaId);
  if (error) throw new ErroDigital("Não foi possível apagar o arquivo.", 500);
  await admin.from("digital_tasks").insert({ user_id: lojaId, tipo: "apagar_objeto", ref: id, proxima_em: remover });
  return { ok: true };
}

/* Retomar um envio que caiu: o que o depósito já recebeu e endereços novos
   para o que falta. */
export async function retomarEnvioDeArquivo(admin: SupabaseClient, lojaId: string, id: string) {
  const cfg = configuracaoDoR2();
  if (!cfg) throw new ErroDigital("O envio de arquivos ainda está sendo ligado. Tente mais tarde.", 503);
  const arquivo = await lerArquivoDaLoja(admin, lojaId, id);
  if (arquivo.status !== "enviando" || !arquivo.envio_id) throw new ErroDigital("Este envio já terminou ou foi cancelado.", 409);

  const recebidas = await partesRecebidas(cfg, arquivo.storage_key, arquivo.envio_id);
  const tamanhoDaParte = arquivo.tamanho_da_parte || TAMANHO_DA_PARTE;
  const total = partesDoArquivo(Number(arquivo.tamanho_bytes), tamanhoDaParte);
  const jaTem = new Set(recebidas.map((p) => p.numero));
  return {
    ok: true,
    tamanhoDaParte,
    recebidas: recebidas.map((p) => ({ numero: p.numero, etag: p.etag })),
    partes: Array.from({ length: total }, (_, i) => i + 1)
      .filter((numero) => !jaTem.has(numero))
      .map((numero) => ({ numero, url: enderecoDaParte(cfg, arquivo.storage_key, arquivo.envio_id!, numero, 6 * 60 * 60) })),
  };
}

/*
 * Fecha o envio: quem diz quais partes chegaram é o depósito (não o
 * navegador). Depois confere o tamanho e o formato real pelos primeiros
 * bytes. Arquivo que não passa sai do depósito na hora e devolve o espaço.
 */
export async function concluirEnvioDeArquivo(admin: SupabaseClient, lojaId: string, id: string, bruto: unknown, request: Request) {
  const cfg = configuracaoDoR2();
  if (!cfg) throw new ErroDigital("O envio de arquivos ainda está sendo ligado. Tente mais tarde.", 503);
  const arquivo = await lerArquivoDaLoja(admin, lojaId, id);
  if (arquivo.status === "pronto") return { ok: true, status: "pronto" };
  if (arquivo.status !== "enviando" || !arquivo.envio_id) throw new ErroDigital("Este envio foi cancelado. Envie o arquivo de novo.", 409);

  const corpo = bruto as { meta?: unknown } | null;
  const tamanho = Number(arquivo.tamanho_bytes);
  const tamanhoDaParte = arquivo.tamanho_da_parte || TAMANHO_DA_PARTE;
  const esperadas = partesDoArquivo(tamanho, tamanhoDaParte);
  const recebidas = (await partesRecebidas(cfg, arquivo.storage_key, arquivo.envio_id)).sort((a, b) => a.numero - b.numero);
  const somadas = recebidas.reduce((soma, p) => soma + p.tamanho, 0);
  if (recebidas.length !== esperadas || somadas !== tamanho) {
    throw new ErroDigital("Ainda faltam partes do arquivo. O envio continua de onde parou.", 409, {
      recebidas: recebidas.length,
      esperadas,
    });
  }

  await admin.from("digital_files").update({ status: "conferindo", atualizado_em: new Date().toISOString() }).eq("id", id).eq("user_id", lojaId);

  const recusar = async (motivo: string) => {
    await apagarObjeto(cfg, arquivo.storage_key).catch(() => undefined);
    const agora = new Date().toISOString();
    await admin
      .from("digital_files")
      .update({ status: "recusado", motivo, envio_id: null, atualizado_em: agora })
      .eq("id", id)
      .eq("user_id", lojaId);
    /* O que não passou na conferência sai dos produtos em que tinha entrado
       enquanto subia: ninguém pode receber um arquivo recusado. */
    const { data: ligados } = await admin.from("digital_deliverables").select("id,product_id").eq("file_id", id).eq("user_id", lojaId);
    const lista = (ligados ?? []) as Array<{ id: string; product_id: string }>;
    for (const item of lista) {
      const { error } = await admin.from("digital_deliverables").delete().eq("id", item.id).eq("user_id", lojaId);
      if (error) {
        await admin.from("digital_deliverables").update({ ativo: false, apagado_em: agora, atualizado_em: agora }).eq("id", item.id).eq("user_id", lojaId);
      }
    }
    for (const produtoId of new Set(lista.map((item) => item.product_id))) {
      await atualizarOQueRecebe(admin, lojaId, produtoId).catch(() => undefined);
    }
    /* E sai das aulas dos cursos (a aula fica, sem o vídeo). */
    await tirarArquivoDosCursos(admin, lojaId, id).catch(() => undefined);
    throw new ErroDigital(motivo, 422, { status: "recusado" });
  };

  try {
    await fecharEnvio(cfg, arquivo.storage_key, arquivo.envio_id, recebidas.map((p) => ({ numero: p.numero, etag: p.etag })));
  } catch (erro) {
    console.error("[digitais] fechar envio", erro);
    await admin.from("digital_files").update({ status: "enviando" }).eq("id", id).eq("user_id", lojaId);
    throw new ErroDigital("O depósito não juntou as partes. Tente concluir de novo.", 502);
  }

  const ficha = await fichaDoObjeto(cfg, arquivo.storage_key);
  if (!ficha || ficha.tamanho !== tamanho) return recusar("O arquivo chegou incompleto. Envie de novo.");

  const comeco = await lerTrecho(cfg, arquivo.storage_key, 0, Math.min(tamanho - 1, 63));
  const conteudo = conferirConteudo(arquivo.extensao, comeco);
  if (!conteudo.ok) return recusar(conteudo.motivo);

  const meta = { ...(arquivo.meta ?? {}), ...metaConferida(corpo?.meta) };
  const { error } = await admin
    .from("digital_files")
    .update({ status: "pronto", motivo: null, envio_id: null, meta, atualizado_em: new Date().toISOString() })
    .eq("id", id)
    .eq("user_id", lojaId);
  if (error) throw new ErroDigital("O arquivo chegou, mas não conseguimos marcar como pronto. Tente de novo.", 500);

  /* Quem comprou enquanto o arquivo subia passa a ter o acesso. */
  const { data: usos } = await admin
    .from("digital_deliverables")
    .select("id,product_id")
    .eq("file_id", id)
    .eq("user_id", lojaId);
  const entregaveis = ((usos ?? []) as Array<{ id: string; product_id: string }>);
  if (entregaveis.length) {
    await admin
      .from("digital_entitlements")
      .update({ status: "ativo", atualizado_em: new Date().toISOString() })
      .in("deliverable_id", entregaveis.map((e) => e.id))
      .eq("status", "aguardando_arquivo");
    for (const produtoId of new Set(entregaveis.map((e) => e.product_id))) {
      await atualizarOQueRecebe(admin, lojaId, produtoId).catch(() => undefined);
    }
    avisarLoja(lojaId, request);
  }
  /* A aula que esperava este vídeo aparece na página do curso. */
  if ((await arquivoProntoNosCursos(admin, lojaId, id).catch(() => 0)) > 0) avisarLoja(lojaId, request);
  return { ok: true, status: "pronto" };
}
