/**
 * ╔══════════════════════════════════════════════════════════════════════════╗
 * ║  IMPORTACIÓN: CLIENTES.xlsx (locales + exterior) → personas + clientes   ║
 * ╠══════════════════════════════════════════════════════════════════════════╣
 * ║  Uso:                                                                    ║
 * ║    npx ts-node scripts/data_script/cliente-naviera/import-clientes-naviera.ts \
 * ║      --ruc=80100092 --dry                                                ║
 * ║    npx ts-node scripts/data_script/cliente-naviera/import-clientes-naviera.ts \
 * ║      --ruc=80100092                                                      ║
 * ║                                                                          ║
 * ║  Flags:                                                                  ║
 * ║    --ruc=<8 dígitos>  RUC sin DV de la empresa destino    (obligatorio)  ║
 * ║    --file=<ruta>      Excel a importar (default: CLIENTES.xlsx de acá)   ║
 * ║    --hoja=<nombre>    Hoja a leer (default: la primera)                  ║
 * ║    --dry              Simula: no escribe nada en la BD                   ║
 * ╠══════════════════════════════════════════════════════════════════════════╣
 * ║  Columnas esperadas (se buscan por nombre, no por posición):             ║
 * ║    "Cliente Local / Exterior" | "C.I. / RUC / CUIT" | "Nombre"           ║
 * ╠══════════════════════════════════════════════════════════════════════════╣
 * ║  Reglas de conversión                                                    ║
 * ║                                                                          ║
 * ║  LOCAL    → Contribuyente. El documento viene siempre como RUC-DV        ║
 * ║             (ej. 80055935-5): se parte en `ruc` + `dv`. El tipo de       ║
 * ║             contribuyente sale del número (> 80000000 → jurídica).       ║
 * ║             tipo_operacion = B2B, pais = Paraguay.                       ║
 * ║                                                                          ║
 * ║  EXTERIOR → No contribuyente. El documento (CNPJ, RUT, CUIT, EIN…) se    ║
 * ║             guarda TAL CUAL en `nro_documento`, sin quitarle guiones:    ║
 * ║             no es un RUC-DV paraguayo y el separador es parte del        ║
 * ║             número. tipo_documento = Cédula extranjera,                  ║
 * ║             tipo_operacion = B2F, pais_id = null (la planilla no trae    ║
 * ║             país; se completa a mano en la ficha).                       ║
 * ╚══════════════════════════════════════════════════════════════════════════╝
 */

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

// ══════════════════════════════════════════════════════════════════════════
//  ARGUMENTOS
// ══════════════════════════════════════════════════════════════════════════
const args = process.argv.slice(2);
const flag = (nombre: string): string | undefined => {
  const hit = args.find((a) => a.startsWith(`--${nombre}=`));
  return hit ? hit.slice(nombre.length + 3) : undefined;
};

const RUC_EMPRESA = flag('ruc');
const HOJA = flag('hoja');
const DRY = args.includes('--dry');
const XLSX_FILE = path.resolve(flag('file') ?? path.join(__dirname, 'CLIENTES.xlsx'));

// ══════════════════════════════════════════════════════════════════════════
//  CATÁLOGOS SIFEN — ids fijos de las tablas de referencia
// ══════════════════════════════════════════════════════════════════════════
const CAT = {
  NATURALEZA_CONTRIBUYENTE: '45f32973-7d88-4d47-b2ea-d63b4d448d54', // cód 1
  NATURALEZA_NO_CONTRIBUYENTE: '50093f82-e37d-4120-ad06-cdfdfffa7ec7', // cód 2
  TIPO_CONTRIBUYENTE_FISICA: 'da9fdf2b-d72c-4bb7-8689-526a7f846c83', // cód 1
  TIPO_CONTRIBUYENTE_JURIDICA: 'b0da490e-ff65-4c8f-a5e5-823e17a67e9e', // cód 2
  TIPO_DOC_CEDULA_EXTRANJERA: 'c26f27a9-9843-43a5-b563-44f5d81fa8bd', // cód 3
  TIPO_OPERACION_B2B: '886b4cb8-17d8-443b-bb00-77664604ed01', // cód 1
  TIPO_OPERACION_B2F: '583492f3-3052-4691-990d-e418e55a47ee', // cód 4
  PAIS_PARAGUAY: '253381af-b7e6-4c2f-9e90-33be1aea687b', // PRY
};

