/**
 * ╔══════════════════════════════════════════════════════════════╗
 * ║        SCRIPT DE IMPORTACIÓN: Excel → Clientes Novasis     ║
 * ╠══════════════════════════════════════════════════════════════╣
 * ║  Crea:                                                       ║
 * ║    • personas (validando si es contribuyente via SIFEN)      ║
 * ║    • clientes (vinculados a la persona)                      ║
 * ╠══════════════════════════════════════════════════════════════╣
 * ║  Instrucciones:                                              ║
 * ║  1. npm install xlsx  (solo la primera vez)            ║
 * ║  2. Colocar clientes.xlsx en scripts/data_script/            ║
 * ║  3. Ajustar la sección CONFIG                                ║
 * ║  4. Dry-run:  npx ts-node scripts/data_script/import-clientes.ts --dry  ║
 * ║  5. Real:     npx ts-node scripts/data_script/import-clientes.ts        ║
 * ╚══════════════════════════════════════════════════════════════╝
 *
 * Columnas esperadas en clientes.xlsx:
 *   codigo | nombre (razon_social) | direccion | celular | nro_documento
 */

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

// Elige http o https según el protocolo de la URL
const httpRequest = (url: URL) => url.protocol === 'https:' ? https.request.bind(https) : http.request.bind(http);

// ══════════════════════════════════════════════════════════════════
//  CONFIG — ajustar antes de ejecutar
// ══════════════════════════════════════════════════════════════════
const CONFIG = {
  // ── Empresa destino ───────────────────────────────────────────
  EMPRESA_ID: '3b327470-82de-4c8b-bc6f-070f38a4f238',

  // ── Archivo Excel ─────────────────────────────────────────────
  XLSX_FILE: path.resolve(__dirname, 'clientes.xlsx'),

  // ── API mdware (consulta SIFEN) ───────────────────────────────
  MDWARE_BASE_URL: 'https://mdware.novasispy.com',
  MDWARE_RUC: '80144315', // RUC de autenticación — ajustar
  MDWARE_PASSWORD: 'ABC#80144315', // Contraseña — ajustar

  // ── IDs si el documento ES contribuyente (RUC encontrado en SIFEN) ──
  CONTRIBUYENTE: {
    NATURALEZA_ID: '45f32973-7d88-4d47-b2ea-d63b4d448d54',
    // tipo_contribuyente_id según el número de RUC:
    //   > 80000000  → empresa/jurídica
    //   <= 80000000 → persona física
    TIPO_CONTRIBUYENTE_JURIDICA_ID: 'b0da490e-ff65-4c8f-a5e5-823e17a67e9e',
    TIPO_CONTRIBUYENTE_FISICA_ID: 'da9fdf2b-d72c-4bb7-8689-526a7f846c83',
    TIPO_DOCUMENTO_ID: null as string | null,
    PAIS_ID: '253381af-b7e6-4c2f-9e90-33be1aea687b',
    TIPO_OPERACION_ID: '886b4cb8-17d8-443b-bb00-77664604ed01', // B2B
  },

  // ── IDs si el documento NO es contribuyente (RUC no encontrado) ──
  NO_CONTRIBUYENTE: {
    NATURALEZA_ID: '50093f82-e37d-4120-ad06-cdfdfffa7ec7',
    TIPO_CONTRIBUYENTE_ID: null as string | null,
    TIPO_DOCUMENTO_ID: '7ce2cf6d-af16-4be2-a1da-628127a2c3c9', // CI paraguaya
    PAIS_ID: '253381af-b7e6-4c2f-9e90-33be1aea687b',
    TIPO_OPERACION_ID: 'f4464d3e-b711-424c-9833-84a9d4eca8e8', // B2C
  },

  LIMITE_CREDITO_DEFAULT: 0,
};

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

/** Sanitiza celular: deja solo dígitos, elimina prefijo país +595/0 y trunca a 20 chars */
function sanitizarCelular(v: any): string | null {
  const raw = String(v ?? '').replace(/[\s\-().+]/g, '').trim();
  if (!raw) return null;
  // Quitar prefijo internacional de Paraguay: 595
  const sinPrefijo = raw.startsWith('595') ? raw.slice(3) : raw;
  // Quitar cero inicial si quedó (ej: 0981... → 981...)
  const normalizado = sinPrefijo.startsWith('0') ? sinPrefijo.slice(1) : sinPrefijo;
  const resultado = normalizado.slice(0, 20);
  return resultado || null;
}

