/**
 * ╔══════════════════════════════════════════════════════════════╗
 * ║   ACTUALIZACIÓN: Facturas a crédito del taller (saldos)         ║
 * ╠══════════════════════════════════════════════════════════════╣
 * ║  Lee la planilla "factura credito 8center*.xlsx" (Hoja1):       ║
 * ║    CONDICIÓN | FECHA DE VENTA | VENCIMIENTO | FALTAN (d) |      ║
 * ║    COMPROBANTE | CLIENTE | RUC | FACTURADO | ADEUDADO           ║
 * ║                                                                ║
 * ║  Por cada comprobante:                                         ║
 * ║   · YA EXISTE  → lleva el saldo al ADEUDADO de la planilla en   ║
 * ║                  factura_cab, factura_cuotas y cuentas_cobrar.  ║
 * ║                  Si la factura tiene cobros registrados en el   ║
 * ║                  sistema nuevo NO se toca (se reporta): pisar   ║
 * ║                  el saldo descuadraría esos recibos.            ║
 * ║   · NO EXISTE y ADEUDADO > 0 → se crea igual que               ║
 * ║                  migrate-ventas-adeudadas-taller.ts (cabecera,  ║
 * ║                  subtotales IVA 10%, cuenta a cobrar, 1 cuota). ║
 * ║   · NO EXISTE y ADEUDADO = 0 → no se migra: no hay nada que     ║
 * ║                  cobrar (mismo criterio que el script original).║
 * ║                                                                ║
 * ║  Lo ya pagado se refleja en el saldo, NO como recibo: esos      ║
 * ║  cobros ocurrieron en el sistema anterior.                     ║
 * ║                                                                ║
 * ║  Idempotente: con los saldos ya iguales no hace nada.          ║
 * ╠══════════════════════════════════════════════════════════════╣
 * ║  Uso:                                                          ║
 * ║    npx ts-node --transpile-only -r tsconfig-paths/register \   ║
 * ║      --project tsconfig.scripts.json \                         ║
 * ║      scripts/migrate-access/actualizar-facturas-credito-taller.ts \
 * ║      --empresa=<uuid> --dry                                    ║
 * ║                                                                ║
 * ║  Flags: --archivo=  --hoja=Hoja1  --iva=10                     ║
 * ╚══════════════════════════════════════════════════════════════╝
 */

import 'dotenv/config';
import * as fs from 'fs';
import * as path from 'path';
import * as XLSX from 'xlsx';
import { PrismaClient } from '@prisma/client';

const CONFIG = {
  DEFAULT_XLSX_PATH: path.resolve(
    __dirname,
    '../../scripts/data_script/cliente-agogo/nuevo/factura  credito 8center120926.xlsx',
  ),
  HOJA: 'Hoja1',
  MONEDA_CODIGO: 'PYG',
  COND_CONTADO: 1,
  COND_CREDITO: 2,
  IVA_DEFAULT: 10,
};

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');
const XLSX_PATH = argValue('archivo') ? path.resolve(process.cwd(), argValue('archivo')!) : CONFIG.DEFAULT_XLSX_PATH;
const HOJA = argValue('hoja') ?? CONFIG.HOJA;
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-facturas-credito-taller-${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 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());

/** Celdas numéricas llegan como number; las de texto usan punto de miles. */
function toNum(v: any): number {
  if (typeof v === 'number') return Number.isFinite(v) ? v : 0;
  const s = toStr(v).replace(/\./g, '').replace(',', '.');
  const n = parseFloat(s);
  return Number.isFinite(n) ? n : 0;
}

/** Serial de Excel (46188) o texto dd/mm/aaaa → Date UTC (columnas @db.Date). */
function toFecha(v: any): Date | null {
  if (typeof v === 'number' && v > 0) return new Date(Date.UTC(1899, 11, 30) + Math.round(v) * 86400000);
  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;
}
const fmtFecha = (d: Date) => d.toISOString().slice(0, 10);

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, '').replace(/\./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 null;
}

