/**
 * ╔══════════════════════════════════════════════════════════════╗
 * ║   SCRIPT DE MIGRACIÓN: Access (NICO) → Novasis ERP        ║
 * ╠══════════════════════════════════════════════════════════════╣
 * ║  Idéntico proceso a scripts/migrate-access/migrate.ts, con    ║
 * ║  UNA sola diferencia de filtro:                              ║
 * ║    • Solo se migran las filas cuyo campo "Articulo" del       ║
 * ║      Excel CONTENGA la palabra "NICO" (case-insensitive).    ║
 * ║      Las demás se omiten.                                    ║
 * ║  El resto (forma de pago, tipo de cliente, saldo>0, IVA 10%, ║
 * ║  cuotas, CxC, cobradores) es exactamente el mismo proceso.  ║
 * ╠══════════════════════════════════════════════════════════════╣
 * ║  Crea:                                                       ║
 * ║    • personas + clientes (deduplicado por cédula)            ║
 * ║    • factura_cab  (registro "migrado", una por venta)        ║
 * ║    • factura_det  (1 ítem con el campo Articulo del Excel)   ║
 * ║    • factura_subtotales (totales + IVA 10%)                  ║
 * ║    • cuentas_cobrar   (saldo pendiente actual)               ║
 * ║    • factura_cuotas   (proyección desde hoy)                 ║
 * ╠══════════════════════════════════════════════════════════════╣
 * ║  Instrucciones:                                              ║
 * ║  1. Colocar clientes.xlsx y facturas.xlsx en ESTA carpeta.   ║
 * ║  2. Indicar la empresa destino con --empresa=<uuid>          ║
 * ║     (o editar CONFIG.EMPRESA_ID abajo).                      ║
 * ║  3. Dry-run:                                                ║
 * ║       npx ts-node scripts/data_script/cliente-niko/migrate-niko.ts \
 * ║         --empresa=<uuid> --dry                               ║
 * ║  4. Real (escribe en la BD):                                ║
 * ║       npx ts-node scripts/data_script/cliente-niko/migrate-niko.ts \
 * ║         --empresa=<uuid>                                     ║
 * ╚══════════════════════════════════════════════════════════════╝
 *
 * Columnas esperadas en clientes.xlsx:
 *   Id | Nombre | Articulo | Cedula | Fecha de registro | Forma de pago |
 *   Tipo de cliente | Dirección | Teléfono 1 | Importe Total | Saldo | Vendedora
 *
 * Columnas esperadas en facturas.xlsx:
 *   Id | Fecha facturación | Abono1 | Saldo
 */

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

// ══════════════════════════════════════════════════════════════════
//  CONFIG
//  Los UUIDs de referenciales son catálogos GLOBALES (sin empresa_id),
//  válidos para cualquier empresa del mismo DB. Solo EMPRESA_ID y el
//  producto genérico (GEN0001) son específicos de la empresa destino.
// ══════════════════════════════════════════════════════════════════
const EMPRESA_ARG = process.argv.find((a) => a.startsWith('--empresa='));
const EMPRESA_ID_CLI = EMPRESA_ARG ? EMPRESA_ARG.replace('--empresa=', '').trim() : '';

