/**
 * Um ZIP escrito à mão, sem dependência nova.
 *
 * O contador pede "me manda os XMLs do mês" e ninguém quer baixar nota por
 * nota. O arquivo sai no formato clássico (deflate + diretório central), que
 * o Windows, o macOS e qualquer sistema contábil abrem sem plugin.
 *
 * Só o necessário: nome em UTF-8 (bit 11 do sinalizador), sem senha, sem
 * ZIP64 — pacote de notas de um mês não chega perto dos 4 GB.
 */

import { deflateRawSync } from "node:zlib";

const TABELA_CRC = (() => {
  const tabela = new Int32Array(256);
  for (let i = 0; i < 256; i += 1) {
    let c = i;
    for (let bit = 0; bit < 8; bit += 1) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
    tabela[i] = c;
  }
  return tabela;
})();

export function crc32(dados: Uint8Array): number {
  let c = 0xffffffff;
  for (let i = 0; i < dados.length; i += 1) c = TABELA_CRC[(c ^ dados[i]) & 0xff] ^ (c >>> 8);
  return (c ^ 0xffffffff) >>> 0;
}

/** Data/hora no formato MS-DOS, que é o que o ZIP guarda. */
function dataDoDos(quando: Date) {
  const ano = Math.max(1980, quando.getFullYear());
  const data = ((ano - 1980) << 9) | ((quando.getMonth() + 1) << 5) | quando.getDate();
  const hora = (quando.getHours() << 11) | (quando.getMinutes() << 5) | Math.floor(quando.getSeconds() / 2);
  return { data, hora };
}

export type ArquivoDoZip = { nome: string; conteudo: Uint8Array; quando?: Date };

/** Nome seguro dentro do pacote: sem pasta de fora, sem caractere proibido. */
export function nomeParaZip(bruto: string) {
  const limpo = String(bruto ?? "")
    .normalize("NFKD")
    .replace(/[̀-ͯ]/g, "")
    .replace(/[\\:*?"<>| -]/g, "-")
    .replace(/\.\.+/g, ".")
    .replace(/^[./]+/, "")
    .trim();
  return limpo || "arquivo";
}

export function montarZip(arquivos: ArquivoDoZip[]): Uint8Array {
  const locais: Buffer[] = [];
  const central: Buffer[] = [];
  let deslocamento = 0;
  const usados = new Set<string>();

  for (const arquivo of arquivos) {
    let nome = nomeParaZip(arquivo.nome);
    if (usados.has(nome)) {
      /* Dois arquivos com o mesmo nome deixam o pacote ambíguo. */
      const ponto = nome.lastIndexOf(".");
      const base = ponto > 0 ? nome.slice(0, ponto) : nome;
      const extensao = ponto > 0 ? nome.slice(ponto) : "";
      let n = 2;
      while (usados.has(`${base}-${n}${extensao}`)) n += 1;
      nome = `${base}-${n}${extensao}`;
    }
    usados.add(nome);

    const nomeBytes = Buffer.from(nome, "utf8");
    const conteudo = Buffer.from(arquivo.conteudo);
    const comprimido = deflateRawSync(conteudo, { level: 6 });
    const crc = crc32(conteudo);
    const { data, hora } = dataDoDos(arquivo.quando ?? new Date());

    const local = Buffer.alloc(30);
    local.writeUInt32LE(0x04034b50, 0);
    local.writeUInt16LE(20, 4);
    local.writeUInt16LE(0x0800, 6);
    local.writeUInt16LE(8, 8);
    local.writeUInt16LE(hora, 10);
    local.writeUInt16LE(data, 12);
    local.writeUInt32LE(crc, 14);
    local.writeUInt32LE(comprimido.length, 18);
    local.writeUInt32LE(conteudo.length, 22);
    local.writeUInt16LE(nomeBytes.length, 26);
    local.writeUInt16LE(0, 28);
    locais.push(local, nomeBytes, comprimido);

    const entrada = Buffer.alloc(46);
    entrada.writeUInt32LE(0x02014b50, 0);
    entrada.writeUInt16LE(20, 4);
    entrada.writeUInt16LE(20, 6);
    entrada.writeUInt16LE(0x0800, 8);
    entrada.writeUInt16LE(8, 10);
    entrada.writeUInt16LE(hora, 12);
    entrada.writeUInt16LE(data, 14);
    entrada.writeUInt32LE(crc, 16);
    entrada.writeUInt32LE(comprimido.length, 20);
    entrada.writeUInt32LE(conteudo.length, 24);
    entrada.writeUInt16LE(nomeBytes.length, 28);
    entrada.writeUInt16LE(0, 30);
    entrada.writeUInt16LE(0, 32);
    entrada.writeUInt16LE(0, 34);
    entrada.writeUInt16LE(0, 36);
    entrada.writeUInt32LE(0, 38);
    entrada.writeUInt32LE(deslocamento, 42);
    central.push(entrada, nomeBytes);

    deslocamento += local.length + nomeBytes.length + comprimido.length;
  }

  const corpo = Buffer.concat(locais);
  const diretorio = Buffer.concat(central);
  const fim = Buffer.alloc(22);
  fim.writeUInt32LE(0x06054b50, 0);
  fim.writeUInt16LE(0, 4);
  fim.writeUInt16LE(0, 6);
  fim.writeUInt16LE(arquivos.length, 8);
  fim.writeUInt16LE(arquivos.length, 10);
  fim.writeUInt32LE(diretorio.length, 12);
  fim.writeUInt32LE(corpo.length, 16);
  fim.writeUInt16LE(0, 20);

  return new Uint8Array(Buffer.concat([corpo, diretorio, fim]));
}