/** Encabezados con espacios alrededor (" CLIENTE ") → clave limpia. */
function leerFilas(): Record<string, any>[] {
  const wb = XLSX.readFile(XLSX_PATH);
  if (!wb.SheetNames.includes(HOJA)) {
    log('FATAL', `El archivo no tiene la hoja "${HOJA}". Hojas: ${wb.SheetNames.join(', ')}`);
    process.exit(1);
  }
  const filas: any[][] = XLSX.utils.sheet_to_json(wb.Sheets[HOJA], { header: 1, defval: null });
  const cab = (filas[0] ?? []).map((c) => normalizar(toStr(c)));
  return filas.slice(1)
    .filter((r) => r.some((c) => c !== null && toStr(c) !== ''))
    .map((r) => Object.fromEntries(cab.map((c, i) => [c, r[i]])));
}

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

async function main() {
  raw('');
  raw('╔══════════════════════════════════════════════════════╗');
  raw(`║  FACTURAS CRÉDITO TALLER ${DRY ? '[ DRY RUN ]             ' : '[ REAL — ESCRIBE ]      '}║`);
  raw(`║  Empresa: ${EMPRESA_ID.padEnd(43)}║`);
  raw(`║  Archivo: ${path.basename(XLSX_PATH).slice(0, 43).padEnd(43)}║`);
  raw(`║  IVA asumido (nuevas): ${(IVA + '%').padEnd(30)}║`);
  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); }

  if (!fs.existsSync(XLSX_PATH)) { log('FATAL', `Archivo no encontrado: ${XLSX_PATH}`); process.exit(1); }
  const filas = leerFilas();
  const faltantes = ['CONDICION', 'FECHA DE VENTA', 'VENCIMIENTO', 'COMPROBANTE', 'CLIENTE', 'RUC', 'FACTURADO', 'ADEUDADO']
    .filter((c) => !(c in (filas[0] ?? {})));
  if (faltantes.length > 0) {
    log('FATAL', `Faltan columnas en la hoja "${HOJA}": ${faltantes.join(', ')}. Detectadas: ${Object.keys(filas[0] ?? {}).join(', ')}`);
    process.exit(1);
  }
  info(`Filas de la planilla: ${filas.length}`);
  raw('');

  const anomalias: Anomalia[] = [];
  const erroresList: string[] = [];
  let sinCambios = 0, actualizadas = 0, saldadas = 0, conCobros = 0, creadas = 0, pagadasNoMigradas = 0;
  let saldoAntes = 0, saldoDespues = 0;
  const vistos = new Set<string>();
  const clienteIdPorClave = new Map<string, string>();

  for (const f of filas) {
    const comprobante = toStr(f['COMPROBANTE']);
    const nombre = toStr(f['CLIENTE']);
    const rucTexto = toStr(f['RUC']);
    const facturado = Math.round(toNum(f['FACTURADO']));
    const adeudado = Math.round(toNum(f['ADEUDADO']));
    const label = `${comprobante} ${nombre.slice(0, 28)}`;

    const partes = partirFactura(comprobante);
    if (!partes) { anomalias.push({ motivo: 'COMPROBANTE ILEGIBLE — no se procesa', detalle: `"${comprobante}" ${nombre}` }); continue; }
    if (vistos.has(comprobante)) { anomalias.push({ motivo: 'COMPROBANTE REPETIDO EN LA PLANILLA — se usa el primero', detalle: label }); continue; }
    vistos.add(comprobante);
    if (adeudado < 0 || adeudado > facturado) {
      anomalias.push({ motivo: 'ADEUDADO FUERA DE RANGO — no se procesa', detalle: `${label} | facturado ${fmtGs(facturado)} adeudado ${fmtGs(adeudado)}` });
      continue;
    }
    const condicion = normalizar(toStr(f['CONDICION']));
    if (condicion && condicion !== 'CREDITO') {
      anomalias.push({ motivo: 'CONDICIÓN DISTINTA DE CRÉDITO — se procesa igual', detalle: `${label} | ${condicion}` });
    }

    const existente = await prisma.factura_cab.findFirst({
      where: { empresa_id: EMPRESA_ID, dest: partes.est, dpunexp: partes.punto, dnumdoc: partes.num },
      select: {
        id: true, total_factura: true, saldo_pendiente: true,
        factura_cuotas: { select: { id: true, dmoncuota: true, saldo_pendiente: true, cuenta_id: true } },
        cuentas_cobrar: { select: { id: true, saldo_pendiente: true } },
        _count: { select: { recibo_cobro_facturas: true } },
      },
    });

    // ── Factura existente: actualizar saldo ──────────────────────
    if (existente) {
      const total = Number(existente.total_factura ?? 0);
      if (Math.round(total) !== facturado) {
        anomalias.push({ motivo: 'TOTAL DISTINTO AL DEL SISTEMA — se actualiza el saldo igual, verificar', detalle: `${label} | sistema ${fmtGs(total)} planilla ${fmtGs(facturado)}` });
      }
      const saldoCuotas = existente.factura_cuotas.reduce((s, c) => s + Number(c.saldo_pendiente ?? 0), 0);
      const saldoCab = Number(existente.saldo_pendiente ?? 0);
      const saldoCxc = existente.cuentas_cobrar.reduce((s, c) => s + Number(c.saldo_pendiente ?? 0), 0);
      if (saldoCab === adeudado && saldoCuotas === adeudado && saldoCxc === adeudado) { sinCambios++; continue; }

      const cuotasConCobro = existente.factura_cuotas.length > 0
        ? await prisma.recibo_cobro_detalle.count({ where: { factura_cuota_id: { in: existente.factura_cuotas.map((c) => c.id) } } })
        : 0;
      if (existente._count.recibo_cobro_facturas > 0 || cuotasConCobro > 0) {
        conCobros++;
        anomalias.push({
          motivo: 'TIENE COBROS EN EL SISTEMA NUEVO — no se tocó, conciliar a mano',
          detalle: `${label} | saldo sistema ${fmtGs(saldoCab)} planilla ${fmtGs(adeudado)}`,
        });
        continue;
      }
      if (existente.factura_cuotas.length !== 1) {
        anomalias.push({
          motivo: 'NO TIENE EXACTAMENTE UNA CUOTA — no se tocó',
          detalle: `${label} | cuotas: ${existente.factura_cuotas.length}`,
        });
        continue;
      }
      if (adeudado > saldoCab) {
        anomalias.push({ motivo: 'EL SALDO SUBE RESPECTO AL SISTEMA — se aplica, verificar', detalle: `${label} | ${fmtGs(saldoCab)} → ${fmtGs(adeudado)}` });
      }

      const etiqueta = `${label} | saldo ${fmtGs(saldoCab)} → ${fmtGs(adeudado)}${adeudado === 0 ? ' (SALDADA)' : ''}`;
      saldoAntes += saldoCab; saldoDespues += adeudado;
      if (adeudado === 0) saldadas++;
      if (DRY) { info(`[DRY] Actualizar | ${etiqueta}`); actualizadas++; continue; }

      const cuota = existente.factura_cuotas[0];
      const montoCuota = Number(cuota.dmoncuota ?? total);
      // Mismos estados que escribe cobros.service al aplicar un recibo.
      const estadoCuota = adeudado <= 0 ? 'pagado' : adeudado < montoCuota ? 'parcial' : 'pendiente';
      const estadoCuenta = adeudado <= 0 ? 'pagado' : 'pendiente';
      try {
        await prisma.$transaction(async (tx) => {
          await tx.factura_cab.update({ where: { id: existente.id }, data: { saldo_pendiente: adeudado } });
          await tx.factura_cuotas.update({
            where: { id: cuota.id },
            data: { saldo_pendiente: adeudado, estado: estadoCuota, updated_at: new Date() },
          });
          for (const c of existente.cuentas_cobrar) {
            await tx.cuentas_cobrar.update({
              where: { id: c.id },
              data: { saldo_pendiente: adeudado, estado: estadoCuenta, updated_at: new Date() },
            });
          }
        });
        actualizadas++;
        ok(`Actualizada | ${etiqueta}`);
      } catch (e: any) {
        erroresList.push(`${etiqueta} | ${e.message}`);
        err(`${etiqueta} | ${e.message}`);
      }
      continue;
    }

    // ── Factura nueva ────────────────────────────────────────────
    if (adeudado <= 0) { pagadasNoMigradas++; continue; }

    const fechaEmision = toFecha(f['FECHA DE VENTA']);
    if (!fechaEmision) { anomalias.push({ motivo: 'FECHA ILEGIBLE — no se migra', detalle: label }); continue; }
    const fechaVencimiento = toFecha(f['VENCIMIENTO']) ?? fechaEmision;
    const esCredito = fechaVencimiento.getTime() !== fechaEmision.getTime() || condicion === 'CREDITO';

    const { base, dv } = partirRuc(rucTexto);
    if (!base) { anomalias.push({ motivo: 'SIN RUC — no se migra (cargar a mano con el cliente correcto)', detalle: label }); continue; }
    const clave = `RUC:${base}`;
    if (/^A{4,}$/.test(normalizar(nombre).replace(/\s/g, ''))) {
      anomalias.push({
        motivo: 'CLIENTE SIN IDENTIFICAR (AAAAAAAA) — se migra contra su ficha, reasignar después',
        detalle: `${label} | adeudado ${fmtGs(adeudado)}`,
      });
    }

    let clienteId = clienteIdPorClave.get(clave);
    if (!clienteId) {
      const cliente = await prisma.clientes.findFirst({
        where: { personas: { empresa_id: EMPRESA_ID, ruc: base } },
        select: { id: true },
      });
      if (cliente) clienteId = cliente.id;
      else {
        const esGobierno = /MUNICIPALIDAD|GOBERNACION|GOBIERNO|MINISTERIO|DEPARTAMENTAL/.test(normalizar(nombre));
        const tipoOperacionId = esGobierno ? OP_B2G : base.length === 8 ? OP_B2B : OP_B2C;
        if (DRY) {
          clienteId = `DRY-${clave}`;
          info(`[DRY] Cliente a crear | ${nombre} | RUC ${base}${dv ? `-${dv}` : ''}`);
        } else {
          try {
            const persona = await prisma.personas.create({
              data: { empresa_id: EMPRESA_ID, razon_social: nombre, ruc: base, dv, active: true, deleted: false },
              select: { id: true },
            });
            const creado = await prisma.clientes.create({
              data: { persona_id: persona.id, tipo_operacion_id: tipoOperacionId, active: true, deleted: false },
              select: { id: true },
            });
            clienteId = creado.id;
            ok(`Cliente creado | ${nombre} | RUC ${base}${dv ? `-${dv}` : ''}`);
          } catch (e: any) {
            erroresList.push(`Cliente "${nombre}" | ${e.message}`);
            err(`Cliente "${nombre}" | ${e.message}`);
            continue;
          }
        }
      }
      clienteIdPorClave.set(clave, clienteId);
    }

    const pagado = facturado - adeudado;
    const base10 = IVA > 0 ? Math.round(facturado / (1 + IVA / 100)) : 0;
    const iva10 = IVA > 0 ? facturado - base10 : 0;
    const etiqueta = `${label} | ${fmtFecha(fechaEmision)} vence ${fmtFecha(fechaVencimiento)} | total ${fmtGs(facturado)} | saldo ${fmtGs(adeudado)}`;
    if (DRY) { info(`[DRY] Crear | ${etiqueta}`); creadas++; saldoDespues += adeudado; continue; }

    try {
      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: partes.est,
            dpunexp: partes.punto,
            dnumdoc: partes.num,
            dfeemide: fechaEmision,
            dvencpag: fechaVencimiento,
            estado: 'Pendiente',
            total_factura: facturado,
            saldo_pendiente: adeudado,
            dinfadic: `Migrada desde ${path.basename(XLSX_PATH)} — saldo pendiente al momento de la migración.`,
          },
          select: { id: true },
        });
        // 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 ? facturado : 0,
            dsubexo: 0,
            dsub5: IVA === 5 ? facturado : 0,
            dsub10: IVA === 10 ? facturado : 0,
            dbasegrav5: IVA === 5 ? base10 : 0,
            dbasegrav10: IVA === 10 ? base10 : 0,
            diva5: IVA === 5 ? iva10 : 0,
            diva10: IVA === 10 ? iva10 : 0,
            dtotiva: iva10,
            dtotope: facturado,
            dtotgralope: facturado,
          },
        });
        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: facturado,
            saldo_pendiente: adeudado,
            estado: 'pendiente',
          },
          select: { id: true },
        });
        await tx.factura_cuotas.create({
          data: {
            factura_cab_id: cab.id,
            cuenta_id: cuenta.id,
            cmonecuo: CONFIG.MONEDA_CODIGO,
            dmoncuota: facturado,
            dvenccuo: fechaVencimiento,
            nro_cuota: 1,
            saldo_pendiente: adeudado,
            estado: pagado > 0 ? 'parcial' : 'pendiente',
          },
        });
      });
      creadas++; saldoDespues += adeudado;
      ok(`Creada | ${etiqueta}`);
    } catch (e: any) {
      erroresList.push(`${etiqueta} | ${e.message}`);
      err(`${etiqueta} | ${e.message}`);
    }
  }

  // Facturas migradas antes que ya no figuran en la planilla: probablemente se
  // saldaron, pero sin evidencia no se tocan.
  const migradas = await prisma.factura_cab.findMany({
    where: { empresa_id: EMPRESA_ID, dinfadic: { startsWith: 'Migrada desde' }, saldo_pendiente: { gt: 0 } },
    select: { dest: true, dpunexp: true, dnumdoc: true, saldo_pendiente: true, clientes: { select: { personas: { select: { razon_social: true } } } } },
  });
  for (const m of migradas) {
    const comp = `${m.dest}-${m.dpunexp}-${m.dnumdoc}`;
    if (vistos.has(comp)) continue;
    anomalias.push({
      motivo: 'FACTURA CON SALDO EN EL SISTEMA QUE NO ESTÁ EN LA PLANILLA — no se tocó',
      detalle: `${comp} ${m.clientes?.personas?.razon_social ?? ''} | saldo ${fmtGs(Number(m.saldo_pendiente))}`,
    });
  }

  raw('');
  raw('╔══════════════════════════════════════════════════════╗');
  raw('║                    RESUMEN FINAL                     ║');
  raw('╠══════════════════════════════════════════════════════╣');
  raw(`║  Filas de la planilla        : ${String(filas.length).padStart(8)}             ║`);
  raw(`║  Saldo ya correcto           : ${String(sinCambios).padStart(8)}             ║`);
  raw(`║  Saldo actualizado           : ${String(actualizadas).padStart(8)}             ║`);
  raw(`║    · quedaron saldadas       : ${String(saldadas).padStart(8)}             ║`);
  raw(`║  Con cobros (no tocadas)     : ${String(conCobros).padStart(8)}             ║`);
  raw(`║  Facturas nuevas creadas     : ${String(creadas).padStart(8)}             ║`);
  raw(`║  Pagadas sin migrar (saldo 0): ${String(pagadasNoMigradas).padStart(8)}             ║`);
  raw('╠══════════════════════════════════════════════════════╣');
  raw(`║  Saldo de las actualizadas   : ${fmtGs(saldoAntes).padStart(20)} ║`);
  raw(`║    queda en (+ nuevas)       : ${fmtGs(saldoDespues).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.forEach((d) => raw(`    · ${d}`));
    }
  }
  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 ? '✓  Actualizació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(); });
