/**
 * ╔══════════════════════════════════════════════════════════════╗
 * ║   MIGRACIÓN: Productos + stock + listas de precios (taller)    ║
 * ╠══════════════════════════════════════════════════════════════╣
 * ║  Lee REPORTE_INVENTARIO.csv y por cada producto:               ║
 * ║   · crea/actualiza el producto (clave: CODIGO)                 ║
 * ║   · carga el stock inicial en el depósito DEPTALLER            ║
 * ║   · crea el precio en cada lista (MINORISTA, MAYORISTA 1/2/3)  ║
 * ║     SOLO cuando el valor del CSV es > 0                        ║
 * ║                                                                ║
 * ║  El precio MINORISTA además se guarda en `productos.precio`,   ║
 * ║  que es el que usa el POS cuando el cliente no tiene lista     ║
 * ║  asignada (el caso normal hasta que el cliente las asigne).    ║
 * ║                                                                ║
 * ║  CÓDIGOS REPETIDOS: la empresa ya tiene otro catálogo cargado  ║
 * ║  y 12 códigos del taller coinciden con productos que son otra  ║
 * ║  cosa (el 282 es una gorra KEPIS y en el taller es un filtro   ║
 * ║  de aire). Esos se migran con el código prefijado (--prefijo,  ║
 * ║  por defecto "TALLER-") para no pisar el producto existente.   ║
 * ║  Los demás conservan su código original.                       ║
 * ║                                                                ║
 * ║  FILAS CORRIDAS: 8 filas traen un ';' dentro del NOMBRE y eso  ║
 * ║  desplaza todas las columnas un lugar. Sin reparar entran con  ║
 * ║  el costo del IVA (Gs. 10). Se detectan usando la moneda como  ║
 * ║  ancla y se valida el arreglo contra "COSTO x STOCK".          ║
 * ║                                                                ║
 * ║  Idempotente: el producto se busca por (empresa, cod_producto),║
 * ║  el precio de lista por (lista, producto). El stock SOLO se    ║
 * ║  carga cuando el producto se crea en esta corrida.             ║
 * ╠══════════════════════════════════════════════════════════════╣
 * ║  Uso:                                                          ║
 * ║    npx ts-node --transpile-only -r tsconfig-paths/register \   ║
 * ║      --project tsconfig.scripts.json \                         ║
 * ║      scripts/migrate-access/migrate-productos-taller.ts \      ║
 * ║      --empresa=<uuid> --dry                                    ║
 * ║                                                                ║
 * ║  Flags: --archivo=  --deposito=DEPTALLER  --prefijo=TALLER-    ║
 * ╚══════════════════════════════════════════════════════════════╝
 */

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/REPORTE_INVENTARIO.csv',
  ),
  ENCODING: 'latin1' as BufferEncoding, // export de Windows, no UTF-8
  SEP: ';',
  DEPOSITO_CODIGO: 'DEPTALLER',
  PREFIJO_COLISION: 'TALLER-',

  AFECTACION_GRAVADO: '7b634413-a746-4152-9941-4d586ed75825',
  AFECTACION_EXENTO: 'd8e2cee1-7e25-44b2-8d06-b2bc8748b6a3',
  UNIDAD_MEDIDA_ID: '4f6a1318-1a42-454d-874e-a72f6cda04df', // "Unidad"
  PROPORCION_IVA: 100,
  TIPO_PRODUCTO: 'producto' as const,

  // Índice de la columna MDA en una fila sana. Es el ancla para detectar las
  // filas cuyo NOMBRE se comió uno o más ';'.
  IDX_MDA: 21,
};

/** Las cuatro listas del CSV. `prioridad` asc = gana la más baja. */
const LISTAS = [
  { columna: 'MINORISTA', codigo: 'MINORISTA', nombre: 'Minorista', prioridad: 10 },
  { columna: 'PRECIO MAYORISTA 1', codigo: 'MAYORISTA1', nombre: 'Mayorista 1', prioridad: 20 },
  { columna: 'PRECIO MAYORISTA 2', codigo: 'MAYORISTA2', nombre: 'Mayorista 2', prioridad: 30 },
  { columna: 'PRECIO MAYORISTA 3', codigo: 'MAYORISTA3', nombre: 'Mayorista 3', prioridad: 40 },
] as const;

// ══════════════════════════════════════════════════════════════════
//  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 CSV_PATH = argValue('archivo')
  ? path.resolve(process.cwd(), argValue('archivo')!)
  : CONFIG.DEFAULT_CSV_PATH;