/** Sanitiza nro_documento: elimina puntos, guiones, espacios y el dígito verificador (después del '-') */
function sanitizarDoc(v: any): string {
  return String(v ?? '')
    .split('-')[0] // quitar DV si viene con guión (ej: "1044824-5")
    .replace(/[\s.]/g, '') // quitar espacios y puntos
    .trim();
}

/** Cálculo de dígito verificador módulo 11 */
function calcularDVModulo11(p_numero: string, p_basemax = 11): number {
  let v_total = 0;
  let k = 2;

  let v_numero_al = '';
  for (let i = 0; i < p_numero.length; i++) {
    const c = p_numero.charAt(i);
    if (/\d/.test(c)) {
      v_numero_al += c;
    } else {
      v_numero_al += c.charCodeAt(0);
    }
  }

  for (let i = v_numero_al.length - 1; i >= 0; i--) {
    k = k > p_basemax ? 2 : k;
    const v_numero_aux = parseInt(v_numero_al.charAt(i), 10);
    v_total += v_numero_aux * k++;
  }

  const v_resto = v_total % 11;
  return v_resto > 1 ? 11 - v_resto : 0;
}

/** Lee columna de forma case-insensitive con múltiples nombres alternativos */
function 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;
}

// ══════════════════════════════════════════════════════════════════
//  LOGGER
// ══════════════════════════════════════════════════════════════════