// ══════════════════════════════════════════════════════════════════════════
//  LOG
// ══════════════════════════════════════════════════════════════════════════
const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
const LOG_FILE = path.join(__dirname, `import-clientes-naviera-${stamp}.log`);
const logStream = fs.createWriteStream(LOG_FILE, { flags: 'a' });
const say = (linea: string) => {
  console.log(linea);
  logStream.write(linea + '\n');
};

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

/** Lee una columna por nombre, tolerando mayúsculas, espacios dobles y acentos. */
function col(row: Record<string, any>, ...alias: string[]): string {
  const norm = (s: string) =>
    s.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/\s+/g, ' ').trim();
  for (const a of alias) {
    const objetivo = norm(a);
    const k = Object.keys(row).find((key) => norm(key) === objetivo);
    if (k !== undefined) return String(row[k] ?? '').trim();
  }
  return '';
}

/** Dígito verificador módulo 11 (mismo cálculo que usa la SET). */
function calcularDV(numero: string, baseMax = 11): number {
  let total = 0;
  let k = 2;
  let alfanumerico = '';
  for (const c of numero) alfanumerico += /\d/.test(c) ? c : c.charCodeAt(0);
  for (let i = alfanumerico.length - 1; i >= 0; i--) {
    if (k > baseMax) k = 2;
    total += parseInt(alfanumerico.charAt(i), 10) * k++;
  }
  const resto = total % 11;
  return resto > 1 ? 11 - resto : 0;
}

const RE_LOCAL = /^local$/i;
const RE_EXTERIOR = /^exterior$/i;
/** Documento paraguayo tal como viene en la planilla: 3-8 dígitos + guion + DV. */
const RE_RUC_DV = /^(\d{3,8})-(\d)$/;

interface Fila {
  fila: number;
  origen: 'LOCAL' | 'EXTERIOR';
  documento: string; // tal cual la planilla
  razonSocial: string;
  // Derivados
  ruc: string | null;
  dv: string | null;
  nroDocumento: string | null;
  naturalezaId: string;
  tipoContribuyenteId: string | null;
  tipoDocumentoId: string | null;
  tipoOperacionId: string;
  paisId: string | null;
  /** Clave de deduplicación dentro de la empresa. */
  clave: string;
  avisos: string[];
}

// ══════════════════════════════════════════════════════════════════════════
//  LECTURA Y CONVERSIÓN
// ══════════════════════════════════════════════════════════════════════════
function leerExcel(ruta: string): { filas: Fila[]; descartadas: string[] } {
  const wb = XLSX.readFile(ruta);
  const nombreHoja = HOJA ?? wb.SheetNames[0];
  const hoja = wb.Sheets[nombreHoja];
  if (!hoja) throw new Error(`La hoja "${nombreHoja}" no existe. Hojas: ${wb.SheetNames.join(', ')}`);

  const rows: Record<string, any>[] = XLSX.utils.sheet_to_json(hoja, { defval: '' });
  const filas: Fila[] = [];
  const descartadas: string[] = [];

  rows.forEach((row, i) => {
    const nroFila = i + 2; // +1 por el encabezado, +1 porque Excel es 1-indexed
    const origenRaw = col(row, 'Cliente Local / Exterior', 'Local / Exterior', 'origen');
    const documento = col(row, 'C.I. / RUC / CUIT', 'CI / RUC / CUIT', 'documento', 'ruc');
    const razonSocial = col(row, 'Nombre', 'razon social', 'razon_social');

    if (!origenRaw && !documento && !razonSocial) return; // fila vacía

    if (!documento || !razonSocial) {
      descartadas.push(`fila ${nroFila}: documento="${documento}" nombre="${razonSocial}" — incompleta`);
      return;
    }

    const avisos: string[] = [];
    if (razonSocial.length < 4) {
      descartadas.push(`fila ${nroFila}: razón social "${razonSocial}" tiene menos de 4 caracteres (mínimo SIFEN)`);
      return;
    }
    if (razonSocial.length > 255) {
      descartadas.push(`fila ${nroFila}: razón social de ${razonSocial.length} caracteres (máximo 255)`);
      return;
    }

    // ── LOCAL → contribuyente ───────────────────────────────────────────
    if (RE_LOCAL.test(origenRaw)) {
      const m = RE_RUC_DV.exec(documento);
      if (!m) {
        descartadas.push(
          `fila ${nroFila}: local "${documento}" no tiene formato RUC-DV (ej. 80055935-5) — revisar a mano`,
        );
        return;
      }
      const [, ruc, dv] = m;
      const dvEsperado = calcularDV(ruc);
      if (Number(dv) !== dvEsperado) {
        avisos.push(`DV ${dv} no coincide con el calculado (${dvEsperado}) para el RUC ${ruc}`);
      }

      filas.push({
        fila: nroFila,
        origen: 'LOCAL',
        documento,
        razonSocial,
        ruc,
        dv,
        nroDocumento: null,
        naturalezaId: CAT.NATURALEZA_CONTRIBUYENTE,
        tipoContribuyenteId:
          Number(ruc) > 80_000_000 ? CAT.TIPO_CONTRIBUYENTE_JURIDICA : CAT.TIPO_CONTRIBUYENTE_FISICA,
        tipoDocumentoId: null,
        tipoOperacionId: CAT.TIPO_OPERACION_B2B,
        paisId: CAT.PAIS_PARAGUAY,
        clave: `RUC:${ruc}`,
        avisos,
      });
      return;
    }

    // ── EXTERIOR → no contribuyente ─────────────────────────────────────
    if (RE_EXTERIOR.test(origenRaw)) {
      if (documento.length > 20) {
        descartadas.push(`fila ${nroFila}: documento "${documento}" excede los 20 caracteres de nro_documento`);
        return;
      }

      filas.push({
        fila: nroFila,
        origen: 'EXTERIOR',
        documento,
        razonSocial,
        ruc: null,
        dv: null,
        nroDocumento: documento, // sin tocar: el guion es parte del número, no un DV
        naturalezaId: CAT.NATURALEZA_NO_CONTRIBUYENTE,
        tipoContribuyenteId: null,
        tipoDocumentoId: CAT.TIPO_DOC_CEDULA_EXTRANJERA,
        tipoOperacionId: CAT.TIPO_OPERACION_B2F,
        paisId: null, // la planilla no trae país
        clave: `DOC:${documento}`,
        avisos,
      });
      return;
    }

    descartadas.push(`fila ${nroFila}: origen "${origenRaw}" no es "Local" ni "Exterior"`);
  });

  return { filas, descartadas };
}

