/**
 * ╔══════════════════════════════════════════════════════════════╗
 * ║   MIGRACIÓN: Facturas de compra a proveedores desde CSV        ║
 * ╠══════════════════════════════════════════════════════════════╣
 * ║  Crea proveedores, facturas de compra (compra_cab + un ítem    ║
 * ║  de detalle) y su cuenta por pagar, respetando lo ya pagado.   ║
 * ║                                                                ║
 * ║  QUÉ SE MIGRA Y CÓMO:                                          ║
 * ║   · Toda factura queda registrada, esté pagada o no.           ║
 * ║   · La cuenta por pagar se crea SIEMPRE (es lo que hace el     ║
 * ║     flujo normal cuando generar_cxp_automatico está activo) y  ║
 * ║     se guarda con el monto pagado y el saldo del CSV, así lo   ║
 * ║     adeudado queda pendiente y lo cobrado queda saldado.       ║
 * ║   · NO se crean pagos_proveedor. Esos pagos ya ocurrieron en   ║
 * ║     el sistema anterior; registrarlos acá movería saldos de    ║
 * ║     tesorería que ya se movieron, contando la plata dos veces. ║
 * ║   · El detalle es una sola línea sintética con afecta_stock    ║
 * ║     en false: el CSV no trae ítems y el inventario se migra    ║
 * ║     por separado.                                              ║
 * ║                                                                ║
 * ║  CSV (ISO-8859-1, separado por ';'):                           ║
 * ║    FECHA;CONDICION;VENCIMIENTO;FALTAN(d);RUBRO;PROVEEDOR;RUC;  ║
 * ║    FACTURA;TIMBRADO;RECIBO;IVA 5;IVA 10;IVA TOTAL;GRAVADO 5;   ║
 * ║    GRAVADO 10;EXENTO;TOTAL;PAGADO;ADEUDADO;MDA                 ║
 * ║                                                                ║
 * ║  Idempotente: la factura se busca por la misma clave que el    ║
 * ║  índice único parcial `uk_compra_documento_activo`             ║
 * ║  (empresa + timbrado + establecimiento + punto + número).      ║
 * ║  Reejecutarlo no duplica ni vuelve a tocar la CxP.             ║
 * ╠══════════════════════════════════════════════════════════════╣
 * ║  Uso:                                                          ║
 * ║    npx ts-node --transpile-only -r tsconfig-paths/register \   ║
 * ║      --project tsconfig.scripts.json \                         ║
 * ║      scripts/migrate-access/migrate-facturas-proveedor-taller.ts \
 * ║      --empresa=<uuid> --dry                                    ║
 * ║                                                                ║
 * ║  Flags:                                                        ║
 * ║    --dry               no escribe nada                         ║
 * ║    --archivo=ruta.csv  otro CSV                                ║
 * ║    --incluir-haberes   incluye las filas cuyo RUBRO no es      ║
 * ║                        "COMPRAS A PROVEEDORES" (liquidaciones  ║
 * ║                        de haberes). Por defecto se saltean:    ║
 * ║                        no son facturas de proveedor, vienen    ║
 * ║                        sin timbrado y con numeración propia,   ║
 * ║                        y entrarían al Libro IVA como           ║
 * ║                        comprobantes que no existen.            ║
 * ║    --electronicas      marca las compras como NO preimpresas   ║
 * ║                        (es_preimpreso=false), o sea que ya     ║
 * ║                        están declaradas en SIFEN/Marangatu y   ║
 * ║                        no se vuelven a declarar en el Libro    ║
 * ║                        IVA. Por defecto se marcan preimpresas. ║
 * ╚══════════════════════════════════════════════════════════════╝
 */

import * as fs from 'fs';
import * as path from 'path';
import { PrismaClient } from '@prisma/client';