const DEPOSITO_CODIGO = argValue('deposito') ?? CONFIG.DEPOSITO_CODIGO;
const PREFIJO = argValue('prefijo') ?? CONFIG.PREFIJO_COLISION;

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-productos-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 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);

// ══════════════════════════════════════════════════════════════════
//  PARSEO DEL CSV
// ══════════════════════════════════════════════════════════════════
/** Split respetando comillas dobles al estilo CSV (`""` es una comilla). */
function partirLinea(linea: string, sep: string): string[] {
  const out: string[] = [];
  let campo = '';
  let enComillas = false;
  for (let i = 0; i < linea.length; i++) {
    const c = linea[i];
    if (enComillas) {
      if (c === '"') {
        if (linea[i + 1] === '"') { campo += '"'; i++; }
        else enComillas = false;
      } else campo += c;
    } else if (c === '"') enComillas = true;
    else if (c === sep) { out.push(campo); campo = ''; }
    else campo += c;
  }
  out.push(campo);
  return out.map((c) => c.trim());
}

/**
 * Repara las filas cuyo NOMBRE se comió uno o más separadores.
 * La moneda ("Gs"/"Usd") tiene que caer en CONFIG.IDX_MDA; si aparece más a la
 * derecha, esa diferencia es la cantidad de campos de más que metió el nombre.
 * Se vuelven a unir y el resto se corre a su lugar.
 */
function repararCorrimiento(campos: string[]): { campos: string[]; corridos: number } {
  const pos = campos.findIndex((v) => v === 'Gs' || v === 'Usd');
  if (pos < 0 || pos <= CONFIG.IDX_MDA) return { campos, corridos: 0 };
  const k = pos - CONFIG.IDX_MDA;
  const nombre = campos.slice(4, 4 + k + 1).join(' ').replace(/\s+/g, ' ').trim();
  return { campos: [...campos.slice(0, 4), nombre, ...campos.slice(4 + k + 1)], corridos: k };
}

// Índices de una fila sana.
const COL = {
  CODIGO: 1, COD_UNICO: 2, REFERENCIA: 3, NOMBRE: 4, UND: 5, CLASE: 6, FAMILIAS: 7,
  IVA: 11, COSTO: 12, MINORISTA: 13, MAY1: 14, MAY2: 15, MAY3: 16,
  STK_MINIMO: 17, STOCK: 18, COSTO_X_STOCK: 19, MDA: 21,
} as const;
const COL_POR_NOMBRE: Record<string, number> = {
  MINORISTA: COL.MINORISTA,
  'PRECIO MAYORISTA 1': COL.MAY1,
  'PRECIO MAYORISTA 2': COL.MAY2,
  'PRECIO MAYORISTA 3': COL.MAY3,
};

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