const CONFIG = {
  // ── Empresa destino ───────────────────────────────────────────
  // Se toma de --empresa=<uuid>. Si preferís fijarla, poné el UUID acá.
  EMPRESA_ID: EMPRESA_ID_CLI || '',

  // ── Numeración de las facturas migradas ───────────────────────
  DEST: '001',
  DPUNEXP: '001',
  SERIE: 'MI', // prefijo visible en dinfadic

  // ── IDs de referenciales (catálogos globales, mismos para toda empresa) ──
  // condicion_operacion: Crédito (codigo=2)
  CONDICION_CREDITO_ID: '01f801b8-0a2d-48c4-9c89-6bedbb145482',
  // tipo_transaccion: Venta de mercadería (codigo=1)
  TIPO_TRANSACCION_ID: '0ba68a57-7e52-487a-8437-6d55f801b935',
  // indicador_presencia: Operación presencial (codigo=1)
  INDICADOR_PRESENCIA_ID: 'eeec7470-e500-499b-ae44-704b929a1d37',
  // tipo_impuesto: IVA (codigo=1)
  TIPO_IMPUESTO_ID: '41a49136-e1ac-4bbf-88c8-3d12674ee4e8',
  // moneda: Guaraní
  MONEDA_ID: '9c131c09-bc75-4cf0-b79b-1c28e498564c',
  MONEDA_ISO: 'PYG',
  // tipo_operacion: B2C (persona física local)
  TIPO_OPERACION_ID: 'f4464d3e-b711-424c-9833-84a9d4eca8e8',
  // tipo_documento_identidad: Cédula paraguaya
  TIPO_DOCUMENTO_ID: '7ce2cf6d-af16-4be2-a1da-628127a2c3c9',
  // naturaleza_receptor: Contribuyente (codigo=1)
  NATURALEZA_ID: '50093f82-e37d-4120-ad06-cdfdfffa7ec7',
  // tipo_contribuyente: Persona Física (codigo=1)
  TIPO_CONTRIBUYENTE_ID: null as string | null,
  // unidades_de_medida: Unidad (codigo=77)
  UNIDAD_MEDIDA_ID: '4f6a1318-1a42-454d-874e-a72f6cda04df',

  // ── Producto genérico para ítems migrados (por empresa) ──────
  COD_PRODUCTO_GENERICO: 'GEN0001',

  // ── IVA ───────────────────────────────────────────────────────
  IVA_TASA: 10, // % — todos los ítems migrados tienen IVA 10%

  // ── Crédito por defecto para clientes migrados ───────────────
  LIMITE_CREDITO_DEFAULT: 0,

  // ── Marca de idempotencia (propia de esta migración NICO) ────
  MIGRADO_MARKER: 'MIGRADO_ACCESS_NICO',

  // ══════════════════════════════════════════════════════════════
  //  FILTROS DE IMPORTACIÓN
  // ══════════════════════════════════════════════════════════════
  FILTROS: {
    // ── ÚNICA diferencia con migrate.ts ────────────────────────
    // Solo se importan filas cuyo "Articulo" CONTENGA alguna de estas
    // palabras (case-insensitive, substring). Las demás se omiten.
    ARTICULO_CONTIENE: ['NICO'],

    // ── Columna "Forma de pago" ────────────────────────────────
    FORMA_PAGO_PERMITIDAS: ['Crédito', 'Precio Contado', 'Precio contado'],

    // ── Columna "Tipo de cliente" ──────────────────────────────
    TIPO_CLIENTE_PERMITIDOS: ['Mensual', 'Quincenal', 'Semanal', 'Cliente Casual'],

    // ── Exclusiones explícitas (corren antes del filtro de permitidas) ──
    EXCLUIR_FORMA_PAGO: ['ANULADO', 'Cancelado', 'Cancelado++', 'Clavos', 'Informconf', 'TRANSFERIDO', 'Retirado'],
  },
};

// ══════════════════════════════════════════════════════════════════
//  HELPERS
// ══════════════════════════════════════════════════════════════════

/** Lee una columna de forma case-insensitive, probando varios nombres. */
function getCol(row: any, ...names: string[]): any {
  for (const name of names) {
    if (row[name] !== undefined && row[name] !== null && row[name] !== '') return row[name];
  }
  const norm = (s: string) => s.toLowerCase().replace(/\s+/g, ' ').trim();
  for (const name of names) {
    const t = norm(name);
    const k = Object.keys(row).find((k2) => norm(k2) === t);
    if (k !== undefined) return row[k];
  }
  return undefined;
}

/** Convierte valor de celda Excel a Date. Soporta número, string, Date. */
function toDate(val: any): Date {
  if (!val) return new Date();
  if (val instanceof Date) return val;
  if (typeof val === 'number') {
    // Excel: días desde 1900-01-01 (con bug de año bisiesto 1900)
    return new Date(Date.UTC(1899, 11, 30 + val));
  }
  const d = new Date(val);
  return isNaN(d.getTime()) ? new Date() : d;
}

/** Agrega N días a una fecha */
function addDays(base: Date, days: number): Date {
  const d = new Date(base);
  d.setUTCDate(d.getUTCDate() + days);
  return d;
}

/** Intervalo en días según tipo de cliente del Excel */
function intervaloDias(tipo: string): number {
  const t = (tipo || '').toLowerCase().trim();
  if (t === 'semanal') return 7;
  if (t === 'quincenal') return 14;
  return 30; // mensual (por defecto)
}

/**
 * Cálculo IVA 10% — precio ya incluye IVA (sistema paraguayo).
 *   base_gravada = round(importe / 1.10)
 *   iva          = importe - base_gravada
 */
function calcIva10(importe: number) {
  const baseGravada = Math.round(importe / 1.1);
  const iva = importe - baseGravada;
  return { baseGravada, iva };
}

/** Normaliza cédula: elimina puntos, guiones y espacios */
function normCedula(v: any): string {
  return String(v || '')
    .replace(/[\s.-]/g, '')
    .trim();
}

/**
 * Verifica si una fila del Excel debe ser importada según los filtros del CONFIG.
 * Retorna null si pasa el filtro, o un string con el motivo de exclusión.
 */