// ══════════════════════════════════════════════════════════════════
//  CONFIG
// ══════════════════════════════════════════════════════════════════
const CONFIG = {
  DEFAULT_CSV_PATH: path.resolve(
    __dirname,
    '../../scripts/data_script/cliente-agogo/taller/FACTURAS_PROVEEDORES.csv',
  ),
  // El archivo viene de un sistema legado en Windows: latin1, no UTF-8.
  ENCODING: 'latin1' as BufferEncoding,
  SEPARADOR: ';',
  RUBRO_COMPRAS: 'COMPRAS A PROVEEDORES',
  PUNTO_ESTABLECIMIENTO: '001',
  MONEDA_CODIGO: 'PYG',
  // condicion_operacion es catálogo global: 1 = Contado, 2 = Crédito.
  COND_CONTADO: 1,
  COND_CREDITO: 2,
};

// ══════════════════════════════════════════════════════════════════
//  ARGUMENTOS
// ══════════════════════════════════════════════════════════════════
function argValue(flag: string): string | undefined {
  const arg = process.argv.find((a) => a.startsWith(`--${flag}=`));
  return arg ? arg.split('=').slice(1).join('=') : undefined;
}

const DRY = process.argv.includes('--dry');
const INCLUIR_HABERES = process.argv.includes('--incluir-haberes');
const ES_PREIMPRESO = !process.argv.includes('--electronicas');
const archivoArg = argValue('archivo');
const CSV_PATH = archivoArg ? path.resolve(process.cwd(), archivoArg) : CONFIG.DEFAULT_CSV_PATH;

const empresaArg = argValue('empresa');
if (!empresaArg) {
  console.error('✗✗FATAL Falta --empresa=<uuid>. Este script no corre contra una empresa por defecto.');
  process.exit(1);
}
const EMPRESA_ID: string = empresaArg;

// ══════════════════════════════════════════════════════════════════
//  LOGGER
// ══════════════════════════════════════════════════════════════════
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
const LOG_FILE = path.join(__dirname, `reporte-facturas-proveedor-${timestamp}.log`);
const logStream = fs.createWriteStream(LOG_FILE, { flags: 'a' });

type LogLevel = 'INFO' | 'OK' | 'SKIP' | 'WARN' | 'ERROR' | 'FATAL' | 'RAW';

function log(level: LogLevel, msg: string) {
  const ts = new Date().toISOString().slice(11, 19);
  const prefix: Record<LogLevel, string> = {
    INFO: `[${ts}] INFO  `,
    OK: `[${ts}] ✓     `,
    SKIP: `[${ts}] ~     `,
    WARN: `[${ts}] WARN  `,
    ERROR: `[${ts}] ✗ ERR `,
    FATAL: `[${ts}] ✗✗FATAL`,
    RAW: '',
  };
  const line = prefix[level] + msg;
  console.log(line);
  logStream.write(line + '\n');
}
const info = (m: string) => log('INFO', m);
const ok = (m: string) => log('OK', m);
const skip = (m: string) => log('SKIP', m);
const warn = (m: string) => log('WARN', m);
const err = (m: string) => log('ERROR', m);
const raw = (m: string) => log('RAW', m);

// ══════════════════════════════════════════════════════════════════
//  HELPERS
// ══════════════════════════════════════════════════════════════════
const fmtGs = (n: number) => `Gs. ${Math.round(n).toLocaleString('es-PY')}`;

function toStr(v: any): string {
  return v === null || v === undefined ? '' : String(v).trim();
}

/** Importe del CSV: usa punto como separador de miles y no trae decimales. */
function toNum(v: any): number {
  const s = toStr(v).replace(/\./g, '').replace(',', '.');
  if (!s) return 0;
  const n = parseFloat(s);
  return Number.isFinite(n) ? n : 0;
}

/** dd/mm/aaaa → Date en UTC (las columnas son @db.Date, sin hora). */
function toFecha(v: string): Date | null {
  const m = toStr(v).match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
  if (!m) return null;
  return new Date(`${m[3]}-${m[2]}-${m[1]}T00:00:00.000Z`);
}

/** Sin acentos y en mayúsculas, para comparar nombres y condiciones. */
function normalizar(s: string): string {
  return toStr(s)
    .normalize('NFD')
    .replace(/[̀-ͯ]/g, '')
    .toUpperCase();
}

/**
 * "3368335-2" → { base: "3368335", dv: "2" }. `personas.ruc` es VarChar(8) y el
 * dígito verificador va en su propia columna, así que hay que separarlos.
 */
