/**
 * ╔══════════════════════════════════════════════════════════════╗
 * ║        SCRIPT DE MIGRACIÓN: Access → Novasis ERP          ║
 * ╠══════════════════════════════════════════════════════════════╣
 * ║  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. npm install xlsx  (solo la primera vez)                  ║
 * ║  2. Colocar clientes.xlsx y facturas.xlsx en esta carpeta    ║
 * ║  3. Ajustar la sección CONFIG si es necesario                ║
 * ║  4. Dry-run:  npx ts-node scripts/migrate-access/migrate.ts --dry  ║
 * ║  5. Real:     npx ts-node scripts/migrate-access/migrate.ts        ║
 * ╚══════════════════════════════════════════════════════════════╝
 *
 * Columnas esperadas en clientes.xlsx:
 *   Id | Nombre | Articulo | Cedula | Fecha de registro | Forma de pago |
 *   Tipo de cliente | Direccion | Telefono | Importe total | Saldo | Cobrador
 *
 * 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 — valores por defecto ya completados con IDs de la BD
//  Solo cambiar si se migra a otra empresa o entorno diferente
// ══════════════════════════════════════════════════════════════════
const CONFIG = {
  // ── Empresa destino ───────────────────────────────────────────
  EMPRESA_ID: '7d3ce403-9487-4289-b21f-2e5d3477f5b5',

  // ── Numeración de las facturas migradas ───────────────────────
  // Se usará una serie especial para identificar facturas migradas
  DEST: '111',
  DPUNEXP: '111',
  SERIE: 'MI', // prefijo visible en dinfadic

  // ── IDs de referenciales (ya consultados en la BD) ───────────
  // 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,
  // unidades_de_medida: Unidad (codigo=77)
  UNIDAD_MEDIDA_ID: '4f6a1318-1a42-454d-874e-a72f6cda04df',

  // ── Producto genérico para ítems migrados ────────────────────
  // Se busca automáticamente por cod_producto = 'GEN0001'.
  // Si en tu BD el código es diferente, cambiarlo aquí.
  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, // en Gs. (0 = sin límite configurado)

  // ══════════════════════════════════════════════════════════════
  //  FILTROS DE IMPORTACIÓN
  //  Controlan qué filas del Excel se procesan.
  //  Comparación case-insensitive y con trim automático.
  // ══════════════════════════════════════════════════════════════

  FILTROS: {
    // ── Columna "Forma de pago" ────────────────────────────────
    // Solo se importan filas cuyo valor esté en esta lista.
    // Agregar o quitar según lo que muestre el Excel.
    // Valores permitidos. '' (vacío) = crédito por defecto.
    // Agregar o quitar según lo que muestre el Excel.
    FORMA_PAGO_PERMITIDAS: ['Crédito', 'Precio Contado', 'Precio contado'],

    // ── Columna "Tipo de cliente" ──────────────────────────────
    // Solo se importan filas cuyo valor esté en esta lista.
    TIPO_CLIENTE_PERMITIDOS: ['Mensual', 'Quincenal', 'Semanal', 'Cliente Casual'],

    // ── Exclusiones explícitas (corren antes del filtro de permitidas) ──
    // Filas con estos valores en "Forma de pago" se descartan siempre.
    EXCLUIR_FORMA_PAGO: [
      'ANULADO',
      'Cancelado',
      'Cancelado++',
      'Clavos',
      'Informconf',
      'TRANSFERIDO',
      'Retirado',
      'Clavos',
    ],
  },
};

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

/** 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 {
  const formaPago = String(row['Forma de pago'] || '')
    .trim()
    .toLowerCase();
  const tipoCliente = String(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: "${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: "${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: "${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-${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();
const DIR = path.resolve(__dirname, '../../data_exportar');
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() {
  raw('');
  raw('╔══════════════════════════════════════════════════╗');
  raw(`║  MIGRACIÓN ACCESS → Novasis  ${DRY_RUN ? '[ DRY RUN ]     ' : '[ REAL — ESCRIBE ]'} ║`);
  if (FILTER_IDS) raw(`║  Filtrando IDs: ${[...FILTER_IDS].join(', ').substring(0, 32).padEnd(32)}║`);
  raw(`║  Log: ${path.basename(LOG_FILE).padEnd(41)}║`);
  raw('╚══════════════════════════════════════════════════╝');
  raw('');

  // ── Buscar producto genérico (GEN0001) ────────────────────────
  const productoGenerico = await prisma.productos.findFirst({
    where: { cod_producto: CONFIG.COD_PRODUCTO_GENERICO },
    select: { id: true, descripcion: true },
  });
  if (!productoGenerico) {
    console.error(`ERROR: No se encontró producto con cod_producto = '${CONFIG.COD_PRODUCTO_GENERICO}'.`);
    console.error('       Crear el producto en el sistema o ajustar CONFIG.COD_PRODUCTO_GENERICO.');
    process.exit(1);
  }
  console.log(`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}`);

  // Helper de columna case-insensitive (disponible antes del loop)
  const colVal = (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;
  };

  // ── Indexar pagos por Id de venta ─────────────────────────────
  const pagosPorVenta = new Map<string, any[]>();
  for (const f of rowsFacturas) {
    const key = String(colVal(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(colVal(a, 'Fecha facturación', 'Fecha Facturación')).getTime() -
        toDate(colVal(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) {
      // Agrupar por motivo para el reporte
      const key = motivo.replace(/:.+$/, ''); // solo la parte descriptiva
      motivosExclusion.set(key, (motivosExclusion.get(key) ?? 0) + 1);
    } else {
      const saldoFila = Number(colVal(c, 'Saldo') || 0);
      const importeFila = Number(colVal(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) ────
  // De existir duplicados, tomamos el primer registro (más antiguo por Id).
  const sortedValidas = [...filasValidas].sort((a, b) => Number(a['Id']) - Number(b['Id']));
  const clientePorCedula = new Map<string, any>();
  for (const c of sortedValidas) {
    const ced = normCedula(c['Cedula']);
    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
  // ══════════════════════════════════════════════════════════════
  title('━━━ PASO 0: Cobradores ━━━');

  // Recopilar nombres únicos no vacíos de la columna "Cobrador"
  const nombresCobradores = new Set<string>();
  for (const c of rowsClientes) {
    const nombre = String(colVal(c, 'Cobrador') || '').trim();
    if (nombre) nombresCobradores.add(nombre.toUpperCase());
  }
  info(`Cobradores únicos encontrados en Excel: ${nombresCobradores.size}`);

  if (!DRY_RUN) {
    // Determinar próximo código disponible buscando los ya existentes
    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 },
    });
    // Mapear los ya existentes por nombre completo para idempotencia
    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(c['Nombre'] || '').trim() || `Cliente ${cedula}`;
    const telefono =
      String(c['Teléfono'] || '')
        .trim()
        .slice(0, 15) || null;
    const celular =
      String(c['Teléfono'] || '')
        .trim()
        .slice(0, 20) || null;
    const direccion = String(c['Dirección'] || '').trim() || null;

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

    if (DRY_RUN) {
      info(`[DRY] ${razonSocial.padEnd(35)} CI: ${cedula} | Cobrador: ${nombreCobradorRaw || '(sin cobrador)'}`);
      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) ───────────
  // La marca MIGRADO_ACCESS|Id:X en dinfadic es la clave de deduplicación.
  const ventasYaMigradas = new Set<string>();
  if (!DRY_RUN) {
    const migradas = await prisma.factura_cab.findMany({
      where: {
        empresa_id: CONFIG.EMPRESA_ID,
        dinfadic: { contains: 'MIGRADO_ACCESS' },
      },
      select: { dinfadic: true },
    });
    for (const f of migradas) {
      // Extraer el Id original: "MIGRADO_ACCESS | Id:1234 | ..."
      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: 'MIGRADO_ACCESS' } },
      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[] = [];
  // IDs de ventas sin registros en facturas.xlsx → para revisión manual
  const sinRegistrosFactura: string[] = [];

  // Helper: leer columna de forma case-insensitive
  const 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((k) => norm(k) === t);
      if (k !== undefined) return row[k];
    }
    return undefined;
  };

  // filasValidas ya pasó todos los filtros y tiene saldo > 0
  for (const c of filasValidas) {
    const ventaId = String(getCol(c, 'Id') ?? '').trim();
    const cedula = normCedula(getCol(c, 'Cedula', 'Cédula'));
    // Saldo final: columna "Saldo" de clientes.xlsx
    const saldo = Number(getCol(c, 'Saldo') || 0);
    // Importe Total: precio unitario + total factura (case-insensitive)
    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 ─────────────────────────────────────────
    //   0 registros → loguear ID para revisión manual y saltar
    //   1 registro  → usar ese valor directamente
    //   N registros → moda (más frecuente); si todos distintos, usar el primero
    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) {
      // Un solo pago → usar ese monto directamente
      montoCuota = Math.round(Number(getCol(pagosConAbono[0], 'Abono1') || 0));
    } else {
      // Varios pagos → moda
      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) {
        // Todos distintos → usar el primero (monto acordado original)
        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 ────────────────────────────────
    // floor para no sobrepasar el saldo; si hay residuo → cuota extra ajustada
    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;

    // Próximo vencimiento = último pago registrado en facturas.xlsx + intervalo.
    // Sin registros de pago → usar Fecha de registro (clientes.xlsx) + intervalo.
    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: `MIGRADO_ACCESS | 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(ultimoPago['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                      ║');
  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);
});
