/**
 * ╔══════════════════════════════════════════════════════════════╗
 * ║   ACTUALIZACIÓN: Stock del taller + productos nuevos            ║
 * ╠══════════════════════════════════════════════════════════════╣
 * ║  Lee el export de stock del sistema viejo (STOCK18center*.csv) ║
 * ║    CODIGO;NOMBRE;CLASE;STK MIN;REPOSICION;STOCK;               ║
 * ║  y deja el depósito DEPTALLER con esas cantidades.             ║
 * ║                                                                ║
 * ║  A diferencia de migrate-productos-taller.ts, acá el stock se  ║
 * ║  FIJA al valor del archivo (no se suma): el archivo es la foto ║
 * ║  del sistema viejo a la fecha de corte. La diferencia contra   ║
 * ║  lo que hay se registra como AJUSTE_POSITIVO / AJUSTE_NEGATIVO ║
 * ║  en movimientos_inventario, así queda la trazabilidad.         ║
 * ║                                                                ║
 * ║  Reejecutarlo es seguro: con el stock ya igual no hace nada.   ║
 * ║                                                                ║
 * ║  PRODUCTOS NUEVOS: el archivo no trae precios. Se crean con    ║
 * ║  costo y precio en 0, en el rubro TALLER, y se listan al final ║
 * ║  para cargarles el precio a mano.                              ║
 * ║                                                                ║
 * ║  CÓDIGOS: migrate-productos-taller prefijó con "TALLER-" los   ║
 * ║  códigos que chocaban con otro catálogo. Se busca primero la   ║
 * ║  versión prefijada y después el código pelado. Si el pelado    ║
 * ║  es de OTRO rubro, no se toca y se reporta.                    ║
 * ║                                                                ║
 * ║  CORTE: si el depósito ya tiene movimientos reales (ventas,    ║
 * ║  compras, ajustes) posteriores a --corte, fijar el stock los   ║
 * ║  borraría. En ese caso aborta salvo --forzar o --netear.       ║
 * ║                                                                ║
 * ║  --netear: en vez de pisar los movimientos posteriores, los    ║
 * ║  suma al archivo:  objetivo = archivo + entradas − salidas     ║
 * ║  (por producto, desde el corte hasta ahora). Cubre ENTRADA /   ║
 * ║  VENTA / SALIDA / AJUSTE_* por afecta_stock, DEVOLUCION como   ║
 * ║  entrada y TRANSFERENCIA según el depósito sea origen/destino. ║
 * ║                                                                ║
 * ║  Stock negativo en el archivo → queda en 0 (misma regla que    ║
 * ║  POST /stock/ajustar).                                         ║
 * ╠══════════════════════════════════════════════════════════════╣
 * ║  Uso:                                                          ║
 * ║    npx ts-node --transpile-only -r tsconfig-paths/register \   ║
 * ║      --project tsconfig.scripts.json \                         ║
 * ║      scripts/migrate-access/actualizar-stock-taller.ts \       ║
 * ║      --empresa=<uuid> --dry                                    ║
 * ║                                                                ║
 * ║  Flags: --archivo=  --deposito=DEPTALLER  --prefijo=TALLER-    ║
 * ║         --rubro=TALLER  --corte=2026-09-12  --forzar --netear  ║
 * ╚══════════════════════════════════════════════════════════════╝
 */

import 'dotenv/config';
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/nuevo/STOCK18center120926.csv',
  ),
  ENCODING: 'latin1' as BufferEncoding, // export de Windows, no UTF-8
  SEP: ';',
  DEPOSITO_CODIGO: 'DEPTALLER',
  PREFIJO_COLISION: 'TALLER-',
  RUBRO_CODIGO: 'TALLER',
  CATEGORIA_RAIZ: 'Taller',
  // Fecha del export: "120926" en el nombre del archivo.
  CORTE: '2026-09-12',
  // Orígenes que escriben los scripts de migración; cualquier otro es operación real.
  ORIGENES_MIGRACION: ['migracion_inicial', 'ajuste_migracion'],

  AFECTACION_GRAVADO: '7b634413-a746-4152-9941-4d586ed75825',
  UNIDAD_MEDIDA_ID: '4f6a1318-1a42-454d-874e-a72f6cda04df', // "Unidad"
  IVA: 10,
  PROPORCION_IVA: 100,
  TIPO_PRODUCTO: 'producto' 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 FORZAR = process.argv.includes('--forzar');
