import 'dotenv/config';
import * as fs from 'fs';
import * as path from 'path';
import * as XLSX from 'xlsx';

/**
 * Dispara la recuperación de comprobantes históricos por CDC contra el endpoint
 * `POST /api/v1/marangatu/recuperar/por-cdc`.
 *
 * Existe porque el import de Marangatu solo alcanza los períodos publicados en el
 * portal de la DNIT: para comprobantes viejos el único camino es preguntarle a SIFEN
 * documento por documento. Este script toma la lista de CDC (de planillas o de un
 * JSON), la manda en lotes y reporta qué entró y qué falló, para no depender de que
 * alguien pegue 600 CDC a mano en el panel.
 *
 * Uso:
 *   pnpm recuperar:cdc -- --token=<jwt> --archivo=Facturas.xlsx --archivo=NC.xlsx --dry
 *   pnpm recuperar:cdc -- --token=<jwt> --json=/tmp/cdcs-2025.json --sin-cartera
 *
 * Flags:
 *   --api=<url>       Base del backend. Default http://localhost:3005/api/v1
 *   --lote=<n>        CDC por request al backend. Default 20.
 *   --dry             No escribe: solo consulta SIFEN y reporta.
 *   --sin-cartera     Importa las facturas con saldo cero (histórico solo fiscal).
 */

type Args = Record<string, string | boolean> & { archivo?: string[] };

function parseArgs(): Args {
  const out: any = { archivo: [] };
  for (const raw of process.argv.slice(2)) {
    const m = raw.match(/^--([^=]+)(?:=(.*))?$/);
    if (!m) continue;
    const [, k, v] = m;
    if (k === 'archivo') out.archivo.push(v);
    else out[k] = v ?? true;
  }
  return out;
}

/** Un CDC de SIFEN son exactamente 44 dígitos. */
const ES_CDC = /^\d{44}$/;

/**
 * Barre toda la planilla celda por celda en vez de buscar una columna "CDC": estas
 * planillas las arma el contador a mano y el encabezado cambia de archivo en archivo.
 */
function cdcsDeExcel(archivo: string): string[] {
  const wb = XLSX.readFile(archivo);
  const encontrados = new Set<string>();
  for (const hoja of wb.SheetNames) {
    const filas: any[][] = XLSX.utils.sheet_to_json(wb.Sheets[hoja], { header: 1, raw: false, defval: '' });
    for (const fila of filas) {
      for (const celda of fila) {
        const v = String(celda ?? '').replace(/\s/g, '');
        if (ES_CDC.test(v)) encontrados.add(v);
      }
    }
  }
  return [...encontrados];
}

/** Acepta tanto un array plano como el shape {"MM/YYYY": {FACTURA: [], NC: []}}. */
function cdcsDeJson(archivo: string): string[] {
  const data = JSON.parse(fs.readFileSync(archivo, 'utf-8'));
  const encontrados = new Set<string>();
  const recorrer = (n: any) => {
    if (typeof n === 'string') {
      const v = n.replace(/\s/g, '');
      if (ES_CDC.test(v)) encontrados.add(v);
    } else if (Array.isArray(n)) n.forEach(recorrer);
    else if (n && typeof n === 'object') Object.values(n).forEach(recorrer);
  };
  recorrer(data);
  return [...encontrados];
}

