import { randomBytes, randomUUID } from "node:crypto";

import type { ContextoDoAssistente } from "./contexto";
import { ErroDaFerramenta } from "./ferramentas/tipos";

/*
 * Entregar um arquivo pelo assistente (PDF da nota, planilha, relatório,
 * catálogo): grava no depósito privado `assistentes-arquivos` (migration 186)
 * e devolve um link assinado que vence. O assistente mostra o link à pessoa;
 * ninguém lista o depósito, e o caminho leva a empresa e um id sorteado.
 */

const BALDE = "assistentes-arquivos";
/* 24 horas: dá para abrir no celular mais tarde, e não fica valendo para sempre. */
const VALIDADE_S = 24 * 60 * 60;

export type ArquivoEntregue = { arquivo: string; link: string; vale_ate: string; tamanho_kb: number };

function nomeLimpo(nome: string) {
  const limpo = nome
    .normalize("NFD")
    .replace(/[̀-ͯ]/g, "")
    .replace(/[^a-zA-Z0-9._-]+/g, "-")
    .replace(/-+/g, "-")
    .replace(/^-|-$/g, "")
    .slice(0, 80);
  return limpo || "arquivo";
}

const LETRAS = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789";

/* 12 letras sorteadas (sem 0/O, 1/l/I, que se confundem): ninguém adivinha, e cabe numa mensagem. */
function codigoCurto() {
  const bytes = randomBytes(12);
  let codigo = "";
  for (const byte of bytes) codigo += LETRAS[byte % LETRAS.length];
  return codigo;
}

export async function entregarArquivo(
  ctx: ContextoDoAssistente,
  arquivo: { conteudo: Buffer | Uint8Array | string; nome: string; tipo: string }
): Promise<ArquivoEntregue> {
  const agora = new Date();
  const nome = nomeLimpo(arquivo.nome);
  const caminho = `${ctx.chave.companyId}/${agora.getUTCFullYear()}/${String(agora.getUTCMonth() + 1).padStart(2, "0")}/${randomUUID()}/${nome}`;
  const corpo = typeof arquivo.conteudo === "string" ? Buffer.from(arquivo.conteudo, "utf8") : Buffer.from(arquivo.conteudo);

  const { error } = await ctx.admin.storage.from(BALDE).upload(caminho, corpo, { contentType: arquivo.tipo, upsert: false });
  if (error) {
    console.error("[assistentes] guardar arquivo", error.message);
    throw new ErroDaFerramenta("Não foi possível preparar o arquivo agora. Tente de novo em instantes.");
  }
  /* O link vai curto, no endereço do painel (migration 191). O assinado do
     depósito tem centenas de letras e o assistente estragava o código do meio
     ao repassar; a rota /baixar gera o assinado só na hora do clique. */
  const valeAte = new Date(agora.getTime() + VALIDADE_S * 1000);
  const codigo = codigoCurto();
  const { error: erroDoLink } = await ctx.admin
    .from("assistentes_links")
    .insert({ codigo, company_id: ctx.chave.companyId, caminho, nome, vence_em: valeAte.toISOString() });
  if (erroDoLink) {
    console.error("[assistentes] link do arquivo", erroDoLink.message);
    throw new ErroDaFerramenta("O arquivo foi gerado, mas o link não saiu. Tente de novo em instantes.");
  }
  /* A faxina não atrasa a entrega: roda solta e nunca derruba nada. */
  void faxinarArquivosVelhos(ctx, agora).catch((erro) => console.error("[assistentes] faxina dos arquivos", erro instanceof Error ? erro.message : erro));
  return {
    arquivo: nome,
    link: `${ctx.origemDoAdmin.replace(/\/$/, "")}/baixar/${codigo}`,
    vale_ate: valeAte.toISOString(),
    tamanho_kb: Math.max(1, Math.round(corpo.length / 1024)),
  };
}

/* ------------------------------------------------------------- faxina */

/* A migration 186 promete: o que tem mais de 7 dias sai do depósito. O link
   vence em 24 horas, então ninguém perde nada. A faxina vem de carona na
   entrega (não há fila para isso), no máximo uma vez por hora por empresa, e
   olha só as pastas dos últimos meses dessa empresa. */
const GUARDA_POR_MS = 7 * 24 * 60 * 60 * 1000;
const FAXINA_A_CADA_MS = 60 * 60 * 1000;
const PASTAS_POR_MES = 200;

declare global {
  var __faxinaDosArquivos: Map<string, number> | undefined;
}

