/*
  A fila do catálogo em PDF.

  Gerar um catálogo leva de segundos a alguns minutos (baixar fotos, abrir o
  Chrome, imprimir), e a Cloudflare corta resposta que demora mais de 100 s.
  Então o pedido só entra na fila e volta na hora com um número; a tela
  pergunta a cada segundo como está, e baixa o PDF quando fica pronto.

  Um catálogo por vez no servidor inteiro: o Chrome e as fotos pesam, e a VPS
  tem 2 núcleos. Uma empresa não entra na fila duas vezes. O PDF pronto fica
  numa pasta temporária por 30 minutos e depois some.

  A fila mora na memória do processo do admin (um processo só no pm2). Um
  deploy no meio apaga a fila: a tela avisa e a pessoa gera de novo.
*/

import { randomUUID } from "node:crypto";
import { promises as fs } from "node:fs";
import path from "node:path";
import { faxinaDasFotos } from "./fotos.ts";
import type { CatalogoGerado, EtapaDoCatalogo } from "./gerar.ts";
import { ErroDoCatalogo } from "./carregar.ts";
import { ChromeAusente } from "./imprimir.ts";
import { pastaDoCatalogo } from "./pasta.ts";

const VALIDADE_DO_PDF_MS = 30 * 60 * 1000;
const TEMPO_MAXIMO_MS = 8 * 60 * 1000;
const TAMANHO_DA_FILA = 12;

type Progresso = (etapa: EtapaDoCatalogo, feito: number, total: number) => void;

type Tarefa = {
  id: string;
  empresa: string;
  criadaEm: number;
  terminadaEm: number | null;
  etapa: EtapaDoCatalogo;
  feito: number;
  total: number;
  erro: string | null;
  arquivo: string | null;
  nome: string | null;
  paginas: number | null;
  tamanho: number | null;
  miniaturas: string[];
  executar: (aoProgredir: Progresso) => Promise<CatalogoGerado>;
};

export type SituacaoDoCatalogo = {
  id: string;
  etapa: EtapaDoCatalogo;
  feito: number;
  total: number;
  /* Quantos catálogos de outras empresas estão na frente. */
  naFrente: number;
  erro: string | null;
  pronto: { nome: string; paginas: number; tamanho: number; miniaturas: string[] } | null;
};

type Estado = { tarefas: Map<string, Tarefa>; fila: string[]; rodando: boolean };

/* No globalThis para sobreviver ao recarregamento do código em desenvolvimento. */
const memoria = globalThis as typeof globalThis & { __ktaloguCatalogoPdf?: Estado };
const estado: Estado = (memoria.__ktaloguCatalogoPdf ??= { tarefas: new Map(), fila: [], rodando: false });

export class FilaCheia extends Error {}
export class JaGerando extends Error {
  id: string;
  constructor(id: string) {
    super("Já tem um catálogo desta empresa sendo gerado.");
    this.id = id;
  }
}

function ativa(tarefa: Tarefa) {
  return tarefa.etapa !== "pronto" && tarefa.etapa !== "erro";
}

async function pastaDosPdfs() {
  const pasta = path.join(await pastaDoCatalogo(), "pdfs");
  await fs.mkdir(pasta, { recursive: true });
  return pasta;
}

/* PDF velho sai da pasta e da memória. */
async function faxina() {
  const agora = Date.now();
  for (const tarefa of [...estado.tarefas.values()]) {
    if (ativa(tarefa) || agora - (tarefa.terminadaEm ?? tarefa.criadaEm) < VALIDADE_DO_PDF_MS) continue;
    estado.tarefas.delete(tarefa.id);
    if (tarefa.arquivo) await fs.rm(tarefa.arquivo, { force: true }).catch(() => undefined);
  }
  /* Sobra de um processo anterior (deploy no meio): a memória esqueceu, a pasta não. */
  try {
    const pasta = await pastaDosPdfs();
    for (const nome of await fs.readdir(pasta)) {
      const caminho = path.join(pasta, nome);
      const info = await fs.stat(caminho).catch(() => null);
      if (info && agora - info.mtimeMs > VALIDADE_DO_PDF_MS) await fs.rm(caminho, { force: true }).catch(() => undefined);
    }
  } catch {
    /* sem pasta ainda */
  }
}

function mensagemDoErro(erro: unknown) {
  if (erro instanceof ErroDoCatalogo) return erro.message;
  if (erro instanceof ChromeAusente) return "O catálogo em PDF está indisponível agora. Tente mais tarde; se continuar, fale com o suporte.";
  return "Não foi possível gerar o catálogo agora. Tente de novo em instantes.";
}