function partirRuc(ruc: string): { base: string; dv: string | null } {
  const limpio = toStr(ruc).replace(/\s+/g, '');
  if (!limpio) return { base: '', dv: null };
  const [base, dv] = limpio.split('-');
  return { base: (base ?? '').slice(0, 8), dv: dv ? dv.slice(0, 1) : null };
}

/**
 * "003-001-0029651" → establecimiento/punto/número.
 * Las liquidaciones de haberes vienen con un número suelto ("10"): en ese caso
 * no hay establecimiento ni punto de expedición y se guarda sólo el número.
 */
function partirFactura(f: string): {
  establecimiento: string | null;
  punto: string | null;
  numero: string | null;
} {
  const limpio = toStr(f);
  if (!limpio) return { establecimiento: null, punto: null, numero: null };
  const partes = limpio.split('-');
  if (partes.length === 3) {
    return { establecimiento: partes[0], punto: partes[1], numero: partes[2] };
  }
  return { establecimiento: null, punto: null, numero: limpio };
}

/**
 * Qué tan confiable es la identificación del proveedor de una fila. Sirve para
 * desempatar cuando la MISMA factura aparece dos veces con proveedores
 * distintos: en el archivo real, `002-002-0033972` está cargada una vez contra
 * "AHORA SOCIEDAD ANONIMA" (RUC 80068721-3) y otra contra el comodín
 * "GENERICO" (RUC 800). Gana el que tiene RUC de verdad.
 */
function puntajeProveedor(ruc: string, nombre: string): number {
  const { base, dv } = partirRuc(ruc);
  if (!base) return 0;
  if (normalizar(nombre) === 'GENERICO') return 1;
  if (base.length < 5) return 2; // RUCs de relleno tipo "800", "8342"
  return dv ? 4 : 3;
}

type Fila = Record<string, string>;

function leerCsv(ruta: string): Fila[] {
  const texto = fs.readFileSync(ruta, CONFIG.ENCODING).replace(/\r/g, '');
  const lineas = texto.split('\n').filter((l) => l.trim().length > 0);
  if (lineas.length < 2) return [];
  const cabecera = lineas[0].split(CONFIG.SEPARADOR).map((c) => c.trim());
  return lineas.slice(1).map((linea) => {
    const celdas = linea.split(CONFIG.SEPARADOR);
    const fila: Fila = {};
    cabecera.forEach((col, i) => (fila[col] = toStr(celdas[i])));
    return fila;
  });
}

// ══════════════════════════════════════════════════════════════════
//  MAIN
// ══════════════════════════════════════════════════════════════════
const prisma = new PrismaClient();

type Anomalia = { motivo: string; detalle: string };