async function main() {
  const args = parseArgs();
  const token = args.token as string;
  if (!token) throw new Error('Falta --token=<jwt> de un usuario con permiso FIS_MAR_MARANGATU_GENERAR');

  const api = String(args.api ?? 'http://localhost:3005/api/v1').replace(/\/+$/, '');
  const lote = Number(args.lote ?? 20);
  const dry = !!args.dry;
  const sinCartera = !!args['sin-cartera'];

  const cdcs = new Set<string>();
  for (const a of args.archivo ?? []) cdcsDeExcel(path.resolve(a)).forEach((c) => cdcs.add(c));
  if (args.json) cdcsDeJson(path.resolve(String(args.json))).forEach((c) => cdcs.add(c));

  const lista = [...cdcs];
  if (lista.length === 0) throw new Error('No se encontró ningún CDC. Pasá --archivo=<xlsx> o --json=<file>');

  console.log(`CDC a recuperar: ${lista.length}`);
  console.log(`Modo: ${dry ? 'DRY-RUN (no escribe)' : 'REAL'}${sinCartera ? ' | facturas con saldo cero' : ''}`);

  const totales = { pedidos: 0, recuperados: 0, fallidos: [] as any[], detalle: [] as any[] };

  for (let i = 0; i < lista.length; i += lote) {
    const chunk = lista.slice(i, i + lote);
    const nro = Math.floor(i / lote) + 1;
    process.stdout.write(`Lote ${nro} (${chunk.length} CDC)... `);

    let res: Response;
    try {
      res = await fetch(`${api}/marangatu/recuperar/por-cdc`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
        body: JSON.stringify({ cdcs: chunk, lote_size: chunk.length, dry_run: dry, sin_cartera: sinCartera }),
      });
    } catch (err: any) {
      // Backend caído o corte de red: se anota el lote y se sigue. Una corrida de 30
      // lotes no puede morir por un timeout en el lote 12 y perder lo ya hecho.
      console.log(`RED: ${err?.message ?? err}`);
      chunk.forEach((cdc) => totales.fallidos.push({ cdc, motivo: `Red: ${err?.message ?? err}` }));
      continue;
    }

    const json: any = await res.json().catch(() => null);
    if (!res.ok) {
      // Un lote caído no corta la corrida: se anota y se sigue con el resto.
      console.log(`ERROR ${res.status}: ${json?.message ?? 'sin detalle'}`);
      chunk.forEach((cdc) => totales.fallidos.push({ cdc, motivo: `HTTP ${res.status}` }));
      continue;
    }

    totales.pedidos += json.cdcs_pedidos ?? chunk.length;
    totales.recuperados += json.recuperados ?? 0;
    (json.fallidos ?? []).forEach((f: any) => totales.fallidos.push(f));
    totales.detalle.push({ lote: nro, ...json });

    // En modo real "recuperados" solo dice cuantos contesto SIFEN, no cuantos se
    // guardaron: un documento puede volver entero y morir al persistirse. Se muestran
    // los contadores del import para no correr a ciegas.
    if (dry) {
      console.log(`ok — recuperados ${json.recuperados ?? 0}, fallidos ${(json.fallidos ?? []).length}`);
    } else {
      const f = json.facturas ?? {};
      const n = json.notas_credito ?? {};
      const imp = (Number(f.importados) || 0) + (Number(n.importados) || 0);
      const omi = (Number(f.omitidos) || 0) + (Number(n.omitidos) || 0);
      const ya = (Number(f.yaExistentes) || 0) + (Number(n.yaExistentes) || 0);
      console.log(`ok — importados ${imp}, ya existian ${ya}, omitidos ${omi}`);
      if (omi > 0) console.log(`   ⚠ ${omi} omitidos: ver logs/error-*.log del backend (MarangatuSyncService)`);
    }
  }

  console.log('\n─── Resumen ───');
  console.log(`Pedidos:     ${totales.pedidos}`);
  console.log(`Recuperados: ${totales.recuperados}`);
  console.log(`Fallidos:    ${totales.fallidos.length}`);

  if (!dry) {
    const sum = (path: string[]) =>
      totales.detalle.reduce((a, d) => {
        let v: any = d;
        for (const k of path) v = v?.[k];
        return a + (Number(v) || 0);
      }, 0);
    console.log(`\nFacturas importadas: ${sum(['facturas', 'importados'])}`);
    console.log(`NC importadas:       ${sum(['notas_credito', 'importados'])}`);
    console.log(`Ya existentes:       ${sum(['facturas', 'yaExistentes']) + sum(['notas_credito', 'yaExistentes'])}`);
    console.log(`Clientes creados:    ${sum(['facturas', 'clientesCreados'])}`);
    const omi = sum(['facturas', 'omitidos']) + sum(['notas_credito', 'omitidos']);
    if (omi > 0) console.log(`\n⚠  ${omi} documentos omitidos por error al guardar. Revisar logs del backend.`);
    console.log(`Cartera saldada:     ${sum(['cartera_saldada', 'facturas'])} facturas`);
  }

  if (dry) {
    // Solo las facturas a crédito abren saldo: son las que hay que mirar antes de
    // decidir si la corrida real va con --sin-cartera.
    const sum = (k: string) => totales.detalle.reduce((a, d) => a + (Number(d[k]) || 0), 0);
    const credito = sum('facturas_credito');
    console.log(`\nFacturas:    ${sum('facturas')} (contado ${sum('facturas_contado')}, crédito ${credito})`);
    console.log(`NC:          ${sum('notas_credito')}`);
    if (credito > 0) {
      console.log(`\n⚠  ${credito} facturas a crédito por Gs. ${sum('monto_credito').toLocaleString('es-PY')}`);
      console.log('   Sin --sin-cartera se cargan con el saldo abierto. Si ya se cobraron,');
      console.log('   le arman al cliente deuda que no existe.');
    }
  }

  if (totales.fallidos.length > 0) {
    const salida = path.resolve('cdc-fallidos.json');
    fs.writeFileSync(salida, JSON.stringify(totales.fallidos, null, 2));
    console.log(`Detalle de fallidos en ${salida}`);
    const porMotivo = new Map<string, number>();
    for (const f of totales.fallidos) porMotivo.set(f.motivo, (porMotivo.get(f.motivo) ?? 0) + 1);
    for (const [motivo, n] of [...porMotivo].sort((a, b) => b[1] - a[1]).slice(0, 8)) {
      console.log(`  ${n}x ${motivo}`);
    }
  }
}

main().catch((e) => {
  console.error(e.message ?? e);
  process.exit(1);
});