async function rodar(tarefa: Tarefa) {
  const aoProgredir: Progresso = (etapa, feito, total) => {
    tarefa.etapa = etapa;
    tarefa.feito = feito;
    tarefa.total = total;
  };
  let relogio: ReturnType<typeof setTimeout> | undefined;
  try {
    const gerado = await Promise.race([
      tarefa.executar(aoProgredir),
      new Promise<never>((_, falhar) => {
        relogio = setTimeout(() => falhar(new ErroDoCatalogo("demorou", "O catálogo demorou demais para ficar pronto. Escolha menos categorias e tente de novo.")), TEMPO_MAXIMO_MS);
      }),
    ]);
    const arquivo = path.join(await pastaDosPdfs(), `${tarefa.id}.pdf`);
    await fs.writeFile(arquivo, gerado.pdf);
    tarefa.arquivo = arquivo;
    tarefa.nome = gerado.nomeDoArquivo;
    tarefa.paginas = gerado.paginas;
    tarefa.tamanho = gerado.pdf.length;
    tarefa.miniaturas = gerado.miniaturas;
    tarefa.etapa = "pronto";
  } catch (erro) {
    if (!(erro instanceof ErroDoCatalogo)) console.error("[catalogo-pdf] falhou", tarefa.id, erro);
    tarefa.erro = mensagemDoErro(erro);
    tarefa.etapa = "erro";
  } finally {
    if (relogio) clearTimeout(relogio);
    tarefa.terminadaEm = Date.now();
  }
}

async function andarComAFila() {
  if (estado.rodando) return;
  estado.rodando = true;
  try {
    for (;;) {
      const id = estado.fila.shift();
      if (!id) break;
      const tarefa = estado.tarefas.get(id);
      if (tarefa) await rodar(tarefa);
    }
  } finally {
    estado.rodando = false;
    await faxina();
    await faxinaDasFotos();
  }
}

/** Põe um catálogo na fila. Volta na hora; o trabalho segue sozinho. */
export async function pedirCatalogo(empresa: string, executar: Tarefa["executar"]) {
  await faxina();
  const daEmpresa = [...estado.tarefas.values()].find((tarefa) => tarefa.empresa === empresa && ativa(tarefa));
  if (daEmpresa) throw new JaGerando(daEmpresa.id);
  if (estado.fila.length >= TAMANHO_DA_FILA) throw new FilaCheia("Fila cheia.");
  const tarefa: Tarefa = {
    id: randomUUID(),
    empresa,
    criadaEm: Date.now(),
    terminadaEm: null,
    etapa: "fila",
    feito: 0,
    total: 0,
    erro: null,
    arquivo: null,
    nome: null,
    paginas: null,
    tamanho: null,
    miniaturas: [],
    executar,
  };
  estado.tarefas.set(tarefa.id, tarefa);
  estado.fila.push(tarefa.id);
  void andarComAFila();
  return tarefa.id;
}

/** Como está o catálogo. Só a empresa que pediu enxerga. */
export function situacaoDoCatalogo(id: string, empresa: string): SituacaoDoCatalogo | null {
  const tarefa = estado.tarefas.get(id);
  if (!tarefa || tarefa.empresa !== empresa) return null;
  const posicao = estado.fila.indexOf(id);
  return {
    id: tarefa.id,
    etapa: tarefa.etapa,
    feito: tarefa.feito,
    total: tarefa.total,
    naFrente: posicao < 0 ? 0 : posicao + (estado.rodando ? 1 : 0),
    erro: tarefa.erro,
    pronto:
      tarefa.etapa === "pronto" && tarefa.nome && tarefa.paginas !== null && tarefa.tamanho !== null
        ? { nome: tarefa.nome, paginas: tarefa.paginas, tamanho: tarefa.tamanho, miniaturas: tarefa.miniaturas }
        : null,
  };
}

/** O PDF pronto, para baixar. */
export async function pdfDoCatalogo(id: string, empresa: string) {
  const tarefa = estado.tarefas.get(id);
  if (!tarefa || tarefa.empresa !== empresa || tarefa.etapa !== "pronto" || !tarefa.arquivo || !tarefa.nome) return null;
  try {
    return { conteudo: await fs.readFile(tarefa.arquivo), nome: tarefa.nome };
  } catch {
    return null;
  }
}
