#!/usr/bin/env node
/**
 * Nuvem Deploy - CLI
 * Sem dependencias externas. Requer Node 18+.
 */

import fs from "node:fs";
import path from "node:path";
import zlib from "node:zlib";

const DIR_CFG = ".nuvem";
const F_CFG = path.join(DIR_CFG, "config.json");
const F_CRED = path.join(DIR_CFG, "credentials.json");

const EXCLUIR = [
  /^\.git$/, /^node_modules$/, /^\.nuvem$/, /^\.env($|\.)/,
  /^\.DS_Store$/, /\.pem$/, /\.key$/, /^id_rsa/, /^\.ssh$/,
  /^vendor$/, /^\.vscode$/, /^\.idea$/,
];

const CANDIDATOS = ["dist", "build", "out", "public", "_site", "."];

// ------------------------------------------------------------------ helpers

const arg = (nome) => {
  const i = process.argv.indexOf(`--${nome}`);
  return i > -1 ? process.argv[i + 1] : null;
};

const morrer = (msg) => {
  console.error(`\n  ${msg}\n`);
  process.exit(1);
};

function lerToken(token) {
  if (!token?.startsWith("nvm_")) morrer("Token invalido: deve comecar com nvm_");
  let b64 = token.slice(4).replace(/-/g, "+").replace(/_/g, "/");
  while (b64.length % 4) b64 += "=";
  const bruto = Buffer.from(b64, "base64").toString("utf8");
  const sep = bruto.indexOf("|");
  if (sep < 0) morrer("Token malformado.");
  const endpoint = bruto.slice(0, sep);
  if (!/^https:\/\//.test(endpoint)) morrer("Token aponta para endpoint sem HTTPS.");
  return { endpoint };
}

async function chamar(acao, cfg, token, extras = null) {
  const fd = extras ?? new FormData();
  fd.append("action", acao);
  fd.append("token", token); // fallback para servidores que removem o Authorization

  let r;
  try {
    r = await fetch(`${cfg.endpoint}?action=${acao}`, {
      method: "POST",
      headers: { Authorization: `Bearer ${token}` },
      body: fd,
    });
  } catch (e) {
    morrer(`Nao foi possivel falar com o servidor: ${e.message}`);
  }

  const texto = await r.text();
  let json;
  try {
    json = JSON.parse(texto);
  } catch {
    morrer(`Resposta inesperada do servidor (HTTP ${r.status}):\n  ${texto.slice(0, 300)}`);
  }
  if (!json.ok) morrer(`Servidor: ${json.error}`);
  return json;
}

// ---------------------------------------------------------------- zip (puro)

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

function crc32(buf) {
  let c = -1;
  for (let i = 0; i < buf.length; i++) c = TABELA_CRC[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
  return (c ^ -1) >>> 0;
}

function horaDos(d) {
  const t = ((d.getHours() << 11) | (d.getMinutes() << 5) | (d.getSeconds() >> 1)) & 0xffff;
  const f = (((d.getFullYear() - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate()) & 0xffff;
  return { t, f };
}

function montarZip(entradas) {
  const locais = [];
  const centrais = [];
  let offset = 0;

  for (const e of entradas) {
    const nome = Buffer.from(e.nome.replace(/\\/g, "/"), "utf8");
    const crc = crc32(e.dados);
    const comp = zlib.deflateRawSync(e.dados, { level: 6 });
    const usarDeflate = comp.length < e.dados.length;
    const carga = usarDeflate ? comp : e.dados;
    const metodo = usarDeflate ? 8 : 0;
    const { t, f } = horaDos(e.mtime);

    const lh = Buffer.alloc(30);
    lh.writeUInt32LE(0x04034b50, 0);
    lh.writeUInt16LE(20, 4);
    lh.writeUInt16LE(0x0800, 6); // nomes em UTF-8
    lh.writeUInt16LE(metodo, 8);
    lh.writeUInt16LE(t, 10);
    lh.writeUInt16LE(f, 12);
    lh.writeUInt32LE(crc, 14);
    lh.writeUInt32LE(carga.length, 18);
    lh.writeUInt32LE(e.dados.length, 22);
    lh.writeUInt16LE(nome.length, 26);
    lh.writeUInt16LE(0, 28);
    locais.push(lh, nome, carga);

    const ch = Buffer.alloc(46);
    ch.writeUInt32LE(0x02014b50, 0);
    ch.writeUInt16LE(20, 4);
    ch.writeUInt16LE(20, 6);
    ch.writeUInt16LE(0x0800, 8);
    ch.writeUInt16LE(metodo, 10);
    ch.writeUInt16LE(t, 12);
    ch.writeUInt16LE(f, 14);
    ch.writeUInt32LE(crc, 16);
    ch.writeUInt32LE(carga.length, 20);
    ch.writeUInt32LE(e.dados.length, 24);
    ch.writeUInt16LE(nome.length, 28);
    ch.writeUInt32LE((0o100644 << 16) >>> 0, 38); // permissoes
    ch.writeUInt32LE(offset, 42);
    centrais.push(ch, nome);

    offset += lh.length + nome.length + carga.length;
  }

  const corpo = Buffer.concat(locais);
  const central = Buffer.concat(centrais);
  const fim = Buffer.alloc(22);
  fim.writeUInt32LE(0x06054b50, 0);
  fim.writeUInt16LE(entradas.length, 8);
  fim.writeUInt16LE(entradas.length, 10);
  fim.writeUInt32LE(central.length, 12);
  fim.writeUInt32LE(corpo.length, 16);

  return Buffer.concat([corpo, central, fim]);
}

// ---------------------------------------------------------------- varredura

function varrer(raiz, rel = "", saida = []) {
  for (const it of fs.readdirSync(path.join(raiz, rel), { withFileTypes: true })) {
    if (EXCLUIR.some((re) => re.test(it.name))) continue;
    const r = rel ? `${rel}/${it.name}` : it.name;
    const abs = path.join(raiz, r);
    if (it.isSymbolicLink()) continue;
    if (it.isDirectory()) varrer(raiz, r, saida);
    else if (it.isFile()) saida.push({ nome: r, abs, mtime: fs.statSync(abs).mtime });
  }
  return saida;
}

function detectarDir() {
  for (const c of CANDIDATOS) {
    if (c === ".") return ".";
    if (fs.existsSync(c) && fs.statSync(c).isDirectory() && fs.readdirSync(c).length) return c;
  }
  return ".";
}

function carregar() {
  if (!fs.existsSync(F_CFG) || !fs.existsSync(F_CRED)) {
    morrer("Projeto nao autorizado. Rode primeiro:  npx <pacote> init --token nvm_...");
  }
  return {
    cfg: JSON.parse(fs.readFileSync(F_CFG, "utf8")),
    token: JSON.parse(fs.readFileSync(F_CRED, "utf8")).token,
  };
}

// ------------------------------------------------------------------ comandos

async function cmdInit() {
  const token = arg("token");
  if (!token) morrer("Falta --token");

  const { endpoint } = lerToken(token);
  const cfg = { endpoint, dir: arg("dir") || null };

  process.stdout.write("  Verificando token... ");
  const st = await chamar("status", cfg, token);
  console.log("ok");

  fs.mkdirSync(DIR_CFG, { recursive: true });
  fs.writeFileSync(F_CFG, JSON.stringify({ ...cfg, site_url: st.site_url }, null, 2));
  fs.writeFileSync(F_CRED, JSON.stringify({ token }, null, 2));
  fs.writeFileSync(path.join(DIR_CFG, ".gitignore"), "*\n");
  fs.chmodSync(F_CRED, 0o600);

  console.log(`\n  Projeto autorizado para ${st.site_url}`);
  console.log(`  Limite de upload do servidor: ${st.limite_upload}`);
  console.log(`\n  Publique com:  npx <pacote>\n`);
}

async function cmdDeploy() {
  const { cfg, token } = carregar();
  const dir = arg("dir") || cfg.dir || detectarDir();

  if (!fs.existsSync(dir)) morrer(`Diretorio nao encontrado: ${dir}`);

  const arquivos = varrer(dir);
  if (!arquivos.length) morrer(`Nada para publicar em "${dir}".`);

  console.log(`\n  Publicando "${dir}" (${arquivos.length} arquivos)`);

  const zip = montarZip(
    arquivos.map((a) => ({ nome: a.nome, dados: fs.readFileSync(a.abs), mtime: a.mtime }))
  );
  console.log(`  Pacote: ${(zip.length / 1024).toFixed(0)} KB`);

  const fd = new FormData();
  fd.append("package", new Blob([zip]), "deploy.zip");

  const r = await chamar("deploy", cfg, token, fd);
  console.log(`\n  No ar: ${r.url}`);
  console.log(`  Release ${r.release}, ${r.arquivos} arquivos\n`);
}

async function cmdStatus() {
  const { cfg, token } = carregar();
  const r = await chamar("status", cfg, token);
  console.log(`\n  Site:    ${r.site_url}`);
  console.log(`  Release: ${r.release_atual ?? "nenhuma"}`);
  console.log(`  Guardadas: ${r.releases}\n`);
}

async function cmdRollback() {
  const { cfg, token } = carregar();
  const fd = new FormData();
  const alvo = arg("release");
  if (alvo) fd.append("release", alvo);
  const r = await chamar("rollback", cfg, token, fd);
  console.log(`\n  Restaurada a release ${r.release}`);
  console.log(`  ${r.url}\n`);
}

function cmdDisconnect() {
  fs.rmSync(DIR_CFG, { recursive: true, force: true });
  console.log("\n  Autorizacao removida deste projeto.\n");
}

function cmdHelp() {
  console.log(`
  Nuvem Deploy

    init --token nvm_...   autoriza este projeto
    (sem comando)          publica
    status                 mostra o estado
    rollback [--release]   volta a versao anterior
    disconnect             remove a autorizacao local

  Opcoes:  --dir <pasta>   diretorio a publicar
`);
}

// ---------------------------------------------------------------------- rota

const cmd = process.argv[2]?.startsWith("--") ? "deploy" : process.argv[2] || "deploy";

try {
  if (cmd === "init") await cmdInit();
  else if (cmd === "deploy") await cmdDeploy();
  else if (cmd === "status") await cmdStatus();
  else if (cmd === "rollback") await cmdRollback();
  else if (cmd === "disconnect") cmdDisconnect();
  else cmdHelp();
} catch (e) {
  morrer(`Erro inesperado: ${e.message}`);
}