function motivoExclusion(row: any): string | null {
  // ── ÚNICA diferencia con migrate.ts: Articulo debe contener "NICO" ──
  const articulo = String(getCol(row, 'Articulo', 'Artículo') || '')
    .trim()
    .toLowerCase();
  const contieneNico = CONFIG.FILTROS.ARTICULO_CONTIENE.some((p) => articulo.includes(p.toLowerCase()));
  if (!contieneNico) {
    return `Artículo no contiene ${CONFIG.FILTROS.ARTICULO_CONTIENE.join('/')}: "${getCol(row, 'Articulo', 'Artículo') || ''}"`;
  }

  const formaPago = String(getCol(row, 'Forma de pago') || '')
    .trim()
    .toLowerCase();
  const tipoCliente = String(getCol(row, 'Tipo de cliente') || '')
    .trim()
    .toLowerCase();

  // Exclusiones explícitas de Forma de pago
  if (CONFIG.FILTROS.EXCLUIR_FORMA_PAGO.map((v) => v.toLowerCase()).includes(formaPago)) {
    return `Forma de pago excluida: "${getCol(row, 'Forma de pago')}"`;
  }

  // Forma de pago debe estar en la lista de permitidas
  if (!CONFIG.FILTROS.FORMA_PAGO_PERMITIDAS.map((v) => v.toLowerCase()).includes(formaPago)) {
    return `Forma de pago no permitida: "${getCol(row, 'Forma de pago')}"`;
  }

  // Tipo de cliente debe estar en la lista de permitidos
  if (!CONFIG.FILTROS.TIPO_CLIENTE_PERMITIDOS.map((v) => v.toLowerCase()).includes(tipoCliente)) {
    return `Tipo de cliente no permitido: "${getCol(row, 'Tipo de cliente')}"`;
  }

  return null; // ← pasa el filtro
}

/** Padding de número de documento a 7 dígitos */
function fmtDoc(n: number): string {
  return String(n).padStart(7, '0');
}

// ══════════════════════════════════════════════════════════════════
//  LOGGER — escribe simultáneamente a consola y a archivo
// ══════════════════════════════════════════════════════════════════

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

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

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

// Accesos rápidos
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 title = (m: string) => log('TITLE', m);
const raw = (m: string) => log('RAW', m);

// ══════════════════════════════════════════════════════════════════
//  MAIN
// ══════════════════════════════════════════════════════════════════
const prisma = new PrismaClient();
// Los Excel viven en ESTA misma carpeta (cliente-niko).
const DIR = __dirname;
const DRY_RUN = process.argv.includes('--dry');
const IDS_ARG = process.argv.find((a) => a.startsWith('--ids='));
const FILTER_IDS: Set<string> | null = IDS_ARG
  ? new Set(
      IDS_ARG.replace('--ids=', '')
        .split(',')
        .map((s) => s.trim())
        .filter(Boolean),
    )
  : null;