const NETEAR = process.argv.includes('--netear');
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 RUBRO_CODIGO = argValue('rubro') ?? CONFIG.RUBRO_CODIGO;
const CORTE_TEXTO = argValue('corte') ?? CONFIG.CORTE;
// Fin del día de corte: lo movido ese mismo día ya está en el export.
const CORTE = new Date(`${CORTE_TEXTO}T23:59:59.999Z`);
if (Number.isNaN(CORTE.getTime())) {
  console.error(`✗✗FATAL --corte inválido: "${CORTE_TEXTO}". Formato aaaa-mm-dd.`);
  process.exit(1);
}

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-stock-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 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());
}

type FilaStock = { linea: number; codigo: string; nombre: string; stock: number | null; crudoStock: string };

/**
 * Las filas sanas terminan en ';' (último campo vacío). Las que traen pulgadas
 * en el nombre ("LIMPIAPARABRISA DE 14""") vienen con un campo de más y sin el
 * ';' final. Por eso las columnas se leen desde la derecha: STOCK es el último
 * campo real, y el NOMBRE es todo lo que queda entre CODIGO y CLASE.
 */
function parsearFila(linea: string, nro: number): FilaStock {
  const campos = partirLinea(linea, CONFIG.SEP);
  if (campos.length > 6 && campos[campos.length - 1] === '') campos.pop();
  const n = campos.length;
  const crudoStock = campos[n - 1] ?? '';
  return {
    linea: nro,
    codigo: (campos[0] ?? '').replace(/^'+/, '').trim(),
    nombre: campos.slice(1, Math.max(2, n - 4)).join(' ').replace(/\s+/g, ' ').trim(),
    stock: toNumOrNull(crudoStock),
    crudoStock,
  };
}

/** En este export la coma es decimal ("11,1") y no hay separador de miles. */
function toNumOrNull(v: string): number | null {
  const s = v.trim().replace(',', '.');
  if (!/^-?\d+(\.\d+)?$/.test(s)) return null;
  const n = parseFloat(s);
  return Number.isFinite(n) ? n : null;
}

const norm = (s: string) =>
  s.normalize('NFD').replace(/[̀-ͯ]/g, '').toUpperCase().replace(/[^A-Z0-9]+/g, ' ').trim();

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

async function main() {
  raw('');
  raw('╔══════════════════════════════════════════════════════╗');
  raw(`║  STOCK TALLER + PRODUCTOS NUEVOS ${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(`║  Corte:    ${CORTE_TEXTO.padEnd(42)}║`);
  raw(`║  Modo:     ${(NETEAR ? 'archivo + movimientos posteriores' : 'fijar al archivo').padEnd(42)}║`);
  raw('╚══════════════════════════════════════════════════════╝');
  raw('');

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

  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 rubro = await prisma.rubros.findFirst({ where: { empresa_id: EMPRESA_ID, codigo: RUBRO_CODIGO }, select: { id: true, descripcion: true } });
  if (!rubro) { log('FATAL', `La empresa no tiene el rubro '${RUBRO_CODIGO}'. Correr setup-rubros-agogo.ts primero.`); process.exit(1); }
  const categoria = await prisma.categorias.findFirst({
    where: { empresa_id: EMPRESA_ID, descripcion: CONFIG.CATEGORIA_RAIZ, deleted_at: false },
    select: { id: true },
  });
  info(`Rubro: ${rubro.descripcion}${categoria ? ` — categoría raíz "${CONFIG.CATEGORIA_RAIZ}"` : ' — sin categoría raíz'}`);

  const tipos = await prisma.tipo_movimiento_inventario.findMany({
    where: { codigo: { in: ['ENTRADA', 'AJUSTE_POSITIVO', 'AJUSTE_NEGATIVO'] } },
    select: { id: true, codigo: true },
  });
  const tipoId = (c: string) => tipos.find((t) => t.codigo === c)?.id;
  const TIPO_ENTRADA = tipoId('ENTRADA'), TIPO_AJ_POS = tipoId('AJUSTE_POSITIVO'), TIPO_AJ_NEG = tipoId('AJUSTE_NEGATIVO');
  if (!TIPO_ENTRADA || !TIPO_AJ_POS || !TIPO_AJ_NEG) {
    log('FATAL', 'Faltan tipos de movimiento ENTRADA / AJUSTE_POSITIVO / AJUSTE_NEGATIVO.');
    process.exit(1);
  }

  // ── Guarda de corte ────────────────────────────────────────────
  // Filtro de "movimiento real posterior": origen o destino (transferencias entrantes) en este depósito.
  const wherePosterior = {
    OR: [{ deposito_origen_id: deposito.id }, { deposito_destino_id: deposito.id }],
    created_at: { gt: CORTE },
    NOT: { documento_origen: { in: CONFIG.ORIGENES_MIGRACION } },
  };
  const posteriores = await prisma.movimientos_inventario.count({ where: wherePosterior });
  if (posteriores > 0 && NETEAR) {
    info(`El depósito tiene ${posteriores} movimiento(s) reales posteriores al ${CORTE_TEXTO}: se netean contra el archivo (--netear).`);
  } else if (posteriores > 0) {
    log(FORZAR ? 'WARN' : 'FATAL',
      `El depósito tiene ${posteriores} movimiento(s) reales posteriores al ${CORTE_TEXTO}. ` +
      `Fijar el stock al archivo los pisaría.${FORZAR ? ' Se sigue por --forzar.' : ' Usá --forzar si igual querés aplicarlo, o --netear para sumarlos al archivo.'}`);
    if (!FORZAR) process.exit(1);
  }

  // ── Delta posterior al corte por producto (solo --netear) ──────
  // objetivo = archivo + Σ entradas − Σ salidas desde el corte. Los movimientos
  // de migración se excluyen: son justamente lo que este script reescribe.
  const deltaPosterior = new Map<string, number>();
  if (NETEAR) {
    const movs = await prisma.movimientos_inventario.findMany({
      where: wherePosterior,
      select: {
        producto_id: true, cantidad: true, deposito_origen_id: true, deposito_destino_id: true,
        tipo_movimiento: { select: { codigo: true, afecta_stock: true } },
      },
    });
    const sinSigno = new Set<string>();
    for (const m of movs) {
      const cant = Number(m.cantidad);
      const { codigo, afecta_stock } = m.tipo_movimiento;
      let signo = 0;
      if (codigo === 'TRANSFERENCIA') {
        // Un solo registro: resta en origen, suma en destino (stock.service transferir).
        signo = (m.deposito_destino_id === deposito.id ? 1 : 0) - (m.deposito_origen_id === deposito.id ? 1 : 0);
      } else if (codigo === 'DEVOLUCION') {
        // afecta_stock=0 en la tabla, pero NC ventas / anulación SIFEN suman stock.
        signo = 1;
      } else if (m.deposito_origen_id === deposito.id) {
        signo = afecta_stock;
        if (afecta_stock === 0) sinSigno.add(codigo);
      }
      if (signo === 0) continue;
      deltaPosterior.set(m.producto_id, (deltaPosterior.get(m.producto_id) ?? 0) + signo * cant);
    }
    if (sinSigno.size > 0) {
      log('WARN', `Tipos con afecta_stock=0 sin regla de neteo, ignorados: ${[...sinSigno].join(', ')}`);
    }
    info(`Movimientos posteriores leídos: ${movs.length} — productos con delta: ${deltaPosterior.size}`);
  }
  raw('');

  // ── Leer 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');
  const cabecera = partirLinea(lineas[0] ?? '', CONFIG.SEP).map((c) => c.toUpperCase());
  if (cabecera[0] !== 'CODIGO' || !cabecera.includes('STOCK')) {
    log('FATAL', `Encabezado inesperado: "${lineas[0]}". Se esperaba CODIGO;NOMBRE;CLASE;STK MIN;REPOSICION;STOCK;`);
    process.exit(1);
  }
  const filas = lineas.slice(1).map((l, i) => ({ l, nro: i + 2 })).filter((x) => x.l.trim()).map((x) => parsearFila(x.l, x.nro));
  info(`Filas leídas: ${filas.length}`);

  // ── Productos de la empresa, indexados por código ─────────────
  const productos = await prisma.productos.findMany({
    where: { empresa_id: EMPRESA_ID, deleted: false },
    select: {
      id: true, cod_producto: true, descripcion: true, rubro_id: true,
      stock_deposito: { where: { deposito_id: deposito.id }, select: { cantidad_disponible: true } },
    },
  });
  const porCodigo = new Map(productos.filter((p) => p.cod_producto).map((p) => [p.cod_producto as string, p]));
  info(`Productos de la empresa: ${productos.length}`);
  raw('');

  const anomalias: Anomalia[] = [];
  const erroresList: string[] = [];
  const nuevosSinPrecio: string[] = [];
  let sinCambios = 0, ajustados = 0, subieron = 0, bajaron = 0, creados = 0, omitidos = 0;
  const vistos = new Set<string>();

  for (const f of filas) {
    if (!f.codigo) {
      omitidos++;
      anomalias.push({ motivo: 'SIN CÓDIGO — no se procesa', detalle: `línea ${f.linea} | "${f.nombre}"` });
      continue;
    }
    if (vistos.has(f.codigo)) {
      omitidos++;
      anomalias.push({ motivo: 'CÓDIGO REPETIDO EN EL ARCHIVO — se usa la primera aparición', detalle: `línea ${f.linea} | [${f.codigo}] ${f.nombre}` });
      continue;
    }
    vistos.add(f.codigo);
    const label = `[${f.codigo}] ${f.nombre.slice(0, 44)}`;

    if (f.stock === null) {
      omitidos++;
      anomalias.push({ motivo: 'STOCK VACÍO O ILEGIBLE — no se toca', detalle: `${label} | celda: "${f.crudoStock}"` });
      continue;
    }
    if (f.stock < 0) {
      anomalias.push({ motivo: 'STOCK NEGATIVO EN EL ARCHIVO — queda en 0', detalle: `${label} | archivo: ${f.stock}` });
    }
    if (!Number.isInteger(f.stock)) {
      anomalias.push({ motivo: 'STOCK CON DECIMALES — se carga tal cual, revisar', detalle: `${label} | archivo: ${f.crudoStock}` });
    }
    const objetivo = Math.max(0, f.stock);

    const producto = porCodigo.get(`${PREFIJO}${f.codigo}`) ?? porCodigo.get(f.codigo);

    // ── Producto nuevo ───────────────────────────────────────────
    if (!producto) {
      nuevosSinPrecio.push(`${label} | stock ${objetivo}`);
      if (DRY) {
        info(`[DRY] Nuevo | ${label} | stock ${objetivo}`);
        creados++;
        continue;
      }
      try {
        await prisma.$transaction(async (tx) => {
          const creado = await tx.productos.create({
            data: {
              empresa_id: EMPRESA_ID,
              cod_producto: f.codigo,
              descripcion: f.nombre,
              precio_costo: 0,
              precio: 0,
              afectacion_id: CONFIG.AFECTACION_GRAVADO,
              unidad_medida_id: CONFIG.UNIDAD_MEDIDA_ID,
              porcentaje_iva: CONFIG.IVA,
              proporcion_iva: CONFIG.PROPORCION_IVA,
              tipo: CONFIG.TIPO_PRODUCTO,
              maneja_inventario: true,
              maneja_lote: false,
              rubro_id: rubro.id,
              ...(categoria ? { categoria_id: categoria.id } : {}),
            },
            select: { id: true },
          });
          await tx.stock_deposito.create({
            data: { deposito_id: deposito.id, producto_id: creado.id, cantidad_disponible: objetivo },
          });
          if (objetivo > 0) {
            await tx.movimientos_inventario.create({
              data: {
                deposito_origen_id: deposito.id,
                producto_id: creado.id,
                tipo_movimiento_id: TIPO_ENTRADA,
                cantidad: objetivo,
                documento_origen: 'migracion_inicial',
                observaciones: `Alta de producto con stock — ${path.basename(CSV_PATH)}`,
              },
            });
          }
        });
        creados++;
        ok(`Creado | ${label} | stock ${objetivo} | SIN PRECIO`);
      } catch (e: any) {
        erroresList.push(`${label} | ${e.message}`);
        err(`${label} | ${e.message}`);
      }
      continue;
    }

    // ── Producto existente ───────────────────────────────────────
    if (producto.rubro_id && producto.rubro_id !== rubro.id) {
      omitidos++;
      anomalias.push({
        motivo: 'EL CÓDIGO ES DE UN PRODUCTO DE OTRO RUBRO — no se toca',
        detalle: `${label} | en el sistema: [${producto.cod_producto}] ${producto.descripcion}`,
      });
      continue;
    }
    if (norm(producto.descripcion ?? '') !== norm(f.nombre)) {
      anomalias.push({
        motivo: 'NOMBRE DISTINTO AL DEL SISTEMA — se actualiza el stock igual, verificar',
        detalle: `${label} | en el sistema: [${producto.cod_producto}] ${producto.descripcion}`,
      });
    }

    const actual = Number(producto.stock_deposito[0]?.cantidad_disponible ?? 0);
    const delta = deltaPosterior.get(producto.id) ?? 0;
    const archivoMasDelta = Math.round((objetivo + delta) * 10000) / 10000;
    if (NETEAR && archivoMasDelta < 0) {
      anomalias.push({
        motivo: 'ARCHIVO + MOVIMIENTOS POSTERIORES DA NEGATIVO — queda en 0, revisar',
        detalle: `${label} | archivo ${objetivo} ${delta >= 0 ? '+' : '−'} ${Math.abs(delta)} = ${archivoMasDelta}`,
      });
    }
    const objetivoFinal = NETEAR ? Math.max(0, archivoMasDelta) : objetivo;
    const diferencia = Math.round((objetivoFinal - actual) * 10000) / 10000;
    if (diferencia === 0) { sinCambios++; continue; }

    const detalleNeteo = NETEAR && delta !== 0 ? ` [archivo ${objetivo} ${delta > 0 ? '+' : '−'} ${Math.abs(delta)} post-corte]` : '';
    const etiqueta = `[${producto.cod_producto}] ${(producto.descripcion ?? '').slice(0, 44)} | ${actual} → ${objetivoFinal}${detalleNeteo} (${diferencia > 0 ? '+' : ''}${diferencia})`;
    if (diferencia > 0) subieron++; else bajaron++;

    if (DRY) {
      info(`[DRY] Ajuste | ${etiqueta}`);
      ajustados++;
      continue;
    }
    try {
      await prisma.$transaction([
        prisma.stock_deposito.upsert({
          where: { deposito_id_producto_id: { deposito_id: deposito.id, producto_id: producto.id } },
          create: { deposito_id: deposito.id, producto_id: producto.id, cantidad_disponible: objetivoFinal },
          update: { cantidad_disponible: objetivoFinal, updated_at: new Date() },
        }),
        prisma.movimientos_inventario.create({
          data: {
            deposito_origen_id: deposito.id,
            producto_id: producto.id,
            tipo_movimiento_id: diferencia > 0 ? TIPO_AJ_POS : TIPO_AJ_NEG,
            cantidad: Math.abs(diferencia),
            documento_origen: 'ajuste_migracion',
            observaciones: NETEAR
              ? `Ajuste a stock del sistema anterior al ${CORTE_TEXTO} + movimientos posteriores (archivo ${objetivo} ${delta >= 0 ? '+' : '−'} ${Math.abs(delta)}; ${actual} → ${objetivoFinal}) — ${path.basename(CSV_PATH)}`
              : `Ajuste a stock del sistema anterior al ${CORTE_TEXTO} (${actual} → ${objetivo}) — ${path.basename(CSV_PATH)}`,
          },
        }),
      ]);
      ajustados++;
      ok(`Ajustado | ${etiqueta}`);
    } catch (e: any) {
      erroresList.push(`${etiqueta} | ${e.message}`);
      err(`${etiqueta} | ${e.message}`);
    }
  }

  // Productos del taller con stock que no vinieron en el archivo: no se tocan,
  // pero conviene saberlo (pueden haberse dado de baja en el sistema viejo).
  const ausentes = productos.filter((p) => {
    if (p.rubro_id !== rubro.id) return false;
    const cod = (p.cod_producto ?? '').replace(new RegExp(`^${PREFIJO}`), '');
    return !vistos.has(cod) && Number(p.stock_deposito[0]?.cantidad_disponible ?? 0) > 0;
  });
  for (const p of ausentes) {
    anomalias.push({
      motivo: 'PRODUCTO DEL TALLER CON STOCK QUE NO ESTÁ EN EL ARCHIVO — no se tocó',
      detalle: `[${p.cod_producto}] ${p.descripcion} | stock ${Number(p.stock_deposito[0]?.cantidad_disponible)}`,
    });
  }

  // ── Resumen ────────────────────────────────────────────────────
  raw('');
  raw('╔══════════════════════════════════════════════════════╗');
  raw('║                    RESUMEN FINAL                     ║');
  raw('╠══════════════════════════════════════════════════════╣');
  raw(`║  Filas del archivo           : ${String(filas.length).padStart(8)}             ║`);
  raw(`║  Stock ya correcto           : ${String(sinCambios).padStart(8)}             ║`);
  raw(`║  Stock ajustado              : ${String(ajustados).padStart(8)}             ║`);
  raw(`║    · subieron                : ${String(subieron).padStart(8)}             ║`);
  raw(`║    · bajaron                 : ${String(bajaron).padStart(8)}             ║`);
  raw(`║  Productos nuevos            : ${String(creados).padStart(8)}             ║`);
  raw(`║  Omitidos                    : ${String(omitidos).padStart(8)}             ║`);
  raw('╠══════════════════════════════════════════════════════╣');
  raw(`║  Anomalías a revisar         : ${String(anomalias.length).padStart(8)}             ║`);
  raw(`║  Errores                     : ${String(erroresList.length).padStart(8)}             ║`);
  raw('╚══════════════════════════════════════════════════════╝');

  if (nuevosSinPrecio.length > 0) {
    raw('');
    raw('── PRODUCTOS NUEVOS SIN PRECIO (cargar costo y precio a mano) ──');
    nuevosSinPrecio.forEach((d) => raw(`    · ${d}`));
  }
  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(); });
