/**
 * ╔══════════════════════════════════════════════════════════════╗
 * ║   MIGRACIÓN: Facturas de venta ADEUDADAS + cuentas a cobrar    ║
 * ╠══════════════════════════════════════════════════════════════╣
 * ║  Migra SOLO las facturas del CSV que quedaron con saldo. Las   ║
 * ║  saldadas no se tocan: no hay nada que cobrar y meterían       ║
 * ║  cientos de documentos fiscales sin detalle en el Libro IVA    ║
 * ║  de un período que el cliente ya declaró.                      ║
 * ║                                                                ║
 * ║  Por cada factura crea:                                        ║
 * ║   · factura_cab      — el documento (estado "Pendiente")        ║
 * ║   · factura_subtotales — desglose de IVA al 10%                 ║
 * ║   · cuentas_cobrar   — la cuenta a cobrar                       ║
 * ║   · factura_cuotas   — una cuota con el saldo real              ║
 * ║                                                                ║
 * ║  NO crea factura_det: el CSV no trae ítems y `producto_id` es   ║
 * ║  obligatorio, así que no hay forma de armar una línea honesta.  ║
 * ║  NO registra cobros por lo ya pagado: esos movimientos ya       ║
 * ║  ocurrieron en el sistema anterior. El pago se refleja en el    ║
 * ║  saldo de la cuota, no como un recibo nuevo.                    ║
 * ║                                                                ║
 * ║  IVA: el archivo sólo trae el total, así que se asume 10%       ║
 * ║  (--iva para cambiarlo). base = total/1,1 e IVA = total − base. ║
 * ║                                                                ║
 * ║  CLIENTE COMODÍN: el sistema viejo cargaba ventas sin           ║
 * ║  identificar contra "AAAAAAAA" (RUC 55555). Se migran igual     ║
 * ║  contra esa misma ficha: son 52,9M de deuda real y dejarlas     ║
 * ║  afuera las borraba del sistema. Hay que reasignarlas al        ║
 * ║  cliente que corresponda cuando se sepa. Las de contado van a   ║
 * ║  una ficha "SIN NOMBRE". Con --saltear-comodin se descartan     ║
 * ║  las de crédito (comportamiento anterior).                      ║
 * ║                                                                ║
 * ║  Idempotente: la factura se busca por (empresa, est, punto,     ║
 * ║  número). Reejecutarlo no duplica.                             ║
 * ╠══════════════════════════════════════════════════════════════╣
 * ║  Uso:                                                          ║
 * ║    npx ts-node --transpile-only -r tsconfig-paths/register \   ║
 * ║      --project tsconfig.scripts.json \                         ║
 * ║      scripts/migrate-access/migrate-ventas-adeudadas-taller.ts \
 * ║      --empresa=<uuid> --dry                                    ║
 * ╚══════════════════════════════════════════════════════════════╝
 */

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

const CONFIG = {
  DEFAULT_CSV_PATH: path.resolve(
    __dirname,
    '../../scripts/data_script/cliente-agogo/taller/VENTAS_ADEUDADAS_DETALLE.csv',
  ),
  ENCODING: 'latin1' as BufferEncoding,
  SEP: ';',
  MONEDA_CODIGO: 'PYG',
  COND_CONTADO: 1,
  COND_CREDITO: 2,
  IVA_DEFAULT: 10,
  NOMBRE_SIN_IDENTIFICAR: 'SIN NOMBRE',
  // Marcas del cliente comodín del sistema viejo.
  RUC_COMODIN: ['55555'],
  NOMBRE_COMODIN: /^A{4,}$/i,
};

function argValue(flag: string): string | undefined {
  const a = process.argv.find((x) => x.startsWith(`--${flag}=`));
  return a ? a.split('=').slice(1).join('=') : undefined;
}
const DRY = process.argv.includes('--dry');
// Por defecto las ventas del cliente comodín se migran igual, contra esa misma
// ficha: dejarlas afuera hacía desaparecer 52,9M de deuda del sistema sin
// dejar rastro. Con --saltear-comodin se descartan las que son a crédito.
const SALTEAR_COMODIN = process.argv.includes('--saltear-comodin');
const CSV_PATH = argValue('archivo') ? path.resolve(process.cwd(), argValue('archivo')!) : CONFIG.DEFAULT_CSV_PATH;
const IVA = Number(argValue('iva') ?? CONFIG.IVA_DEFAULT);
if (![0, 5, 10].includes(IVA)) { console.error(`✗ --iva debe ser 0, 5 o 10 (recibido: ${IVA})`); process.exit(1); }
const empresaArg = argValue('empresa');
if (!empresaArg) { console.error('✗✗FATAL Falta --empresa=<uuid>.'); process.exit(1); }
const EMPRESA_ID: string = empresaArg;