/** El CSV usa el punto como separador de miles y la coma como decimal. */
function toNum(v: string | undefined): number {
  const s = (v ?? '').trim().replace(/\./g, '').replace(',', '.');
  if (!s) return 0;
  const n = parseFloat(s);
  return Number.isFinite(n) ? n : 0;
}
/** Guaraní no lleva decimales. */
const aGs = (v: string | undefined) => Math.max(0, Math.round(toNum(v)));
/** Los códigos vienen con apóstrofo de Excel: '103 → 103 */
const limpiarCodigo = (v: string | undefined) => (v ?? '').replace(/^'+/, '').trim();

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

async function main() {
  raw('');
  raw('╔══════════════════════════════════════════════════════╗');
  raw(`║  PRODUCTOS + STOCK + LISTAS ${DRY ? '[ DRY RUN ]          ' : '[ REAL — ESCRIBE ]   '}║`);
  raw(`║  Empresa:  ${EMPRESA_ID.padEnd(42)}║`);
  raw(`║  Archivo:  ${path.basename(CSV_PATH).slice(0, 42).padEnd(42)}║`);
  raw(`║  Depósito: ${DEPOSITO_CODIGO.padEnd(42)}║`);
  raw(`║  Prefijo colisiones: ${PREFIJO.padEnd(32)}║`);
  raw('╚══════════════════════════════════════════════════════╝');
  raw('');

  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}`);

  // El depósito se busca por código dentro de la empresa: es lo que pidió el
  // pedido ("el depósito con código DEPTALLER"), y evita depender de cuál esté
  // marcado como principal.
  const deposito = await prisma.depositos.findFirst({
    where: { codigo: DEPOSITO_CODIGO, empresa_id: EMPRESA_ID },
    select: {
      id: true,
      descripcion: true,
      sucursal: { select: { descripcion: true, punto_establecimiento: true } },
    },
  });
  if (!deposito) {
    log('FATAL', `La empresa no tiene ningún depósito con código '${DEPOSITO_CODIGO}'.`);
    process.exit(1);
  }
  info(`Depósito: ${deposito.descripcion} (${DEPOSITO_CODIGO}) — sucursal ${deposito.sucursal?.descripcion} [${deposito.sucursal?.punto_establecimiento}]`);

  const tipoEntrada = await prisma.tipo_movimiento_inventario.findUnique({
    where: { codigo: 'ENTRADA' }, select: { id: true },
  });
  if (!tipoEntrada) { log('FATAL', `Falta el tipo de movimiento 'ENTRADA'.`); process.exit(1); }
  raw('');

  // ── Leer y reparar el CSV ──────────────────────────────────────
  if (!fs.existsSync(CSV_PATH)) { log('FATAL', `Archivo no encontrado: ${CSV_PATH}`); process.exit(1); }
  const lineas = fs.readFileSync(CSV_PATH, CONFIG.ENCODING).replace(/\r/g, '').split('\n').filter((l) => l.trim());
  const filas: string[][] = [];
  const anomalias: Anomalia[] = [];
  let corridas = 0;

  for (const linea of lineas.slice(1)) {
    const { campos, corridos } = repararCorrimiento(partirLinea(linea, CONFIG.SEP));
    if (corridos > 0) {
      corridas++;
      // Chequeo independiente: COSTO x STOCK tiene que dar costo × stock.
      const costo = toNum(campos[COL.COSTO]);
      const stock = toNum(campos[COL.STOCK]);
      const esperado = toNum(campos[COL.COSTO_X_STOCK]);
      const cierra = Math.abs(costo * stock - esperado) < 1;
      anomalias.push({
        motivo: `FILA CORRIDA POR UN ';' EN EL NOMBRE — reparada${cierra ? '' : ' (¡NO valida contra COSTO x STOCK!)'}`,
        detalle: `[${limpiarCodigo(campos[COL.CODIGO])}] ${campos[COL.NOMBRE]} | costo ${fmtGs(costo)} × stock ${stock} = ${fmtGs(costo * stock)} vs archivo ${fmtGs(esperado)}`,
      });
    }
    filas.push(campos);
  }
  info(`Filas leídas : ${filas.length}   (reparadas por corrimiento: ${corridas})`);

  // ── Resolver colisiones de código ──────────────────────────────
  const codigosCsv = filas.map((f) => limpiarCodigo(f[COL.CODIGO]));
  const existentes = await prisma.productos.findMany({
    where: { empresa_id: EMPRESA_ID, cod_producto: { in: codigosCsv.filter(Boolean) } },
    select: { cod_producto: true, descripcion: true },
  });
  // Un código "choca" sólo si en el sistema es OTRO producto. Si la descripción
  // coincide es el mismo y hay que actualizarlo, no duplicarlo con prefijo.
  const norm = (s: string) => s.normalize('NFD').replace(/[̀-ͯ]/g, '').toUpperCase().replace(/\s+/g, ' ').trim();
  const descPorCodigo = new Map(existentes.map((p) => [p.cod_producto!, norm(p.descripcion ?? '')]));
  const codigoFinal = new Map<number, string>();
  for (let i = 0; i < filas.length; i++) {
    const cod = codigosCsv[i];
    const enSistema = descPorCodigo.get(cod);
    if (enSistema !== undefined && enSistema !== norm(filas[i][COL.NOMBRE])) {
      const prefijado = `${PREFIJO}${cod}`;
      codigoFinal.set(i, prefijado);
      anomalias.push({
        motivo: 'CÓDIGO YA USADO POR OTRO PRODUCTO — se migra con prefijo',
        detalle: `${cod} → ${prefijado} | taller: "${filas[i][COL.NOMBRE]}" | ya existía: "${existentes.find((e) => e.cod_producto === cod)?.descripcion}"`,
      });
    } else {
      codigoFinal.set(i, cod);
    }
  }
  info(`Códigos prefijados por colisión: ${[...codigoFinal.values()].filter((c) => c.startsWith(PREFIJO)).length}`);
  raw('');

  // ── Asegurar las 4 listas de precios ───────────────────────────
  const listaIdPorColumna = new Map<string, string>();
  for (const l of LISTAS) {
    const existente = await prisma.lista_precios.findFirst({
      where: { empresa_id: EMPRESA_ID, codigo: l.codigo },
      select: { id: true },
    });
    if (existente) {
      listaIdPorColumna.set(l.columna, existente.id);
      skip(`Lista ya existente | ${l.codigo} — ${l.nombre}`);
      continue;
    }
    if (DRY) {
      listaIdPorColumna.set(l.columna, `DRY-${l.codigo}`);
      info(`[DRY] Lista a crear | ${l.codigo} — ${l.nombre} (prioridad ${l.prioridad})`);
      continue;
    }
    const creada = await prisma.lista_precios.create({
      data: {
        empresa_id: EMPRESA_ID,
        codigo: l.codigo,
        nombre: l.nombre,
        descripcion: `Creada por migración desde ${path.basename(CSV_PATH)}`,
        tipo_aplicacion: 'general',
        moneda: 'PYG',
        prioridad: l.prioridad,
        activa: true,
      },
      select: { id: true },
    });
    listaIdPorColumna.set(l.columna, creada.id);
    ok(`Lista creada | ${l.codigo} — ${l.nombre}`);
  }
  raw('');

  // ── Procesar productos ─────────────────────────────────────────
  let creados = 0, actualizados = 0, omitidos = 0;
  let conStock = 0, stockNegativo = 0;
  const preciosPorLista = new Map<string, number>();
  const erroresList: string[] = [];

  for (let i = 0; i < filas.length; i++) {
    const f = filas[i];
    const cod = codigoFinal.get(i)!;
    const descripcion = (f[COL.NOMBRE] ?? '').trim();
    if (!cod || !descripcion) {
      omitidos++;
      anomalias.push({ motivo: 'SIN CÓDIGO O SIN NOMBRE — no se migra', detalle: `fila ${i + 2}` });
      continue;
    }
    const label = `[${cod}] ${descripcion.slice(0, 44)}`;

    const ivaTexto = (f[COL.IVA] ?? '').trim();
    const iva = ivaTexto === '0' ? 0 : 10; // tras reparar, el resto es 10
    const precioCosto = aGs(f[COL.COSTO]);
    const minorista = aGs(f[COL.MINORISTA]);
    const stock = toNum(f[COL.STOCK]);
    const stockMinimo = Math.max(0, toNum(f[COL.STK_MINIMO]));

    if (stock < 0) {
      stockNegativo++;
      anomalias.push({ motivo: 'STOCK NEGATIVO — queda en 0', detalle: `${label} | archivo: ${stock}` });
    }
    if (minorista === 0) {
      // Sin precio, sin costo y sin stock casi nunca es un artículo de
      // inventario: en este archivo son ALINEACION, BALANCEO, MONTAJE,
      // REDONDEO. Se migran igual como producto (el CSV no distingue), pero
      // conviene pasarlos a tipo "servicio" desde la pantalla de productos:
      // así dejan de pedir stock para poder venderse.
      if (precioCosto === 0 && stock <= 0) {
        anomalias.push({
          motivo: 'PARECE UN SERVICIO, NO UN PRODUCTO — pasalo a tipo "servicio" en la UI',
          detalle: label,
        });
      } else {
        anomalias.push({ motivo: 'SIN PRECIO MINORISTA — se guarda en 0', detalle: label });
      }
    }
    if ((f[COL.MDA] ?? '') === 'Usd') {
      anomalias.push({ motivo: 'PRODUCTO EN DÓLARES — se migra igual, revisá el precio', detalle: label });
    }

    // Qué listas corresponden: sólo las columnas con valor > 0.
    const listasDelProducto = LISTAS
      .map((l) => ({ l, precio: aGs(f[COL_POR_NOMBRE[l.columna]]) }))
      .filter((x) => x.precio > 0);

    if (DRY) {
      const detalle = listasDelProducto.map((x) => `${x.l.codigo}=${fmtGs(x.precio)}`).join(' ');
      info(`[DRY] ${label} | costo ${fmtGs(precioCosto)} | stock ${stock} | ${detalle || 'sin listas'}`);
      creados++;
      if (stock > 0) conStock++;
      for (const x of listasDelProducto) {
        preciosPorLista.set(x.l.codigo, (preciosPorLista.get(x.l.codigo) ?? 0) + 1);
      }
      continue;
    }

    try {
      const existente = await prisma.productos.findFirst({
        where: { empresa_id: EMPRESA_ID, cod_producto: cod },
        select: { id: true },
      });

      const data = {
        descripcion,
        cod_producto: cod,
        precio_costo: precioCosto,
        precio: minorista,
        afectacion_id: iva === 0 ? CONFIG.AFECTACION_EXENTO : CONFIG.AFECTACION_GRAVADO,
        unidad_medida_id: CONFIG.UNIDAD_MEDIDA_ID,
        porcentaje_iva: iva,
        proporcion_iva: CONFIG.PROPORCION_IVA,
        tipo: CONFIG.TIPO_PRODUCTO,
        maneja_inventario: true,
        maneja_lote: false,
      };

      let productoId: string;
      let esNuevo: boolean;
      if (existente) {
        await prisma.productos.update({ where: { id: existente.id }, data });
        productoId = existente.id;
        esNuevo = false;
        actualizados++;
        skip(`Actualizado (sin tocar stock) | ${label}`);
      } else {
        const creado = await prisma.productos.create({
          data: { ...data, empresa_id: EMPRESA_ID }, select: { id: true },
        });
        productoId = creado.id;
        esNuevo = true;
        creados++;
        ok(`Creado | ${label} | costo ${fmtGs(precioCosto)} | minorista ${fmtGs(minorista)}`);
      }

      // Precios de lista: idempotentes por (lista, producto).
      for (const { l, precio } of listasDelProducto) {
        const listaId = listaIdPorColumna.get(l.columna)!;
        await prisma.lista_precios_productos.upsert({
          where: { unique_lista_precio_producto: { lista_precios_id: listaId, producto_id: productoId } },
          create: { lista_precios_id: listaId, producto_id: productoId, precio_base: precio, origen_precio: 'manual' },
          update: { precio_base: precio, fecha_actualizacion: new Date() },
        });
        preciosPorLista.set(l.codigo, (preciosPorLista.get(l.codigo) ?? 0) + 1);
      }

      // El stock inicial sólo la primera vez, para que reejecutar no sume dos veces.
      if (esNuevo && stock > 0) {
        await prisma.$transaction([
          prisma.stock_deposito.upsert({
            where: { deposito_id_producto_id: { deposito_id: deposito.id, producto_id: productoId } },
            create: { deposito_id: deposito.id, producto_id: productoId, cantidad_disponible: stock, stock_minimo: stockMinimo },
            update: { cantidad_disponible: { increment: stock }, stock_minimo: stockMinimo },
          }),
          prisma.movimientos_inventario.create({
            data: {
              deposito_origen_id: deposito.id,
              producto_id: productoId,
              tipo_movimiento_id: tipoEntrada.id,
              cantidad: stock,
              documento_origen: 'migracion_inicial',
              observaciones: `Carga inicial de stock — migración desde ${path.basename(CSV_PATH)}`,
            },
          }),
        ]);
        conStock++;
      } else if (esNuevo && stockMinimo > 0) {
        // Sin stock pero con mínimo definido: igual sirve para los avisos de reposición.
        await prisma.stock_deposito.upsert({
          where: { deposito_id_producto_id: { deposito_id: deposito.id, producto_id: productoId } },
          create: { deposito_id: deposito.id, producto_id: productoId, cantidad_disponible: 0, stock_minimo: stockMinimo },
          update: { stock_minimo: stockMinimo },
        });
      }
    } 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(`║  Productos creados           : ${String(creados).padStart(8)}             ║`);
  raw(`║  Productos actualizados      : ${String(actualizados).padStart(8)}             ║`);
  raw(`║  Con stock inicial cargado   : ${String(conStock).padStart(8)}             ║`);
  raw(`║  Stock negativo (queda en 0) : ${String(stockNegativo).padStart(8)}             ║`);
  raw(`║  Omitidos (sin código/nombre): ${String(omitidos).padStart(8)}             ║`);
  raw('╠══════════════════════════════════════════════════════╣');
  for (const l of LISTAS) {
    raw(`║  Precios en ${l.nombre.padEnd(16)}: ${String(preciosPorLista.get(l.codigo) ?? 0).padStart(8)}             ║`);
  }
  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, 30).forEach((d) => raw(`    · ${d}`));
      if (detalles.length > 30) raw(`    … y ${detalles.length - 30} más (ver log completo)`);
    }
  }
  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(); });