async function main() {
  raw('');
  raw('╔══════════════════════════════════════════════════════╗');
  raw(`║  MIGRACIÓN FACTURAS DE COMPRA ${DRY ? '[ DRY RUN ]        ' : '[ REAL — ESCRIBE ]  '}║`);
  raw(`║  Empresa: ${EMPRESA_ID.padEnd(43)}║`);
  raw(`║  Archivo: ${path.basename(CSV_PATH).slice(0, 43).padEnd(43)}║`);
  raw(`║  Liquidaciones de haberes: ${(INCLUIR_HABERES ? 'SE INCLUYEN' : 'se saltean').padEnd(26)}║`);
  raw(`║  Comprobantes: ${(ES_PREIMPRESO ? 'preimpresos (van al Libro IVA)' : 'electrónicos (no se declaran)').padEnd(38)}║`);
  raw(`║  Log: ${path.basename(LOG_FILE).slice(0, 47).padEnd(47)}║`);
  raw('╚══════════════════════════════════════════════════════╝');
  raw('');

  // ── Resolver empresa y catálogos ───────────────────────────────
  const empresa = await prisma.empresas.findUnique({
    where: { id: EMPRESA_ID },
    select: { id: true, razon_social: true },
  });
  if (!empresa) {
    log('FATAL', `No existe ninguna empresa con id ${EMPRESA_ID}`);
    process.exit(1);
  }
  info(`Empresa: ${empresa.razon_social} (${empresa.id})`);

  // sucursal y depósito son opcionales en compra_cab. Se usan si están.
  const sucursal = await prisma.empresas_sucursales.findFirst({
    where: { empresa_id: EMPRESA_ID, punto_establecimiento: CONFIG.PUNTO_ESTABLECIMIENTO },
    select: { id: true, descripcion: true },
  });
  if (sucursal) info(`Sucursal ${CONFIG.PUNTO_ESTABLECIMIENTO}: ${sucursal.descripcion} (${sucursal.id})`);
  else warn(`La empresa no tiene sucursal con punto_establecimiento='${CONFIG.PUNTO_ESTABLECIMIENTO}'. Las compras quedan sin sucursal.`);

  const deposito = sucursal
    ? await prisma.depositos.findFirst({
        where: { sucursal_id: sucursal.id, es_principal: true },
        select: { id: true, descripcion: true },
      })
    : null;
  if (deposito) info(`Depósito principal: ${deposito.descripcion} (${deposito.id})`);
  else warn('Sin depósito principal. No afecta al stock porque el detalle va con afecta_stock=false.');

  const moneda = await prisma.moneda.findFirst({
    where: { codigo: CONFIG.MONEDA_CODIGO },
    select: { id: true, descripcion: true },
  });
  if (!moneda) {
    log('FATAL', `No existe la moneda con código '${CONFIG.MONEDA_CODIGO}' en el catálogo global.`);
    process.exit(1);
  }
  info(`Moneda: ${moneda.descripcion} (${moneda.id})`);

  const [condContado, condCredito] = await Promise.all([
    prisma.condicion_operacion.findFirst({ where: { codigo: CONFIG.COND_CONTADO }, select: { id: true } }),
    prisma.condicion_operacion.findFirst({ where: { codigo: CONFIG.COND_CREDITO }, select: { id: true } }),
  ]);
  if (!condContado || !condCredito) {
    log('FATAL', 'Faltan condiciones de operación (1=Contado, 2=Crédito) en el catálogo global.');
    process.exit(1);
  }
  raw('');

  // ── Leer CSV ───────────────────────────────────────────────────
  if (!fs.existsSync(CSV_PATH)) {
    log('FATAL', `Archivo no encontrado: ${CSV_PATH}`);
    process.exit(1);
  }
  const filas = leerCsv(CSV_PATH);
  info(`Filas leídas del CSV : ${filas.length}`);

  const columnasReq = ['FECHA', 'CONDICION', 'PROVEEDOR', 'RUC', 'FACTURA', 'TOTAL', 'PAGADO', 'ADEUDADO'];
  const faltantes = columnasReq.filter((c) => !(c in (filas[0] ?? {})));
  if (faltantes.length > 0) {
    log('FATAL', `Faltan columnas en el CSV: ${faltantes.join(', ')}`);
    process.exit(1);
  }
  raw('');

  // ── Deduplicar por documento ───────────────────────────────────
  // La clave es la misma del índice único parcial de compra_cab. Cuando una
  // factura viene repetida, se importa la fila con el proveedor mejor
  // identificado; antes ganaba la que aparecía primero, y eso dejó una compra
  // atribuida al comodín "GENERICO" y al proveedor real sin ninguna.
  const anomalias: Anomalia[] = [];
  const mejorPorDocumento = new Map<string, Fila>();
  for (const f of filas) {
    if (!INCLUIR_HABERES && normalizar(f['RUBRO']) !== CONFIG.RUBRO_COMPRAS) continue;
    const clave = `${toStr(f['FACTURA'])}|${toStr(f['TIMBRADO'])}`;
    const previa = mejorPorDocumento.get(clave);
    if (!previa) {
      mejorPorDocumento.set(clave, f);
      continue;
    }
    const pNueva = puntajeProveedor(toStr(f['RUC']), toStr(f['PROVEEDOR']));
    const pPrevia = puntajeProveedor(toStr(previa['RUC']), toStr(previa['PROVEEDOR']));
    const gana = pNueva > pPrevia ? f : previa;
    const pierde = gana === f ? previa : f;
    mejorPorDocumento.set(clave, gana);
    anomalias.push({
      motivo: 'FACTURA REPETIDA CON DISTINTO PROVEEDOR — se importa la del RUC más confiable',
      detalle: `${toStr(f['FACTURA'])} (timbrado ${toStr(f['TIMBRADO']) || '—'}) → se usa "${toStr(gana['PROVEEDOR'])}" (${toStr(gana['RUC']) || 'sin RUC'}); se descarta "${toStr(pierde['PROVEEDOR'])}" (${toStr(pierde['RUC']) || 'sin RUC'})`,
    });
  }
  const filasAMigrar = [...mejorPorDocumento.values()];

  const erroresList: string[] = [];
  let proveedoresCreados = 0;
  let proveedoresReusados = 0;
  let comprasCreadas = 0;
  let comprasExistentes = 0;
  // Se cuentan acá porque el loop de abajo ya recorre la lista deduplicada.
  const haberesSalteadas = filas.filter(
    (f) => !INCLUIR_HABERES && normalizar(f['RUBRO']) !== CONFIG.RUBRO_COMPRAS,
  ).length;
  let totalCeroSalteadas = 0;
  let cxpPendientes = 0;
  let cxpParciales = 0;
  let cxpPagadas = 0;
  let totalMigrado = 0;
  let totalAdeudado = 0;

  // ── Paso 1: proveedores ────────────────────────────────────────
  // Se agrupan por RUC, no por nombre: el CSV trae el mismo RUC escrito con
  // dos razones sociales distintas (RAMIREZ AUTOREPUESTO / AUTOREPUESTOS) y
  // son el mismo proveedor. Sin RUC, el nombre es lo único que queda.
  const porClave = new Map<string, { nombre: string; ruc: string; alias: Set<string> }>();
  for (const f of filasAMigrar) {
    const nombre = toStr(f['PROVEEDOR']);
    const ruc = toStr(f['RUC']);
    if (!nombre) continue;
    const { base } = partirRuc(ruc);
    const clave = base ? `RUC:${base}` : `NOMBRE:${normalizar(nombre)}`;
    const prev = porClave.get(clave);
    if (prev) prev.alias.add(nombre);
    else porClave.set(clave, { nombre, ruc, alias: new Set([nombre]) });
  }

  info(`Proveedores distintos a asegurar: ${porClave.size}`);
  for (const [clave, p] of porClave) {
    if (p.alias.size > 1) {
      anomalias.push({
        motivo: 'MISMO RUC CON VARIAS RAZONES SOCIALES — se unifica en una sola ficha',
        detalle: `${clave} → se usa "${p.nombre}"; también aparece como: ${[...p.alias].filter((a) => a !== p.nombre).join(', ')}`,
      });
    }
    if (!partirRuc(p.ruc).base) {
      anomalias.push({ motivo: 'PROVEEDOR SIN RUC — se crea igual, cargalo después', detalle: p.nombre });
    } else if (!partirRuc(p.ruc).dv) {
      anomalias.push({
        motivo: 'RUC SIN DÍGITO VERIFICADOR — se guarda sin DV',
        detalle: `${p.nombre} (${p.ruc})`,
      });
    }
  }

  const proveedorIdPorClave = new Map<string, string>();

  for (const [clave, p] of porClave) {
    const { base, dv } = partirRuc(p.ruc);
    try {
      // ¿Ya existe en la empresa? Se busca por RUC de la persona; si el
      // proveedor vino sin RUC, por razón social exacta.
      const existente = await prisma.proveedores.findFirst({
        where: {
          empresa_id: EMPRESA_ID,
          personas: base ? { ruc: base } : { razon_social: p.nombre },
        },
        select: { id: true, personas: { select: { razon_social: true } } },
      });

      if (existente) {
        proveedorIdPorClave.set(clave, existente.id);
        proveedoresReusados++;
        skip(`Proveedor ya existente | ${existente.personas?.razon_social ?? p.nombre}`);
        continue;
      }

      if (DRY) {
        proveedorIdPorClave.set(clave, `DRY-${clave}`);
        proveedoresCreados++;
        info(`[DRY] Proveedor a crear | ${p.nombre} | RUC ${base || '—'}-${dv || '—'}`);
        continue;
      }

      const persona = await prisma.personas.create({
        data: {
          empresa_id: EMPRESA_ID,
          razon_social: p.nombre,
          ruc: base || null,
          dv,
          active: true,
          deleted: false,
        },
        select: { id: true },
      });
      const proveedor = await prisma.proveedores.create({
        data: {
          empresa_id: EMPRESA_ID,
          persona_id: persona.id,
          tipo_entidad: 'PROVEEDOR_LOCAL',
          es_extranjero: false,
          activo: true,
          observaciones: `Alta por migración desde ${path.basename(CSV_PATH)}`,
        },
        select: { id: true },
      });
      proveedorIdPorClave.set(clave, proveedor.id);
      proveedoresCreados++;
      ok(`Proveedor creado | ${p.nombre} | RUC ${base || '—'}${dv ? `-${dv}` : ''}`);
    } catch (e: any) {
      const msg = `Proveedor "${p.nombre}" | ${e.message}`;
      erroresList.push(msg);
      err(msg);
    }
  }
  raw('');

  // ── Paso 2: facturas ───────────────────────────────────────────
  for (const f of filasAMigrar) {
    const proveedorNombre = toStr(f['PROVEEDOR']);
    const facturaTexto = toStr(f['FACTURA']);
    const label = `${toStr(f['FECHA'])} ${proveedorNombre} ${facturaTexto}`;

    // Una factura en 0 no es una compra: en el archivo real es una fila de
    // relleno del sistema viejo. Importarla dejaría una compra y una cuenta
    // por pagar de importe cero ensuciando el estado de cuenta.
    if (toNum(f['TOTAL']) === 0) {
      totalCeroSalteadas++;
      anomalias.push({ motivo: 'FACTURA CON TOTAL 0 — no se migra', detalle: label });
      skip(`Salteada (total 0) | ${label}`);
      continue;
    }

    const fechaEmision = toFecha(f['FECHA']);
    if (!fechaEmision) {
      anomalias.push({ motivo: 'FECHA ILEGIBLE — fila no migrada', detalle: `${label} | FECHA="${toStr(f['FECHA'])}"` });
      warn(`Fila omitida — fecha ilegible | ${label}`);
      continue;
    }
    const fechaVencimiento = toFecha(f['VENCIMIENTO']) ?? fechaEmision;

    const { base } = partirRuc(toStr(f['RUC']));
    const clave = base ? `RUC:${base}` : `NOMBRE:${normalizar(proveedorNombre)}`;
    const proveedorId = proveedorIdPorClave.get(clave);
    if (!proveedorId) {
      anomalias.push({ motivo: 'SIN PROVEEDOR RESUELTO — fila no migrada', detalle: label });
      warn(`Fila omitida — no se resolvió el proveedor | ${label}`);
      continue;
    }

    const { establecimiento, punto, numero } = partirFactura(facturaTexto);
    const timbrado = toStr(f['TIMBRADO']) || null;
    const esCredito = normalizar(f['CONDICION']).startsWith('CRED');

    const gravado5 = toNum(f['GRAVADO 5']);
    const gravado10 = toNum(f['GRAVADO 10']);
    const iva5 = toNum(f['IVA 5']);
    const iva10 = toNum(f['IVA 10']);
    const ivaTotal = toNum(f['IVA TOTAL']);
    const exento = toNum(f['EXENTO']);
    const total = toNum(f['TOTAL']);
    const pagado = toNum(f['PAGADO']);
    const adeudado = toNum(f['ADEUDADO']);
    const subtotal = gravado5 + gravado10 + exento;

    // Chequeos de consistencia contra el propio archivo.
    if (Math.round(pagado + adeudado) !== Math.round(total)) {
      anomalias.push({
        motivo: 'PAGADO + ADEUDADO ≠ TOTAL — se respeta el TOTAL del archivo',
        detalle: `${label} | ${fmtGs(pagado)} + ${fmtGs(adeudado)} ≠ ${fmtGs(total)}`,
      });
    }
    if (Math.round(subtotal + ivaTotal) !== Math.round(total)) {
      anomalias.push({
        motivo: 'GRAVADO + IVA + EXENTO ≠ TOTAL (redondeo del sistema viejo)',
        detalle: `${label} | desglose ${fmtGs(subtotal + ivaTotal)} vs total ${fmtGs(total)} (dif ${fmtGs(total - subtotal - ivaTotal)})`,
      });
    }
    if (!numero) {
      anomalias.push({ motivo: 'SIN NÚMERO DE FACTURA', detalle: label });
    }

    // Estado según lo que quedó debiendo. Es lo que pidió el cliente: lo
    // pagado queda saldado y lo adeudado sigue abierto.
    const estado = adeudado <= 0 ? 'pagada' : pagado > 0 ? 'parcial' : 'pendiente';

    // Misma clave que el índice único parcial uk_compra_documento_activo. Se
    // consulta también en dry-run: si no, el informe diría que va a crear
    // facturas que en realidad ya están migradas.
    const yaExiste = await prisma.compra_cab.findFirst({
      where: {
        empresa_id: EMPRESA_ID,
        timbrado_proveedor: timbrado,
        establecimiento,
        punto_expedicion: punto,
        numero_factura: numero,
        anulado: false,
      },
      select: { id: true },
    });
    if (yaExiste) {
      comprasExistentes++;
      skip(`Ya migrada | ${label}`);
      continue;
    }

    if (DRY) {
      info(
        `[DRY] ${label} | ${esCredito ? 'CRÉDITO' : 'CONTADO'} | total ${fmtGs(total)} | pagado ${fmtGs(pagado)} | saldo ${fmtGs(adeudado)} → ${estado}`,
      );
      comprasCreadas++;
      totalMigrado += total;
      totalAdeudado += adeudado;
      if (estado === 'pagada') cxpPagadas++;
      else if (estado === 'parcial') cxpParciales++;
      else cxpPendientes++;
      continue;
    }

    try {
      await prisma.$transaction(async (tx) => {
        const compra = await tx.compra_cab.create({
          data: {
            empresa_id: EMPRESA_ID,
            sucursal_id: sucursal?.id ?? null,
            deposito_id: deposito?.id ?? null,
            proveedor_id: proveedorId,
            moneda_id: moneda.id,
            condicion_operacion_id: esCredito ? condCredito.id : condContado.id,
            timbrado_proveedor: timbrado,
            establecimiento,
            punto_expedicion: punto,
            numero_factura: numero,
            fecha_emision: fechaEmision,
            total_exento: exento,
            total_gravado_5: gravado5,
            total_gravado_10: gravado10,
            total_iva_5: iva5,
            total_iva_10: iva10,
            total_iva: ivaTotal,
            subtotal,
            total,
            cotizacion: 1,
            estado,
            plazo_credito: 0,
            es_preimpreso: ES_PREIMPRESO,
            activo: true,
            anulado: false,
            observaciones: `Migrado desde ${path.basename(CSV_PATH)} — rubro: ${toStr(f['RUBRO'])}`,
          },
          select: { id: true },
        });

        // Una sola línea sintética: el CSV no trae ítems. afecta_stock=false
        // para que la migración de facturas no mueva inventario.
        await tx.compra_det.create({
          data: {
            compra_id: compra.id,
            descripcion: `Compra migrada — ${toStr(f['RUBRO'])}`,
            cantidad: 1,
            precio_unitario: subtotal,
            porcentaje_iva: gravado10 > 0 ? 10 : gravado5 > 0 ? 5 : 0,
            monto_iva: ivaTotal,
            subtotal: total,
            afecta_stock: false,
          },
        });

        // La CxP se crea siempre (igual que el flujo normal con
        // generar_cxp_automatico) y se guarda con lo ya pagado. NO se crean
        // pagos_proveedor: esos movimientos ya ocurrieron en el sistema viejo.
        await tx.cuentas_pagar.create({
          data: {
            empresa_id: EMPRESA_ID,
            compra_id: compra.id,
            proveedor_id: proveedorId,
            moneda_id: moneda.id,
            origen_tipo: 'compra',
            origen_id: compra.id,
            numero_cuota: 1,
            total_cuotas: 1,
            fecha_emision: fechaEmision,
            fecha_vencimiento: fechaVencimiento,
            monto_original: total,
            monto_pagado: pagado,
            saldo_pendiente: adeudado,
            estado,
            activo: true,
            observaciones: `Migrada desde ${path.basename(CSV_PATH)}. Pagos previos no se registran como pagos_proveedor: ya se movieron en el sistema anterior.`,
          },
        });
      });

      comprasCreadas++;
      totalMigrado += total;
      totalAdeudado += adeudado;
      if (estado === 'pagada') cxpPagadas++;
      else if (estado === 'parcial') cxpParciales++;
      else cxpPendientes++;
      ok(`${label} | total ${fmtGs(total)} | saldo ${fmtGs(adeudado)} → ${estado}`);
    } catch (e: any) {
      const msg = `${label} | ${e.message}`;
      erroresList.push(msg);
      err(msg);
    }
  }

  // ── Resumen ────────────────────────────────────────────────────
  raw('');
  raw('╔══════════════════════════════════════════════════════╗');
  raw('║                    RESUMEN FINAL                     ║');
  raw('╠══════════════════════════════════════════════════════╣');
  raw(`║  Filas del CSV               : ${String(filas.length).padStart(8)}             ║`);
  raw(`║  Proveedores creados         : ${String(proveedoresCreados).padStart(8)}             ║`);
  raw(`║  Proveedores reusados        : ${String(proveedoresReusados).padStart(8)}             ║`);
  raw(`║  Facturas migradas           : ${String(comprasCreadas).padStart(8)}             ║`);
  raw(`║  Facturas ya existentes      : ${String(comprasExistentes).padStart(8)}             ║`);
  raw(`║  Salteadas (otro rubro)      : ${String(haberesSalteadas).padStart(8)}             ║`);
  raw(`║  Salteadas (total 0)         : ${String(totalCeroSalteadas).padStart(8)}             ║`);
  raw('╠══════════════════════════════════════════════════════╣');
  raw(`║  CxP pagadas                 : ${String(cxpPagadas).padStart(8)}             ║`);
  raw(`║  CxP parciales               : ${String(cxpParciales).padStart(8)}             ║`);
  raw(`║  CxP pendientes              : ${String(cxpPendientes).padStart(8)}             ║`);
  raw(`║  Total facturado             : ${fmtGs(totalMigrado).padStart(20)} ║`);
  raw(`║  Saldo que queda pendiente   : ${fmtGs(totalAdeudado).padStart(20)} ║`);
  raw('╠══════════════════════════════════════════════════════╣');
  raw(`║  Anomalías a revisar         : ${String(anomalias.length).padStart(8)}             ║`);
  raw(`║  Errores                     : ${String(erroresList.length).padStart(8)}             ║`);
  raw('╚══════════════════════════════════════════════════════╝');

  if (anomalias.length > 0) {
    raw('');
    raw('── ANOMALÍAS (revisar a mano) ───────────────────────');
    const porMotivo = new Map<string, string[]>();
    for (const a of anomalias) {
      const arr = porMotivo.get(a.motivo) ?? [];
      arr.push(a.detalle);
      porMotivo.set(a.motivo, arr);
    }
    for (const [motivo, detalles] of porMotivo) {
      raw(`  ${motivo}  (${detalles.length})`);
      detalles.slice(0, 25).forEach((d) => raw(`    · ${d}`));
      if (detalles.length > 25) raw(`    … y ${detalles.length - 25} más (ver log completo)`);
    }
  }

  if (erroresList.length > 0) {
    raw('');
    raw('── ERRORES ──────────────────────────────────────────');
    erroresList.forEach((e, i) => raw(`  [${String(i + 1).padStart(3)}] ${e}`));
  }

  raw('');
  if (DRY) {
    raw('⚠  DRY RUN — No se escribió nada en la base de datos.');
    raw('   Ejecutar sin --dry para aplicar la migración real.');
  } else {
    raw(
      erroresList.length === 0
        ? '✓  Migración completada sin errores.'
        : `⚠  Migración completada con ${erroresList.length} error(es).`,
    );
  }
  raw(`\nLog guardado en: ${LOG_FILE}\n`);
}

main()
  .catch((e) => {
    log('FATAL', `Error fatal: ${e.message}\n${e.stack}`);
    process.exit(1);
  })
  .finally(async () => {
    logStream.end();
    await prisma.$disconnect();
  });