export async function faxinarArquivosVelhos(ctx: ContextoDoAssistente, agora: Date) {
  globalThis.__faxinaDosArquivos ??= new Map();
  const ultimas = globalThis.__faxinaDosArquivos;
  const empresa = ctx.chave.companyId;
  if ((ultimas.get(empresa) ?? 0) > agora.getTime() - FAXINA_A_CADA_MS) return;
  ultimas.set(empresa, agora.getTime());

  const { error: erroDosLinks } = await ctx.admin.from("assistentes_links").delete().eq("company_id", empresa).lt("vence_em", agora.toISOString());
  if (erroDosLinks) console.error("[assistentes] faxina: links vencidos", erroDosLinks.message);

  const deposito = ctx.admin.storage.from(BALDE);

  /* Imagens enviadas pela página /enviar (migration 192): valem 24 horas e
     saem um dia depois de vencer, a linha e o arquivo em `entrada/`. */
  try {
    const { data: envios, error: erroDosEnvios } = await ctx.admin
      .from("assistentes_envios")
      .select("codigo,caminho")
      .eq("company_id", empresa)
      .lt("vence_em", new Date(agora.getTime() - 24 * 60 * 60 * 1000).toISOString())
      .limit(500);
    if (erroDosEnvios) console.error("[assistentes] faxina: envios vencidos", erroDosEnvios.message);
    const vencidos = (envios ?? []) as Array<{ codigo: string; caminho: string | null }>;
    /* Só o que é desta empresa: o caminho começa pela pasta dela. */
    const arquivosDosEnvios = vencidos.map((envio) => envio.caminho).filter((caminho): caminho is string => Boolean(caminho?.startsWith(`${empresa}/entrada/`)));
    for (let i = 0; i < arquivosDosEnvios.length; i += 100) {
      const { error: erroAoApagar } = await deposito.remove(arquivosDosEnvios.slice(i, i + 100));
      if (erroAoApagar) console.error("[assistentes] faxina: apagar envios", erroAoApagar.message);
    }
    if (vencidos.length) {
      const { error: erroDasLinhas } = await ctx.admin
        .from("assistentes_envios")
        .delete()
        .eq("company_id", empresa)
        .in("codigo", vencidos.map((envio) => envio.codigo));
      if (erroDasLinhas) console.error("[assistentes] faxina: linhas dos envios", erroDasLinhas.message);
    }
  } catch (erro) {
    console.error("[assistentes] faxina dos envios", erro instanceof Error ? erro.message : erro);
  }
  const limite = agora.getTime() - GUARDA_POR_MS;
  /* O mês atual e os dois anteriores: no começo do mês, o que tem 7 dias está
     no mês passado; e quem usa pouco pode ter ficado um mês sem faxina. */
  for (let volta = 0; volta < 3; volta++) {
    const mes = new Date(Date.UTC(agora.getUTCFullYear(), agora.getUTCMonth() - volta, 1));
    const prefixo = `${empresa}/${mes.getUTCFullYear()}/${String(mes.getUTCMonth() + 1).padStart(2, "0")}`;
    const { data: pastas, error } = await deposito.list(prefixo, { limit: PASTAS_POR_MES });
    if (error) {
      console.error("[assistentes] faxina: listar", prefixo, error.message);
      continue;
    }
    const velhos: string[] = [];
    /* Cada arquivo mora numa pasta com um id sorteado: ${empresa}/AAAA/MM/<id>/<nome>. */
    for (const pasta of pastas ?? []) {
      if (pasta.id) continue;
      const { data: arquivos, error: erroDaPasta } = await deposito.list(`${prefixo}/${pasta.name}`, { limit: 20 });
      if (erroDaPasta) continue;
      for (const arquivo of arquivos ?? []) {
        const criado = Date.parse(arquivo.created_at ?? "");
        if (arquivo.id && Number.isFinite(criado) && criado < limite) velhos.push(`${prefixo}/${pasta.name}/${arquivo.name}`);
      }
    }
    for (let i = 0; i < velhos.length; i += 100) {
      const { error: erroAoApagar } = await deposito.remove(velhos.slice(i, i + 100));
      if (erroAoApagar) console.error("[assistentes] faxina: apagar", erroAoApagar.message);
    }
  }
}

/** O texto que acompanha todo arquivo entregue (o assistente repete à pessoa). */
export const COMO_ENTREGAR_O_ARQUIVO =
  "Mostre o link à pessoa exatamente como veio, letra por letra, sem encurtar nem reescrever. O link vale 24 horas; depois disso, peça o arquivo de novo. Nunca monte um PDF, planilha ou documento por conta própria no lugar deste arquivo: ele é o oficial da Ktalogu.";