// ══════════════════════════════════════════════════════════════════════════
//  MAIN
// ══════════════════════════════════════════════════════════════════════════
const prisma = new PrismaClient();

async function main() {
  if (!RUC_EMPRESA) throw new Error('Falta --ruc=<8 dígitos>. Ej: --ruc=80100092');
  if (!fs.existsSync(XLSX_FILE)) throw new Error(`No existe el archivo: ${XLSX_FILE}`);

  say('═'.repeat(78));
  say('  IMPORTACIÓN DE CLIENTES (locales + exterior)');
  say('═'.repeat(78));
  say(`  Archivo : ${XLSX_FILE}`);
  say(`  RUC     : ${RUC_EMPRESA}`);
  say(`  Modo    : ${DRY ? 'DRY-RUN (no escribe)' : 'REAL'}`);
  say(`  Log     : ${LOG_FILE}`);
  say('═'.repeat(78));

  const empresa = await prisma.empresas.findUnique({
    where: { ruc: RUC_EMPRESA },
    select: { id: true, ruc: true, razon_social: true },
  });
  if (!empresa) throw new Error(`No existe empresa con RUC ${RUC_EMPRESA}`);
  say(`\n▸ Empresa: ${empresa.razon_social} (${empresa.ruc})  id=${empresa.id}\n`);

  // ── 1. Leer y convertir ───────────────────────────────────────────────
  const { filas, descartadas } = leerExcel(XLSX_FILE);
  const locales = filas.filter((f) => f.origen === 'LOCAL');
  const exterior = filas.filter((f) => f.origen === 'EXTERIOR');

  say(`▸ Excel: ${filas.length} filas válidas  (${locales.length} locales, ${exterior.length} exterior)`);
  if (descartadas.length) {
    say(`  ${descartadas.length} filas descartadas:`);
    descartadas.forEach((d) => say(`    ✗ ${d}`));
  }

  // ── 2. Duplicados dentro del propio Excel ─────────────────────────────
  const porClave = new Map<string, Fila>();
  const duplicadosExcel: string[] = [];
  for (const f of filas) {
    const previa = porClave.get(f.clave);
    if (previa) {
      duplicadosExcel.push(`fila ${f.fila} "${f.razonSocial}" repite ${f.documento} (ya en fila ${previa.fila})`);
      continue;
    }
    porClave.set(f.clave, f);
  }
  if (duplicadosExcel.length) {
    say(`\n  ${duplicadosExcel.length} duplicados en el Excel (se toma el primero):`);
    duplicadosExcel.forEach((d) => say(`    ~ ${d}`));
  }

  const avisosDV = [...porClave.values()].filter((f) => f.avisos.length);
  if (avisosDV.length) {
    say(`\n  ⚠ ${avisosDV.length} filas con avisos (se importan igual):`);
    avisosDV.forEach((f) => f.avisos.forEach((a) => say(`    ⚠ fila ${f.fila} ${f.razonSocial}: ${a}`)));
  }

  // ── 3. Insertar ───────────────────────────────────────────────────────
  say('\n▸ Procesando:');
  let personasCreadas = 0;
  let personasReusadas = 0;
  let clientesCreados = 0;
  let clientesExistentes = 0;
  const errores: string[] = [];

  for (const f of [...porClave.values()]) {
    const etiqueta = `${f.origen.padEnd(8)} ${f.documento.padEnd(17)} ${f.razonSocial}`;
    try {
      // La persona se busca por RUC (locales) o por nro_documento (exterior),
      // siempre dentro de la empresa y descartando las borradas lógicamente.
      const persona = await prisma.personas.findFirst({
        where: {
          empresa_id: empresa.id,
          deleted: false,
          ...(f.origen === 'LOCAL' ? { ruc: f.ruc } : { nro_documento: f.nroDocumento }),
        },
        select: { id: true, razon_social: true },
      });

      let personaId: string;
      if (persona) {
        personaId = persona.id;
        personasReusadas++;
        say(`  ~ ${etiqueta}  → persona ya existía ("${persona.razon_social}")`);
      } else if (DRY) {
        personaId = `dry-${f.clave}`;
        personasCreadas++;
        say(`  + ${etiqueta}`);
      } else {
        const creada = await prisma.personas.create({
          data: {
            empresa_id: empresa.id,
            razon_social: f.razonSocial,
            naturaleza_id: f.naturalezaId,
            tipo_contribuyente_id: f.tipoContribuyenteId,
            tipo_documento_id: f.tipoDocumentoId,
            ruc: f.ruc,
            dv: f.dv,
            nro_documento: f.nroDocumento,
            pais_id: f.paisId,
            active: true,
            deleted: false,
          },
          select: { id: true },
        });
        personaId = creada.id;
        personasCreadas++;
        say(`  + ${etiqueta}`);
      }

      // Cliente sobre esa persona (puede existir si la persona ya estaba como
      // proveedor/cliente previo).
      if (DRY) {
        clientesCreados++;
        continue;
      }
      const clienteExistente = await prisma.clientes.findFirst({
        where: { persona_id: personaId, deleted: false },
        select: { id: true },
      });
      if (clienteExistente) {
        clientesExistentes++;
        say(`      · ya tenía ficha de cliente — no se duplica`);
      } else {
        await prisma.clientes.create({
          data: {
            persona_id: personaId,
            tipo_operacion_id: f.tipoOperacionId,
            active: true,
            deleted: false,
          },
        });
        clientesCreados++;
      }
    } catch (e: any) {
      errores.push(`fila ${f.fila} ${f.documento} ${f.razonSocial}: ${e.message}`);
      say(`  ✗ ${etiqueta}  → ${e.message}`);
    }
  }

  // ── 4. Resumen ────────────────────────────────────────────────────────
  say('\n' + '═'.repeat(78));
  say('  RESUMEN');
  say('═'.repeat(78));
  say(`  Filas del Excel        : ${filas.length + descartadas.length}`);
  say(`  Descartadas            : ${descartadas.length}`);
  say(`  Duplicadas en el Excel : ${duplicadosExcel.length}`);
  say(`  Personas creadas       : ${personasCreadas}`);
  say(`  Personas reutilizadas  : ${personasReusadas}`);
  say(`  Clientes creados       : ${clientesCreados}`);
  say(`  Clientes ya existentes : ${clientesExistentes}`);
  say(`  Errores                : ${errores.length}`);
  errores.forEach((e) => say(`    ✗ ${e}`));

  if (exterior.length) {
    say(`\n  ⚠ Los ${exterior.length} clientes del exterior quedaron SIN país en la ficha.`);
    say('    Hoy no bloquea la facturación (la validación solo exige país si el cliente');
    say('    tiene direcciones cargadas), pero conviene completarlo a mano.');
    say('    Ojo aparte: sifen-payload.service.ts:143 manda cPaisRec fijo en \'PRY\' para');
    say('    todas las facturas, así que las B2F hoy salen con país Paraguay igual.');
  }

  if (DRY) say('\n  (DRY-RUN: no se escribió nada en la base de datos)');
  say('');
}

main()
  .catch((e) => {
    say(`\n✖ ${e.message}\n`);
    process.exitCode = 1;
  })
  .finally(async () => {
    logStream.end();
    await prisma.$disconnect();
  });