async function run() {
  // ── Validar empresa destino ────────────────────────────────────
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
  if (!CONFIG.EMPRESA_ID || !UUID_RE.test(CONFIG.EMPRESA_ID)) {
    console.error('ERROR: Falta la empresa destino o el UUID es inválido.');
    console.error('       Pasala con  --empresa=<uuid>  o editá CONFIG.EMPRESA_ID.');
    process.exit(1);
  }

  raw('');
  raw('╔══════════════════════════════════════════════════╗');
  raw(`║  MIGRACIÓN ACCESS·NICO → Novasis  ${DRY_RUN ? '[ DRY RUN ]  ' : '[ ESCRIBE ]  '}║`);
  if (FILTER_IDS) raw(`║  Filtrando IDs: ${[...FILTER_IDS].join(', ').substring(0, 32).padEnd(32)}║`);
  raw(`║  Empresa: ${CONFIG.EMPRESA_ID.padEnd(37)}║`);
  raw(`║  Log: ${path.basename(LOG_FILE).padEnd(41)}║`);
  raw('╚══════════════════════════════════════════════════╝');
  raw('');

  // ── Verificar que la empresa exista ────────────────────────────
  const empresa = await prisma.empresas.findUnique({
    where: { id: CONFIG.EMPRESA_ID },
    select: { id: true, razon_social: true },
  });
  if (!empresa) {
    console.error(`ERROR: No existe empresa con id = '${CONFIG.EMPRESA_ID}'.`);
    process.exit(1);
  }
  info(`Empresa destino        : ${empresa.razon_social} (${empresa.id})`);

  // ── Buscar producto genérico (GEN0001) DE ESTA EMPRESA ────────
  const productoGenerico = await prisma.productos.findFirst({
    where: { cod_producto: CONFIG.COD_PRODUCTO_GENERICO, empresa_id: CONFIG.EMPRESA_ID },
    select: { id: true, descripcion: true },
  });
  if (!productoGenerico) {
    console.error(
      `ERROR: No se encontró producto con cod_producto = '${CONFIG.COD_PRODUCTO_GENERICO}' en la empresa destino.`,
    );
    console.error('       Crear el producto en el sistema o ajustar CONFIG.COD_PRODUCTO_GENERICO.');
    process.exit(1);
  }
  info(`Producto genérico      : ${productoGenerico.descripcion} (${productoGenerico.id})`);

  // ── Leer archivos Excel ────────────────────────────────────────
  const wbCli = XLSX.readFile(path.join(DIR, 'clientes.xlsx'));
  const wbFact = XLSX.readFile(path.join(DIR, 'facturas.xlsx'));

  const rowsClientes: any[] = XLSX.utils.sheet_to_json(wbCli.Sheets[wbCli.SheetNames[0]]);
  const rowsFacturas: any[] = XLSX.utils.sheet_to_json(wbFact.Sheets[wbFact.SheetNames[0]]);

  info(`Filas en clientes.xlsx : ${rowsClientes.length}`);
  info(`Filas en facturas.xlsx : ${rowsFacturas.length}`);

  // ── Indexar pagos por Id de venta ─────────────────────────────
  const pagosPorVenta = new Map<string, any[]>();
  for (const f of rowsFacturas) {
    const key = String(getCol(f, 'Id') ?? '').trim();
    if (!pagosPorVenta.has(key)) pagosPorVenta.set(key, []);
    pagosPorVenta.get(key).push(f);
  }
  // Ordenar cada grupo por fecha asc para identificar el último pago
  for (const pagos of pagosPorVenta.values()) {
    pagos.sort(
      (a, b) =>
        toDate(getCol(a, 'Fecha facturación', 'Fecha Facturación')).getTime() -
        toDate(getCol(b, 'Fecha facturación', 'Fecha Facturación')).getTime(),
    );
  }

  // ── Analizar y filtrar filas ───────────────────────────────────
  const motivosExclusion = new Map<string, number>(); // motivo → cantidad
  const filasValidas: any[] = [];

  for (const c of rowsClientes) {
    const motivo = motivoExclusion(c);
    if (motivo) {
      const key = motivo.replace(/:.+$/, ''); // solo la parte descriptiva
      motivosExclusion.set(key, (motivosExclusion.get(key) ?? 0) + 1);
    } else {
      const saldoFila = Number(getCol(c, 'Saldo') || 0);
      const importeFila = Number(getCol(c, 'Importe Total', 'Importe total', 'Importe_Total') || 0);
      if (saldoFila > 0 && importeFila > 0) {
        filasValidas.push(c);
      } else {
        const key = 'Saldo = 0 o sin importe';
        motivosExclusion.set(key, (motivosExclusion.get(key) ?? 0) + 1);
      }
    }
  }

  // ── Deduplicar clientes por cédula (solo de filas válidas) ────
  const sortedValidas = [...filasValidas].sort((a, b) => Number(getCol(a, 'Id')) - Number(getCol(b, 'Id')));
  const clientePorCedula = new Map<string, any>();
  for (const c of sortedValidas) {
    const ced = normCedula(getCol(c, 'Cedula', 'Cédula'));
    if (ced && !clientePorCedula.has(ced)) {
      clientePorCedula.set(ced, c);
    }
  }

  title('━━━ ANÁLISIS DE DATOS ━━━');
  info(`Filas totales en Excel           : ${rowsClientes.length}`);
  info(`Filas válidas para importar      : ${filasValidas.length}`);
  info(`Clientes únicos (por cédula)     : ${clientePorCedula.size}`);
  raw('');
  info('Filas excluidas por motivo:');
  for (const [motivo, cant] of motivosExclusion) {
    skip(`  ${String(cant).padStart(5)}  ${motivo}`);
  }
  raw('');

  // ── Mapas de IDs ─────────────────────────────────────────────
  const erpClienteId = new Map<string, string>(); // cedula → cliente.id
  const erpClienteDirId = new Map<string, string>(); // cedula → cliente_direcciones.id
  const cobradorIdMap = new Map<string, string>(); // nombre_normalizado → vendedores_cobradores.id

  // ══════════════════════════════════════════════════════════════
  //  PASO 0 — Crear/encontrar cobradores únicos (columna "Vendedora")
  // ══════════════════════════════════════════════════════════════
  title('━━━ PASO 0: Cobradores (desde "Vendedora") ━━━');

  // Solo cobradores de las filas válidas (NICO) que realmente se importan.
  const nombresCobradores = new Set<string>();
  for (const c of filasValidas) {
    const nombre = String(getCol(c, 'Vendedora', 'Cobrador') || '').trim();
    if (nombre) nombresCobradores.add(nombre.toUpperCase());
  }
  info(`Cobradores únicos encontrados en Excel: ${nombresCobradores.size}`);

  if (!DRY_RUN) {
    const existentesCobradores = await prisma.vendedores_cobradores.findMany({
      where: { empresa_id: CONFIG.EMPRESA_ID, codigo: { startsWith: 'COB' } },
      select: { codigo: true, nombre: true, apellido: true, id: true },
    });
    for (const vc of existentesCobradores) {
      const nombreCompleto = `${vc.nombre}${vc.apellido ? ' ' + vc.apellido : ''}`.toUpperCase();
      cobradorIdMap.set(nombreCompleto, vc.id);
    }

    let cobradorCounter =
      existentesCobradores.reduce((max, vc) => {
        const num = parseInt((vc.codigo || '').replace('COB', '') || '0', 10);
        return num > max ? num : max;
      }, 0) + 1;

    for (const nombreCompleto of nombresCobradores) {
      if (cobradorIdMap.has(nombreCompleto)) {
        skip(`Cobrador ya existe: ${nombreCompleto} → id: ${cobradorIdMap.get(nombreCompleto)}`);
        continue;
      }
      const partes = nombreCompleto.split(' ');
      const nombre = partes[0];
      const apellido = partes.slice(1).join(' ') || null;
      const codigo = `COB${String(cobradorCounter).padStart(3, '0')}`;
      cobradorCounter++;

      const cobrador = await prisma.vendedores_cobradores.create({
        data: {
          empresa_id: CONFIG.EMPRESA_ID,
          tipo: 'cobrador',
          codigo,
          nombre,
          apellido,
          active: true,
        },
      });
      cobradorIdMap.set(nombreCompleto, cobrador.id);
      ok(`Cobrador creado: ${codigo} | ${nombreCompleto} → id: ${cobrador.id}`);
    }
  } else {
    let i = 1;
    for (const nombre of nombresCobradores) {
      info(`[DRY] Cobrador COB${String(i).padStart(3, '0')}: ${nombre}`);
      cobradorIdMap.set(nombre, `fake-cob-${i}`);
      i++;
    }
  }
  raw('');

  // ══════════════════════════════════════════════════════════════
  //  PASO 1 — Crear personas + clientes únicos
  // ══════════════════════════════════════════════════════════════
  title('━━━ PASO 1: Personas y Clientes ━━━');

  let cliCreados = 0;
  let cliExistentes = 0;
  const erroresPaso1: string[] = [];

  for (const [cedula, c] of clientePorCedula) {
    const razonSocial = String(getCol(c, 'Nombre') || '').trim() || `Cliente ${cedula}`;
    const telefonoRaw = String(getCol(c, 'Teléfono 1', 'Teléfono', 'Telefono', 'Teléfono1') || '').trim();
    const telefono = telefonoRaw.slice(0, 15) || null;
    const celular = telefonoRaw.slice(0, 20) || null;
    const direccion = String(getCol(c, 'Dirección', 'Direccion') || '').trim() || null;

    const nombreCobradorRaw = String(getCol(c, 'Vendedora', 'Cobrador') || '')
      .trim()
      .toUpperCase();
    const cobradorId = nombreCobradorRaw ? (cobradorIdMap.get(nombreCobradorRaw) ?? null) : null;

    if (DRY_RUN) {
      info(`[DRY] ${razonSocial.padEnd(35)} CI: ${cedula} | Vendedora: ${nombreCobradorRaw || '(sin vendedora)'}`);
      erpClienteId.set(cedula, `fake-${cedula}`);
      erpClienteDirId.set(cedula, `fake-dir-${cedula}`);
      cliCreados++;
      continue;
    }

    try {
      // ── Buscar o crear persona ──────────────────────────────
      let persona = await prisma.personas.findFirst({
        where: { empresa_id: CONFIG.EMPRESA_ID, nro_documento: cedula, deleted: false },
      });

      if (!persona) {
        persona = await prisma.personas.create({
          data: {
            empresa_id: CONFIG.EMPRESA_ID,
            razon_social: razonSocial,
            ruc: null,
            nro_documento: cedula,
            tipo_documento_id: CONFIG.TIPO_DOCUMENTO_ID,
            naturaleza_id: CONFIG.NATURALEZA_ID,
            tipo_contribuyente_id: CONFIG.TIPO_CONTRIBUYENTE_ID,
            telefono,
            celular,
            direccion,
            active: true,
            deleted: false,
          },
        });
      }

      // ── Buscar o crear cliente ──────────────────────────────
      let cliente = await prisma.clientes.findFirst({
        where: { persona_id: persona.id, deleted: false },
      });

      if (!cliente) {
        cliente = await prisma.clientes.create({
          data: {
            persona_id: persona.id,
            tipo_operacion_id: CONFIG.TIPO_OPERACION_ID,
            referencia_domicilio: direccion,
            limite_credito: CONFIG.LIMITE_CREDITO_DEFAULT,
            active: true,
            deleted: false,
          },
        });
        cliCreados++;
        ok(`Creado  | ${razonSocial.padEnd(35)} CI: ${cedula} | id: ${cliente.id}`);
      } else {
        cliExistentes++;
        skip(`Existe  | ${razonSocial.padEnd(35)} CI: ${cedula} | id: ${cliente.id}`);
      }

      erpClienteId.set(cedula, cliente.id);

      // ── Buscar o crear cliente_direcciones principal ────────
      let clienteDir = await prisma.cliente_direcciones.findFirst({
        where: { cliente_id: cliente.id, empresa_id: CONFIG.EMPRESA_ID },
        select: { id: true },
      });
      if (!clienteDir) {
        clienteDir = await prisma.cliente_direcciones.create({
          data: {
            cliente_id: cliente.id,
            empresa_id: CONFIG.EMPRESA_ID,
            label: 'Principal',
            direccion: direccion,
            es_principal: true,
            cobrador_id: cobradorId,
          },
          select: { id: true },
        });
        if (cobradorId) {
          ok(`  Dir+Cobrador | ${razonSocial.padEnd(30)} → cobrador: ${nombreCobradorRaw}`);
        }
      }
      erpClienteDirId.set(cedula, clienteDir.id);
    } catch (e: any) {
      const msg = `CI: ${cedula} | Nombre: ${razonSocial} | ERROR: ${e.message}`;
      erroresPaso1.push(msg);
      err(msg);
    }
  }

  raw('');
  info(
    `→ Paso 1 completo — Creados: ${cliCreados}  |  Ya existían: ${cliExistentes}  |  Errores: ${erroresPaso1.length}`,
  );

  // ══════════════════════════════════════════════════════════════
  //  PASO 2 — Facturas + detalles + subtotales + CxC + cuotas
  // ══════════════════════════════════════════════════════════════
  title('━━━ PASO 2: Facturas, CxC y Cuotas ━━━');

  // ── Cargar IDs de ventas ya migradas (idempotencia) ───────────
  const ventasYaMigradas = new Set<string>();
  if (!DRY_RUN) {
    const migradas = await prisma.factura_cab.findMany({
      where: {
        empresa_id: CONFIG.EMPRESA_ID,
        dinfadic: { contains: CONFIG.MIGRADO_MARKER },
      },
      select: { dinfadic: true },
    });
    for (const f of migradas) {
      const match = (f.dinfadic || '').match(/Id:(\w+)/);
      if (match) ventasYaMigradas.add(match[1]);
    }
    info(`Ventas ya migradas en BD (se saltarán) : ${ventasYaMigradas.size}`);
  }

  // Determinar el próximo número de documento basado en los ya migrados
  let docCounter = 1;
  if (!DRY_RUN && ventasYaMigradas.size > 0) {
    const ultimaFact = await prisma.factura_cab.findFirst({
      where: { empresa_id: CONFIG.EMPRESA_ID, dinfadic: { contains: CONFIG.MIGRADO_MARKER } },
      orderBy: { dnumdoc: 'desc' },
      select: { dnumdoc: true },
    });
    if (ultimaFact?.dnumdoc) {
      docCounter = parseInt(ultimaFact.dnumdoc, 10) + 1;
      info(`Continuando numeración desde : ${fmtDoc(docCounter)}`);
    }
  }

  let factCreadas = 0;
  let factSaltadas = 0;
  let cuotasCreadas = 0;
  let omitidas = 0;
  const erroresPaso2: string[] = [];
  const sinRegistrosFactura: string[] = [];

  // filasValidas ya pasó todos los filtros (incl. NICO) y tiene saldo > 0
  for (const c of filasValidas) {
    const ventaId = String(getCol(c, 'Id') ?? '').trim();
    const cedula = normCedula(getCol(c, 'Cedula', 'Cédula'));
    const saldo = Number(getCol(c, 'Saldo') || 0);
    const importeTotal = Number(getCol(c, 'Importe Total', 'Importe total', 'Importe_Total') || 0);
    const articulo = String(getCol(c, 'Articulo', 'Artículo') || 'Producto migrado').trim();
    const tipo = String(getCol(c, 'Tipo de cliente') || 'Mensual').trim();
    const fechaVenta = toDate(getCol(c, 'Fecha de registro'));

    // ── Filtro por --ids (para pruebas) ───────────────────────────
    if (FILTER_IDS && !FILTER_IDS.has(ventaId)) continue;

    // ── Idempotencia: saltar si esta venta ya fue migrada ──────
    if (!DRY_RUN && ventasYaMigradas.has(ventaId)) {
      skip(`Saltada | Venta ${ventaId} ya existe en BD`);
      factSaltadas++;
      continue;
    }

    const clienteId = erpClienteId.get(cedula);
    if (!clienteId) {
      const msg = `Venta ${ventaId} | CI: ${cedula} | Nombre: ${getCol(c, 'Nombre')} | ERROR: cliente no encontrado en Paso 1 (cédula no procesada)`;
      erroresPaso2.push(msg);
      err(msg);
      omitidas++;
      continue;
    }

    // ── Obtener historial y último pago con abono real ─────────
    const historial = pagosPorVenta.get(ventaId) || [];
    const pagosConAbono = historial.filter((p) => Number(getCol(p, 'Abono1') || 0) > 0);
    const ultimoPago = pagosConAbono.length > 0 ? pagosConAbono[pagosConAbono.length - 1] : null;

    // ── Monto de cuota ─────────────────────────────────────────
    let montoCuota = 0;

    if (pagosConAbono.length === 0) {
      sinRegistrosFactura.push(ventaId);
      warn(
        `SIN_REGISTROS | VentaId:${ventaId} | CI:${cedula} | ${articulo.substring(0, 28)} | Saldo:${saldo.toLocaleString('es-PY')} → revisión manual`,
      );
      omitidas++;
      continue;
    } else if (pagosConAbono.length === 1) {
      montoCuota = Math.round(Number(getCol(pagosConAbono[0], 'Abono1') || 0));
    } else {
      const frecuencia = new Map<number, number>();
      for (const p of pagosConAbono) {
        const v = Math.round(Number(getCol(p, 'Abono1') || 0));
        if (v > 0) frecuencia.set(v, (frecuencia.get(v) || 0) + 1);
      }
      const sorted = [...frecuencia.entries()].sort((a, b) => b[1] - a[1] || b[0] - a[0]);
      const maxFrecuencia = sorted[0]?.[1] ?? 1;
      if (maxFrecuencia === 1) {
        montoCuota = Math.round(Number(getCol(pagosConAbono[0], 'Abono1') || 0));
      } else {
        montoCuota = sorted[0][0];
      }
    }

    if (montoCuota <= 0) {
      const msg = `Venta ${ventaId} | CI: ${cedula} | Artículo: ${articulo} | OMITIDA: monto de cuota calculado = 0`;
      erroresPaso2.push(msg);
      warn(msg);
      omitidas++;
      continue;
    }

    // ── Calcular cuotas y fechas ────────────────────────────────
    const intervalo = intervaloDias(tipo);
    const cantCuotasBase = Math.max(1, Math.floor(saldo / montoCuota));
    const residuo = saldo - cantCuotasBase * montoCuota;
    const cantCuotas = residuo > 0 ? cantCuotasBase + 1 : cantCuotasBase;

    const baseVencimiento = ultimoPago
      ? toDate(getCol(ultimoPago, 'Fecha facturación', 'Fecha Facturación', 'Fecha de facturación'))
      : fechaVenta;
    const primeraCuota = addDays(baseVencimiento, intervalo);
    const ultimaCuota = addDays(primeraCuota, intervalo * (cantCuotas - 1));

    // ── Cálculos IVA 10% ────────────────────────────────────────
    const { baseGravada, iva } = calcIva10(importeTotal);

    const dnumdoc = fmtDoc(docCounter++);

    info(
      `Venta ${ventaId.padEnd(6)} | CI ${cedula.padEnd(10)} | ${articulo.substring(0, 22).padEnd(22)} | ` +
        `Total: ${importeTotal.toLocaleString('es-PY').padStart(13)} | Saldo: ${saldo.toLocaleString('es-PY').padStart(13)} | ` +
        `${cantCuotas} cuotas x ${montoCuota.toLocaleString('es-PY')} [${tipo}]`,
    );

    if (DRY_RUN) {
      factCreadas++;
      cuotasCreadas += cantCuotas;
      continue;
    }

    try {
      await prisma.$transaction(async (tx) => {
        // ── 1. factura_cab ─────────────────────────────────────
        const factura = await tx.factura_cab.create({
          data: {
            empresa_id: CONFIG.EMPRESA_ID,
            cliente_id: clienteId,
            dest: CONFIG.DEST,
            dpunexp: CONFIG.DPUNEXP,
            dnumdoc: dnumdoc,
            dfeemide: fechaVenta,
            estado: 'Aprobado',
            icondcred: 2,
            dcuotas: cantCuotas,
            condicion_operacion_id: CONFIG.CONDICION_CREDITO_ID,
            tipo_transaccion_id: CONFIG.TIPO_TRANSACCION_ID,
            indicador_presencia_id: CONFIG.INDICADOR_PRESENCIA_ID,
            tipo_impuesto_id: CONFIG.TIPO_IMPUESTO_ID,
            moneda_id: CONFIG.MONEDA_ID,
            dinfadic: `${CONFIG.MIGRADO_MARKER} | Id:${ventaId} | Tipo:${tipo} | CI:${cedula}`,
          },
        });

        // ── 2. factura_det ─────────────────────────────────────
        await tx.factura_det.create({
          data: {
            factura_cab_id: factura.id,
            producto_id: productoGenerico.id,
            ddesproser: articulo,
            cunimed: 'UNI',
            dcantproser: 1,
            duniproser: importeTotal,
            dtotbruopeitem: importeTotal,
            dtotopeitem: importeTotal,
            iafeciva: 1,
            dpropiva: 100,
            dtasiva: CONFIG.IVA_TASA,
            dbasgraviva: baseGravada,
            dliqivaitem: iva,
            item: 1,
          },
        });

        // ── 3. factura_subtotales ──────────────────────────────
        await tx.factura_subtotales.create({
          data: {
            factura_cab_id: factura.id,
            dsub10: importeTotal,
            dtotope: importeTotal,
            dtotgralope: importeTotal,
            dbasegrav10: baseGravada,
            diva10: iva,
            dliqtotiva10: iva,
            dtbasgraiva: baseGravada,
            dtotiva: iva,
            dtotdesc: 0,
            dtotdescglotem: 0,
            dtotantitem: 0,
            dtotant: 0,
            dporcdesctotal: 0,
            danticipo: 0,
            dredon: 0,
            dcomi: 0,
          },
        });

        // ── 4. cuentas_cobrar ──────────────────────────────────
        const cuenta = await tx.cuentas_cobrar.create({
          data: {
            empresa_id: CONFIG.EMPRESA_ID,
            cliente_id: clienteId,
            factura_venta_id: factura.id,
            fecha_emision: fechaVenta,
            fecha_vencimiento: ultimaCuota,
            moneda_iso: CONFIG.MONEDA_ISO,
            monto_total: importeTotal,
            saldo_pendiente: saldo,
            estado: 'pendiente',
          },
        });

        // ── 5. factura_cuotas ──────────────────────────────────
        await tx.factura_cuotas.createMany({
          data: Array.from({ length: cantCuotas }, (_, i) => {
            const esUltima = residuo > 0 && i === cantCuotas - 1;
            const monto = esUltima ? residuo : montoCuota;
            return {
              factura_cab_id: factura.id,
              cuenta_id: cuenta.id,
              nro_cuota: i + 1,
              cmonecuo: CONFIG.MONEDA_ISO,
              dmoncuota: monto,
              saldo_pendiente: monto,
              dvenccuo: addDays(primeraCuota, intervalo * i),
              estado: 'pendiente',
            };
          }),
        });

        // ── 6. Actualizar saldo_pendiente del cliente ──────────
        await tx.clientes.update({
          where: { id: clienteId },
          data: {
            saldo_pendiente: { increment: saldo },
            fecha_ultimo_pago: ultimoPago
              ? toDate(getCol(ultimoPago, 'Fecha facturación', 'Fecha Facturación'))
              : undefined,
          },
        });

        factCreadas++;
        cuotasCreadas += cantCuotas;
        ok(`Migrado | Venta ${ventaId} → factura_cab: ${factura.id} | ${cantCuotas} cuotas`);
      });
    } catch (e: any) {
      const msg = `Venta ${ventaId} | CI: ${cedula} | Artículo: ${articulo} | Total: ${importeTotal} | Saldo: ${saldo} | ERROR DB: ${e.message}`;
      erroresPaso2.push(msg);
      err(msg);
    }
  }

  // ══════════════════════════════════════════════════════════════
  //  RESUMEN FINAL
  // ══════════════════════════════════════════════════════════════
  const totalErrores = erroresPaso1.length + erroresPaso2.length;

  raw('');
  raw('╔══════════════════════════════════════════════════════╗');
  raw('║                RESUMEN FINAL (NICO)                 ║');
  raw('╠══════════════════════════════════════════════════════╣');
  raw(`║  Filas en Excel (clientes)    : ${String(rowsClientes.length).padStart(6)}               ║`);
  raw(`║  Filas válidas importadas     : ${String(filasValidas.length).padStart(6)}               ║`);
  raw(
    `║  Filas omitidas/excluidas     : ${String(rowsClientes.length - filasValidas.length).padStart(6)}               ║`,
  );
  raw('║──────────────────────────────────────────────────────║');
  raw(`║  Clientes creados             : ${String(cliCreados).padStart(6)}               ║`);
  raw(`║  Clientes ya existentes       : ${String(cliExistentes).padStart(6)}               ║`);
  raw(`║  Facturas migradas (nuevas)   : ${String(factCreadas).padStart(6)}               ║`);
  raw(`║  Facturas saltadas (ya exist) : ${String(factSaltadas).padStart(6)}               ║`);
  raw(`║  Cuotas generadas             : ${String(cuotasCreadas).padStart(6)}               ║`);
  raw(`║  Ventas omitidas (sin datos)  : ${String(omitidas).padStart(6)}               ║`);
  raw('║──────────────────────────────────────────────────────║');
  raw(`║  Errores Paso 1 (clientes)    : ${String(erroresPaso1.length).padStart(6)}               ║`);
  raw(`║  Errores Paso 2 (facturas)    : ${String(erroresPaso2.length).padStart(6)}               ║`);
  raw(`║  ERRORES TOTALES              : ${String(totalErrores).padStart(6)}               ║`);
  raw('╚══════════════════════════════════════════════════════╝');

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

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

  if (sinRegistrosFactura.length > 0) {
    raw('');
    raw('── SIN REGISTROS EN facturas.xlsx (revisión manual) ─');
    raw(`   Total: ${sinRegistrosFactura.length} ventas sin historial de pagos`);
    raw('   IDs para revisar manualmente:');
    sinRegistrosFactura.forEach((id, i) => raw(`  [${String(i + 1).padStart(3)}] VentaId: ${id}`));
  }

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

  logStream.end();
  await prisma.$disconnect();
}

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