const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
const LOG_FILE = path.join(__dirname, `import-clientes-${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);
  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');
}

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

// ══════════════════════════════════════════════════════════════════
//  AUTENTICACIÓN — obtiene token una vez al inicio
// ══════════════════════════════════════════════════════════════════

/** Token obtenido al autenticar, reutilizado en todo el proceso */
let tokenAPI: string | null = null;

async function autenticarAPI(): Promise<{ ok: boolean; motivo?: string }> {
  return new Promise((resolve) => {
    const body = JSON.stringify({ ruc: CONFIG.MDWARE_RUC, password: CONFIG.MDWARE_PASSWORD });
    const url = new URL(`${CONFIG.MDWARE_BASE_URL}/api/auth`);
    const options = {
      hostname: url.hostname,
      port: url.port || 443,
      path: url.pathname,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(body),
      },
      timeout: 10_000,
    };

    const req = httpRequest(url)(options, (res) => {
      let raw = '';
      res.on('data', (chunk) => {
        raw += chunk;
      });
      res.on('end', () => {
        const status = res.statusCode ?? 0;
        if (status === 401 || status === 403) {
          resolve({ ok: false, motivo: `Credenciales incorrectas (HTTP ${status})` });
          return;
        }
        if (status < 200 || status >= 300) {
          resolve({ ok: false, motivo: `El servidor respondió con HTTP ${status}: ${raw.substring(0, 120)}` });
          return;
        }
        try {
          const data = JSON.parse(raw) as { status: string; token?: string };
          if (data?.token) {
            tokenAPI = data.token;
            resolve({ ok: true });
          } else {
            resolve({ ok: false, motivo: `Respuesta sin token: ${raw.substring(0, 120)}` });
          }
        } catch {
          resolve({ ok: false, motivo: `Respuesta no es JSON válido: ${raw.substring(0, 80)}` });
        }
      });
    });

    req.on('error', (e) => resolve({ ok: false, motivo: `No se puede conectar: ${e.message}` }));
    req.on('timeout', () => {
      req.destroy();
      resolve({ ok: false, motivo: 'Timeout al conectar (>10s)' });
    });
    req.write(body);
    req.end();
  });
}

// ══════════════════════════════════════════════════════════════════
//  CONSULTA SIFEN via mdware
// ══════════════════════════════════════════════════════════════════

interface SifenResult {
  esContribuyente: boolean;
  razonSocialSifen?: string; // la que devuelve SIFEN (puede usarse para validar)
  error?: string; // si hubo falla de conexión
}

async function consultarSifen(nroDoc: string): Promise<SifenResult> {
  return new Promise((resolve) => {
    const body = JSON.stringify({ tipOpe: '7', ruc: nroDoc });
    const url = new URL(`${CONFIG.MDWARE_BASE_URL}/api/maintenance`);
    const options = {
      hostname: url.hostname,
      port: url.port || 443,
      path: url.pathname,
      method: 'POST',
      headers: {
        Authorization: `Bearer ${tokenAPI}`,
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(body),
      },
      timeout: 10_000,
    };

    const req = httpRequest(url)(options, (res) => {
      let rawBody = '';
      res.on('data', (chunk) => { rawBody += chunk; });
      res.on('end', () => {
        try {
          const data = JSON.parse(rawBody);
          const status     = data?.status ?? '(sin status)';
          const codResp    = data?.response?.cod_respuesta ?? '(sin cod_respuesta)';
          const descResp   = data?.response?.d_des_resp ?? data?.response?.descripcion ?? '';
          const razonSifen = data?.response?.razon_social ?? '';

          if (data?.status === 'success' && data?.response?.cod_respuesta === '0502') {
            log('INFO', `  SIFEN RUC=${nroDoc} → CONTRIBUYENTE ✓ | razon_social="${razonSifen}"`);
            resolve({ esContribuyente: true, razonSocialSifen: razonSifen });
          } else {
            log('INFO', `  SIFEN RUC=${nroDoc} → NO encontrado | status="${status}" cod="${codResp}" desc="${descResp}"`);
            resolve({ esContribuyente: false });
          }
        } catch {
          log('WARN', `  SIFEN RUC=${nroDoc} → respuesta no es JSON válido: ${rawBody.substring(0, 120)}`);
          resolve({ esContribuyente: false, error: 'Respuesta no es JSON válido' });
        }
      });
    });

    req.on('error', (e: any) => {
      log('WARN', `  SIFEN RUC=${nroDoc} → error de red: ${e.message}`);
      resolve({ esContribuyente: false, error: e.message });
    });
    req.on('timeout', () => {
      req.destroy();
      log('WARN', `  SIFEN RUC=${nroDoc} → timeout (>10s)`);
      resolve({ esContribuyente: false, error: 'Timeout' });
    });
    req.write(body);
    req.end();
  });
}

// ══════════════════════════════════════════════════════════════════
//  MAIN
// ══════════════════════════════════════════════════════════════════
const prisma = new PrismaClient();
const DRY_RUN = process.argv.includes('--dry');

async function run() {
  raw('');
  raw('╔══════════════════════════════════════════════════╗');
  raw(`║  IMPORTACIÓN CLIENTES → Novasis  ${DRY_RUN ? '[ DRY RUN ]    ' : '[ REAL — ESCRIBE ]'} ║`);
  raw(`║  Log: ${path.basename(LOG_FILE).padEnd(41)}║`);
  raw('╚══════════════════════════════════════════════════╝');
  raw('');

  if (!CONFIG.MDWARE_RUC || !CONFIG.MDWARE_PASSWORD) {
    raw('ERROR: Debes configurar CONFIG.MDWARE_RUC y CONFIG.MDWARE_PASSWORD antes de ejecutar.');
    process.exit(1);
  }

  // ── Autenticación mdware (una sola vez) ──────────────────────
  if (!DRY_RUN) {
    raw('');
    info(`Autenticando en API mdware con RUC ${CONFIG.MDWARE_RUC}...`);
    const auth = await autenticarAPI();
    if (!auth.ok) {
      raw('');
      raw('╔══════════════════════════════════════════════════════╗');
      raw('║  ✗ ERROR FATAL— No se puede continuar              ║');
      raw('╠══════════════════════════════════════════════════════╣');
      raw(`║  ${auth.motivo!.substring(0, 52).padEnd(52)}║`);
      raw('╚══════════════════════════════════════════════════════╝');
      raw('');
      logStream.end();
      await prisma.$disconnect();
      process.exit(1);
    }
    ok('Token obtenido correctamente — listo para procesar. ;' + tokenAPI);
    raw('');
  }

  // ── Leer Excel ────────────────────────────────────────────────
  if (!fs.existsSync(CONFIG.XLSX_FILE)) {
    raw(`ERROR: No se encontró el archivo: ${CONFIG.XLSX_FILE}`);
    process.exit(1);
  }

  const wb = XLSX.readFile(CONFIG.XLSX_FILE);
  const rows: any[] = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
  info(`Filas en Excel: ${rows.length}`);

  // ── Parsear y deduplicar por nro_documento ───────────────────
  interface RowParseada {
    codigo: string;
    razonSocial: string;
    direccion: string | null;
    celular: string | null;
    nroDoc: string; // sanitizado
  }

  const filasParseadas: RowParseada[] = [];
  const sinDocumento: number[] = [];

  for (let i = 0; i < rows.length; i++) {
    const row = rows[i];
    const nroDocRaw = colVal(row, 'nro_documento', 'nro documento', 'documento', 'cedula', 'ruc');
    const nroDoc = sanitizarDoc(nroDocRaw);

    if (!nroDoc) {
      sinDocumento.push(i + 2); // fila Excel (1-indexed + cabecera)
      warn(`Fila ${i + 2}: sin nro_documento — omitida`);
      continue;
    }

    filasParseadas.push({
      codigo: String(colVal(row, 'codigo', 'código', 'cod') ?? '').trim(),
      razonSocial:
        String(colVal(row, 'razon_social', 'razon social', 'nombre', 'name') ?? '').trim() || `Cliente ${nroDoc}`,
      direccion: String(colVal(row, 'direccion', 'dirección', 'address') ?? '').trim() || null,
      celular: sanitizarCelular(colVal(row, 'celular', 'telefono', 'teléfono', 'phone')),
      nroDoc,
    });
  }

  // Deduplicar por nroDoc (tomar el primero)
  const vistosDocs = new Map<string, RowParseada>();
  for (const fp of filasParseadas) {
    if (!vistosDocs.has(fp.nroDoc)) {
      vistosDocs.set(fp.nroDoc, fp);
    } else {
      warn(`Documento duplicado en Excel: ${fp.nroDoc} (${fp.razonSocial}) — se usa el primer registro`);
    }
  }

  info(`Filas con documento   : ${filasParseadas.length}`);
  info(`Documentos únicos     : ${vistosDocs.size}`);
  info(`Sin documento (skip)  : ${sinDocumento.length}`);
  raw('');

  // ══════════════════════════════════════════════════════════════
  //  BUCLE PRINCIPAL — consulta SIFEN + crear persona + cliente
  // ══════════════════════════════════════════════════════════════
  title('━━━ PROCESANDO CLIENTES ━━━');

  let creados = 0;
  let existentes = 0;
  let errores = 0;
  let sifenContribuyentes = 0;
  let sifenNoContribuyentes = 0;
  let sifenErrores = 0;
  const errList: string[] = [];

  const entries = [...vistosDocs.entries()];

  for (let i = 0; i < entries.length; i++) {
    const [nroDoc, fp] = entries[i];
    const progreso = `[${String(i + 1).padStart(4)}/${entries.length}]`;

    // ── Consultar SIFEN ────────────────────────────────────────
    let sifen: SifenResult;
    if (DRY_RUN) {
      // En dry-run simulamos: doc > 80000000 → contribuyente jurídica, si no → no contribuyente
      sifen = { esContribuyente: false };
    } else {
      sifen = await consultarSifen(nroDoc);
    }

    if (sifen.error) {
      sifenErrores++;
      warn(`${progreso} SIFEN error para ${nroDoc}: ${sifen.error} → tratado como no contribuyente`);
    } else if (sifen.esContribuyente) {
      sifenContribuyentes++;
    } else {
      sifenNoContribuyentes++;
    }

    // ── Construir datos de persona ─────────────────────────────
    let personaData: {
      naturaleza_id: string;
      tipo_contribuyente_id: string | null;
      tipo_documento_id: string | null;
      ruc: string | null;
      dv: number | null;
      nro_documento: string | null;
      pais_id: string;
    };

    if (sifen.esContribuyente) {
      const esJuridica = parseInt(nroDoc, 10) > 80_000_000;
      personaData = {
        naturaleza_id: CONFIG.CONTRIBUYENTE.NATURALEZA_ID,
        tipo_contribuyente_id: esJuridica
          ? CONFIG.CONTRIBUYENTE.TIPO_CONTRIBUYENTE_JURIDICA_ID
          : CONFIG.CONTRIBUYENTE.TIPO_CONTRIBUYENTE_FISICA_ID,
        tipo_documento_id: CONFIG.CONTRIBUYENTE.TIPO_DOCUMENTO_ID,
        ruc: nroDoc,
        dv: calcularDVModulo11(nroDoc),
        nro_documento: null,
        pais_id: CONFIG.CONTRIBUYENTE.PAIS_ID,
      };
    } else {
      personaData = {
        naturaleza_id: CONFIG.NO_CONTRIBUYENTE.NATURALEZA_ID,
        tipo_contribuyente_id: CONFIG.NO_CONTRIBUYENTE.TIPO_CONTRIBUYENTE_ID,
        tipo_documento_id: CONFIG.NO_CONTRIBUYENTE.TIPO_DOCUMENTO_ID,
        ruc: null,
        dv: null,
        nro_documento: nroDoc,
        pais_id: CONFIG.NO_CONTRIBUYENTE.PAIS_ID,
      };
    }

    const tag = sifen.esContribuyente ? 'CONTRIB' : 'CI/DOC';
    const dvStr = personaData.dv !== null ? `-${personaData.dv}` : '';
    info(
      `${progreso} ${tag} | ${fp.razonSocial.substring(0, 35).padEnd(35)} | doc: ${nroDoc}${dvStr} | cod: ${fp.codigo}`,
    );

    if (DRY_RUN) continue;

    try {
      // ── Buscar persona existente ───────────────────────────────
      // Buscar por RUC (si contribuyente) o por nro_documento (si no)
      const wherePersona = sifen.esContribuyente
        ? { empresa_id: CONFIG.EMPRESA_ID, ruc: nroDoc, deleted: false }
        : { empresa_id: CONFIG.EMPRESA_ID, nro_documento: nroDoc, deleted: false };

      let persona = await prisma.personas.findFirst({ where: wherePersona });

      if (!persona) {
        persona = await prisma.personas.create({
          data: {
            empresa_id: CONFIG.EMPRESA_ID,
            razon_social: fp.razonSocial,
            naturaleza_id: personaData.naturaleza_id,
            tipo_contribuyente_id: personaData.tipo_contribuyente_id,
            tipo_documento_id: personaData.tipo_documento_id,
            ruc: personaData.ruc,
            dv: personaData.dv !== null ? String(personaData.dv) : null,
            nro_documento: personaData.nro_documento,
            pais_id: personaData.pais_id,
            celular: fp.celular,
            direccion: fp.direccion,
            active: true,
            deleted: false,
          },
        });
      }

      // ── Buscar cliente existente ───────────────────────────────
      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: sifen.esContribuyente
              ? CONFIG.CONTRIBUYENTE.TIPO_OPERACION_ID
              : CONFIG.NO_CONTRIBUYENTE.TIPO_OPERACION_ID,
            referencia_domicilio: fp.direccion,
            limite_credito: CONFIG.LIMITE_CREDITO_DEFAULT,
            active: true,
            deleted: false,
          },
        });
        creados++;
        ok(`Creado  | ${fp.razonSocial.substring(0, 35).padEnd(35)} | cliente_id: ${cliente.id}`);
      } else {
        existentes++;
        skip(`Existe  | ${fp.razonSocial.substring(0, 35).padEnd(35)} | cliente_id: ${cliente.id}`);
      }
    } catch (e: any) {
      errores++;
      const msg = `doc: ${nroDoc} | ${fp.razonSocial} | ERROR: ${e.message}`;
      errList.push(msg);
      err(msg);
    }
  }

  // ══════════════════════════════════════════════════════════════
  //  RESUMEN FINAL
  // ══════════════════════════════════════════════════════════════
  raw('');
  raw('╔══════════════════════════════════════════════════════╗');
  raw('║                   RESUMEN FINAL                      ║');
  raw('╠══════════════════════════════════════════════════════╣');
  raw(`║  Filas en Excel                 : ${String(rows.length).padStart(6)}               ║`);
  raw(`║  Sin documento (omitidas)       : ${String(sinDocumento.length).padStart(6)}               ║`);
  raw(`║  Documentos únicos a procesar   : ${String(vistosDocs.size).padStart(6)}               ║`);
  raw('║──────────────────────────────────────────────────────║');
  raw(`║  SIFEN → Contribuyentes         : ${String(sifenContribuyentes).padStart(6)}               ║`);
  raw(`║  SIFEN → No contribuyentes      : ${String(sifenNoContribuyentes).padStart(6)}               ║`);
  raw(`║  SIFEN → Errores de conexión    : ${String(sifenErrores).padStart(6)}               ║`);
  raw('║──────────────────────────────────────────────────────║');
  raw(`║  Clientes creados               : ${String(creados).padStart(6)}               ║`);
  raw(`║  Clientes ya existentes         : ${String(existentes).padStart(6)}               ║`);
  raw(`║  Errores de BD                  : ${String(errores).padStart(6)}               ║`);
  raw('╚══════════════════════════════════════════════════════╝');

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

  raw('');
  if (DRY_RUN) {
    raw('⚠  DRY RUN — No se escribió nada en la base de datos.');
    raw('   Ejecutar sin --dry para aplicar la importación real.');
  } else {
    raw(
      errores === 0
        ? '✓  Importación completada sin errores.'
        : `⚠  Importación completada con ${errores} 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);
});