const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
const LOG_FILE = path.join(__dirname, `reporte-ventas-adeudadas-${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 p: Record<LogLevel, string> = {
    INFO: `[${ts}] INFO  `, OK: `[${ts}] ✓     `, SKIP: `[${ts}] ~     `,
    WARN: `[${ts}] WARN  `, ERROR: `[${ts}] ✗ ERR `, FATAL: `[${ts}] ✗✗FATAL`, RAW: '',
  };
  const line = p[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);

const fmtGs = (n: number) => `Gs. ${Math.round(n).toLocaleString('es-PY')}`;
const toStr = (v: any) => (v === null || v === undefined ? '' : String(v).trim());
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 UTC (las columnas de fecha son @db.Date). */
function toFecha(v: string): Date | null {
  const m = toStr(v).match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
  return m ? new Date(`${m[3]}-${m[2]}-${m[1]}T00:00:00.000Z`) : null;
}
function normalizar(s: string) {
  return toStr(s).normalize('NFD').replace(/[̀-ͯ]/g, '').toUpperCase();
}
/** "3368335-2" → base + dv. `personas.ruc` es VarChar(8) y el dv va aparte. */
function partirRuc(ruc: string) {
  const l = toStr(ruc).replace(/\s+/g, '');
  if (!l) return { base: '', dv: null as string | null };
  const [b, d] = l.split('-');
  return { base: (b ?? '').slice(0, 8), dv: d ? d.slice(0, 1) : null };
}
/** "001-001-0000005" → est / punto / número. */
function partirFactura(f: string) {
  const p = toStr(f).split('-');
  if (p.length === 3) return { est: p[0], punto: p[1], num: p[2] };
  return { est: null as string | null, punto: null as string | null, num: toStr(f) || null };
}
/** ¿Es el comodín de ventas sin identificar del sistema viejo? */
function esComodin(nombre: string, ruc: string) {
  const base = partirRuc(ruc).base;
  return CONFIG.NOMBRE_COMODIN.test(normalizar(nombre).replace(/\s/g, '')) || CONFIG.RUC_COMODIN.includes(base) || !base;
}

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());
  const cab = lineas[0].split(CONFIG.SEP).map((c) => c.trim());
  return lineas.slice(1).map((l) => {
    const c = l.split(CONFIG.SEP);
    const f: Fila = {};
    cab.forEach((col, i) => (f[col] = toStr(c[i])));
    return f;
  });
}

const prisma = new PrismaClient();
type Anomalia = { motivo: string; detalle: string };

async function main() {
  raw('');
  raw('╔══════════════════════════════════════════════════════╗');
  raw(`║  VENTAS ADEUDADAS → CxC ${DRY ? '[ DRY RUN ]              ' : '[ REAL — ESCRIBE ]       '}║`);
  raw(`║  Empresa: ${EMPRESA_ID.padEnd(43)}║`);
  raw(`║  Archivo: ${path.basename(CSV_PATH).slice(0, 43).padEnd(43)}║`);
  raw(`║  IVA asumido: ${(IVA + '%').padEnd(39)}║`);
  raw(`║  Cliente comodín: ${(SALTEAR_COMODIN ? 'se saltea a crédito' : 'se migra igual').padEnd(35)}║`);
  raw('╚══════════════════════════════════════════════════════╝');
  raw('');

  const empresa = await prisma.empresas.findUnique({ where: { id: EMPRESA_ID }, select: { razon_social: true } });
  if (!empresa) { log('FATAL', `No existe la empresa ${EMPRESA_ID}`); process.exit(1); }
  info(`Empresa: ${empresa.razon_social}`);

  const moneda = await prisma.moneda.findFirst({ where: { codigo: CONFIG.MONEDA_CODIGO }, select: { id: true } });
  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 (!moneda || !condContado || !condCredito) {
    log('FATAL', 'Faltan catálogos globales (moneda PYG / condición 1-Contado / 2-Crédito).');
    process.exit(1);
  }
  const tiposOperacion = await prisma.tipo_operacion.findMany({ select: { id: true, descripcion: true } });
  const tipoOp = (d: string) => tiposOperacion.find((t) => t.descripcion === d)?.id;
  const OP_B2B = tipoOp('B2B'), OP_B2C = tipoOp('B2C'), OP_B2G = tipoOp('B2G');
  if (!OP_B2B || !OP_B2C || !OP_B2G) { log('FATAL', 'Faltan tipos de operación B2B/B2C/B2G.'); process.exit(1); }
  raw('');

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

  const anomalias: Anomalia[] = [];
  const erroresList: string[] = [];

  // ── Selección: sólo facturas con saldo ──────────────────────────
  const notasCredito = filas.filter((f) => toStr(f['TIPO']) !== 'FACTURA');
  const conSaldo = filas.filter((f) => toStr(f['TIPO']) === 'FACTURA' && toNum(f['PENDIENTE']) > 0);
  const saldadas = filas.filter((f) => toStr(f['TIPO']) === 'FACTURA' && toNum(f['PENDIENTE']) <= 0);
  info(`Facturas con saldo : ${conSaldo.length}   (saldadas: ${saldadas.length}, notas de crédito: ${notasCredito.length})`);
  for (const nc of notasCredito) {
    anomalias.push({
      motivo: 'NOTA DE CRÉDITO — no se migra (va en otro módulo y el archivo no dice contra qué factura aplica)',
      detalle: `${toStr(nc['FECHA'])} ${toStr(nc['CLIENTE'])} ${toStr(nc['NC'])} ${fmtGs(toNum(nc['TOTAL']))}`,
    });
  }

  // ── Filtrar el comodín a crédito ────────────────────────────────
  const aMigrar: Fila[] = [];
  for (const f of conSaldo) {
    const nombre = toStr(f['CLIENTE']);
    const ruc = toStr(f['RUC']);
    if (!esComodin(nombre, ruc)) { aMigrar.push(f); continue; }

    const esCredito = toNum(f['DIAS']) > 0 || toStr(f['VENCIMIENTO']) !== toStr(f['FECHA']);

    if (SALTEAR_COMODIN && esCredito) {
      anomalias.push({
        motivo: 'CLIENTE SIN IDENTIFICAR A CRÉDITO — descartada por --saltear-comodin',
        detalle: `${toStr(f['FECHA'])} ${nombre} (RUC ${ruc || '—'}) ${toStr(f['FACTURA'])} pendiente ${fmtGs(toNum(f['PENDIENTE']))}`,
      });
      continue;
    }

    if (esCredito) {
      // Se migra contra la misma ficha comodín que trae el archivo. Queda la
      // deuda registrada; a quién corresponde cada venta lo tiene que resolver
      // el cliente después, reasignando la factura al cliente real.
      anomalias.push({
        motivo: 'CLIENTE SIN IDENTIFICAR — se migra contra su misma ficha, hay que reasignarlas después',
        detalle: `${toStr(f['FECHA'])} ${nombre} (RUC ${ruc || '—'}) ${toStr(f['FACTURA'])} pendiente ${fmtGs(toNum(f['PENDIENTE']))}`,
      });
      aMigrar.push(f);
      continue;
    }

    // Al contado no hay deuda que reclamar, así que no hace falta arrastrar el
    // nombre del comodín: se agrupan bajo una ficha genérica.
    anomalias.push({
      motivo: `CLIENTE SIN IDENTIFICAR AL CONTADO — se migra como "${CONFIG.NOMBRE_SIN_IDENTIFICAR}"`,
      detalle: `${toStr(f['FECHA'])} ${toStr(f['FACTURA'])} ${fmtGs(toNum(f['TOTAL']))}`,
    });
    aMigrar.push({ ...f, CLIENTE: CONFIG.NOMBRE_SIN_IDENTIFICAR, RUC: '' });
  }
  info(`A migrar tras filtrar el comodín : ${aMigrar.length}`);
  raw('');

  // ── Clientes ────────────────────────────────────────────────────
  // Se agrupan por RUC; sin RUC, por nombre normalizado.
  const porClave = new Map<string, { nombre: string; ruc: string }>();
  for (const f of aMigrar) {
    const nombre = toStr(f['CLIENTE']);
    const ruc = toStr(f['RUC']);
    const { base } = partirRuc(ruc);
    porClave.set(base ? `RUC:${base}` : `NOMBRE:${normalizar(nombre)}`, { nombre, ruc });
  }
  info(`Clientes a asegurar: ${porClave.size}`);

  const clienteIdPorClave = new Map<string, string>();
  for (const [clave, c] of porClave) {
    const { base, dv } = partirRuc(c.ruc);
    try {
      const existente = await prisma.clientes.findFirst({
        where: {
          personas: {
            empresa_id: EMPRESA_ID,
            ...(base ? { ruc: base } : { razon_social: c.nombre }),
          },
        },
        select: { id: true, personas: { select: { razon_social: true } } },
      });
      if (existente) {
        clienteIdPorClave.set(clave, existente.id);
        skip(`Cliente ya existente | ${existente.personas?.razon_social ?? c.nombre}`);
        continue;
      }
      // Gobierno → B2G; RUC de 8 dígitos (persona jurídica) → B2B; resto B2C.
      const esGobierno = /MUNICIPALIDAD|GOBERNACION|GOBIERNO|MINISTERIO|DEPARTAMENTAL/.test(normalizar(c.nombre));
      const tipoOperacionId = esGobierno ? OP_B2G : base.length === 8 ? OP_B2B : OP_B2C;

      if (DRY) {
        clienteIdPorClave.set(clave, `DRY-${clave}`);
        info(`[DRY] Cliente a crear | ${c.nombre} | RUC ${base || '—'}${dv ? `-${dv}` : ''} | ${esGobierno ? 'B2G' : base.length === 8 ? 'B2B' : 'B2C'}`);
        continue;
      }
      const persona = await prisma.personas.create({
        data: { empresa_id: EMPRESA_ID, razon_social: c.nombre, ruc: base || null, dv, active: true, deleted: false },
        select: { id: true },
      });
      const cliente = await prisma.clientes.create({
        data: { persona_id: persona.id, tipo_operacion_id: tipoOperacionId, active: true, deleted: false },
        select: { id: true },
      });
      clienteIdPorClave.set(clave, cliente.id);
      ok(`Cliente creado | ${c.nombre} | RUC ${base || '—'}${dv ? `-${dv}` : ''}`);
    } catch (e: any) {
      const msg = `Cliente "${c.nombre}" | ${e.message}`;
      erroresList.push(msg);
      err(msg);
    }
  }
  raw('');

  // ── Facturas ────────────────────────────────────────────────────
  let creadas = 0, existentes = 0;
  let totalFacturado = 0, totalPendiente = 0, parciales = 0;

  for (const f of aMigrar) {
    const nombre = toStr(f['CLIENTE']);
    const facturaTexto = toStr(f['FACTURA']);
    const label = `${toStr(f['FECHA'])} ${nombre.slice(0, 26)} ${facturaTexto}`;

    const fechaEmision = toFecha(f['FECHA']);
    if (!fechaEmision) {
      anomalias.push({ motivo: 'FECHA ILEGIBLE — no se migra', detalle: label });
      continue;
    }
    const fechaVencimiento = toFecha(f['VENCIMIENTO']) ?? fechaEmision;
    const { est, punto, num } = partirFactura(facturaTexto);
    if (!num) { anomalias.push({ motivo: 'SIN NÚMERO DE FACTURA — no se migra', detalle: label }); continue; }

    const { base } = partirRuc(toStr(f['RUC']));
    const clave = base ? `RUC:${base}` : `NOMBRE:${normalizar(nombre)}`;
    const clienteId = clienteIdPorClave.get(clave);
    if (!clienteId) { anomalias.push({ motivo: 'SIN CLIENTE RESUELTO — no se migra', detalle: label }); continue; }

    const total = Math.round(toNum(f['TOTAL']));
    const pagado = Math.round(toNum(f['PAGADO']));
    const pendiente = Math.round(toNum(f['PENDIENTE']));
    if (pagado + pendiente !== total) {
      anomalias.push({
        motivo: 'PAGADO + PENDIENTE ≠ TOTAL — se respeta el TOTAL',
        detalle: `${label} | ${fmtGs(pagado)} + ${fmtGs(pendiente)} ≠ ${fmtGs(total)}`,
      });
    }
    if (pagado > 0) parciales++;

    // El archivo sólo trae el total con IVA incluido.
    const base10 = IVA > 0 ? Math.round(total / (1 + IVA / 100)) : 0;
    const iva10 = IVA > 0 ? total - base10 : 0;
    const esCredito = toNum(f['DIAS']) > 0 || toStr(f['VENCIMIENTO']) !== toStr(f['FECHA']);
    const estadoCuota = pagado > 0 ? 'parcial' : 'pendiente';

    if (DRY) {
      info(`[DRY] ${label} | ${esCredito ? 'CRÉDITO' : 'CONTADO'} | total ${fmtGs(total)} | pagado ${fmtGs(pagado)} | saldo ${fmtGs(pendiente)}`);
      creadas++; totalFacturado += total; totalPendiente += pendiente;
      continue;
    }

    try {
      const ya = await prisma.factura_cab.findFirst({
        where: { empresa_id: EMPRESA_ID, dest: est ?? '', dpunexp: punto ?? '', dnumdoc: num },
        select: { id: true },
      });
      if (ya) { existentes++; skip(`Ya migrada | ${label}`); continue; }

      await prisma.$transaction(async (tx) => {
        const cab = await tx.factura_cab.create({
          data: {
            empresa_id: EMPRESA_ID,
            cliente_id: clienteId,
            moneda_id: moneda.id,
            condicion_operacion_id: esCredito ? condCredito.id : condContado.id,
            dest: est ?? '',
            dpunexp: punto ?? '',
            dnumdoc: num,
            dfeemide: fechaEmision,
            dvencpag: fechaVencimiento,
            estado: 'Pendiente',
            total_factura: total,
            saldo_pendiente: pendiente,
            dinfadic: `Migrada desde ${path.basename(CSV_PATH)} — saldo pendiente al momento de la migración.`,
          },
          select: { id: true },
        });

        // Desglose de IVA para el Libro IVA. dsub10 es el importe BRUTO
        // (base + IVA), como lo trata facturas.service.ts.
        await tx.factura_subtotales.create({
          data: {
            factura_cab_id: cab.id,
            dsubexe: IVA === 0 ? total : 0,
            dsubexo: 0,
            dsub5: IVA === 5 ? total : 0,
            dsub10: IVA === 10 ? total : 0,
            dbasegrav5: IVA === 5 ? base10 : 0,
            dbasegrav10: IVA === 10 ? base10 : 0,
            diva5: IVA === 5 ? iva10 : 0,
            diva10: IVA === 10 ? iva10 : 0,
            dtotiva: iva10,
            dtotope: total,
            dtotgralope: total,
          },
        });

        const cuenta = await tx.cuentas_cobrar.create({
          data: {
            empresa_id: EMPRESA_ID,
            cliente_id: clienteId,
            factura_venta_id: cab.id,
            fecha_emision: fechaEmision,
            fecha_vencimiento: fechaVencimiento,
            moneda_iso: CONFIG.MONEDA_CODIGO,
            monto_total: total,
            saldo_pendiente: pendiente,
            estado: 'pendiente',
          },
          select: { id: true },
        });

        // Una sola cuota: el archivo no trae plan de cuotas. Lo ya pagado se
        // refleja en el saldo, no como un recibo (esos cobros ya ocurrieron).
        await tx.factura_cuotas.create({
          data: {
            factura_cab_id: cab.id,
            cuenta_id: cuenta.id,
            cmonecuo: CONFIG.MONEDA_CODIGO,
            dmoncuota: total,
            dvenccuo: fechaVencimiento,
            nro_cuota: 1,
            saldo_pendiente: pendiente,
            estado: estadoCuota,
          },
        });
      });

      creadas++; totalFacturado += total; totalPendiente += pendiente;
      ok(`${label} | total ${fmtGs(total)} | saldo ${fmtGs(pendiente)}`);
    } catch (e: any) {
      const msg = `${label} | ${e.message}`;
      erroresList.push(msg);
      err(msg);
    }
  }

  raw('');
  raw('╔══════════════════════════════════════════════════════╗');
  raw('║                    RESUMEN FINAL                     ║');
  raw('╠══════════════════════════════════════════════════════╣');
  raw(`║  Filas del CSV               : ${String(filas.length).padStart(8)}             ║`);
  raw(`║  Saldadas (no se migran)     : ${String(saldadas.length).padStart(8)}             ║`);
  raw(`║  Notas de crédito (no migran): ${String(notasCredito.length).padStart(8)}             ║`);
  raw(`║  Con saldo                   : ${String(conSaldo.length).padStart(8)}             ║`);
  raw(`║  Descartadas por comodín     : ${String(conSaldo.length - aMigrar.length).padStart(8)}             ║`);
  raw('╠══════════════════════════════════════════════════════╣');
  raw(`║  Clientes asegurados         : ${String(porClave.size).padStart(8)}             ║`);
  raw(`║  Facturas migradas           : ${String(creadas).padStart(8)}             ║`);
  raw(`║  Facturas ya existentes      : ${String(existentes).padStart(8)}             ║`);
  raw(`║  Con pago parcial            : ${String(parciales).padStart(8)}             ║`);
  raw(`║  Total facturado             : ${fmtGs(totalFacturado).padStart(20)} ║`);
  raw(`║  Queda a cobrar              : ${fmtGs(totalPendiente).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)`);
    }
  }
  if (erroresList.length > 0) {
    raw('');
    raw('── ERRORES ──────────────────────────────────────────');
    erroresList.forEach((e, i) => raw(`  [${String(i + 1).padStart(3)}] ${e}`));
  }

  raw('');
  raw(DRY ? '⚠  DRY RUN — No se escribió nada. Ejecutar sin --dry para aplicar.'
          : erroresList.length === 0 ? '✓  Migración completada sin errores.'
          : `⚠  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(); });
