/**
 * ╔══════════════════════════════════════════════════════════════════════════╗
 * ║  IMPORTACIÓN: Plan de cuentas propio del cliente (Excel abaco) → ERP     ║
 * ╠══════════════════════════════════════════════════════════════════════════╣
 * ║  La empresa NO usa el plan default del seed: se reemplaza por completo   ║
 * ║  con el "INFORME DE PLAN DE CUENTAS" que exporta abaco.com.py.           ║
 * ║                                                                          ║
 * ║  Uso:                                                                    ║
 * ║    npx ts-node scripts/data_script/cliente-naviera/import-plan-cuentas.ts \
 * ║      --ruc=80100092 --dry                                                ║
 * ║    npx ts-node scripts/data_script/cliente-naviera/import-plan-cuentas.ts \
 * ║      --ruc=80100092 --reemplazar                                         ║
 * ║                                                                          ║
 * ║  Flags:                                                                  ║
 * ║    --ruc=<8 dígitos>   RUC sin DV de la empresa destino   (obligatorio)  ║
 * ║    --file=<ruta>       Excel a importar (default: el de esta carpeta)    ║
 * ║    --dry               Simula: no escribe nada en la BD                  ║
 * ║    --reemplazar        Borra el plan previo de la empresa antes de cargar║
 * ╠══════════════════════════════════════════════════════════════════════════╣
 * ║  Columnas esperadas (fila 5 = encabezado):                               ║
 * ║    CÓDIGO | DESCRIPCIÓN | TIPO | EQUIVALENCIAS                           ║
 * ║  TIPO del Excel es MAYORIZADOR / IMPUTABLE / FORMULADO: NO es el `tipo`  ║
 * ║  del ERP (ACTIVO/PASIVO/...). Solo define `acepta_movimientos`.          ║
 * ╚══════════════════════════════════════════════════════════════════════════╝
 */

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 tiene = (nombre: string) => args.includes(`--${nombre}`);

const RUC = flag('ruc');
const DRY = tiene('dry');
const REEMPLAZAR = tiene('reemplazar');
const XLSX_FILE = path.resolve(
  flag('file') ?? path.join(__dirname, 'INFORME DE PLAN DE CUENTAS (2).xlsx'),
);

// ══════════════════════════════════════════════════════════════════════════
//  REGLAS DE CONVERSIÓN
// ══════════════════════════════════════════════════════════════════════════

type TipoErp = 'ACTIVO' | 'PASIVO' | 'PATRIMONIO' | 'INGRESO' | 'COSTO' | 'GASTO';
type Naturaleza = 'DEUDORA' | 'ACREEDORA';

/**
 * El `tipo` del ERP sale de la raíz del código (los 2 primeros dígitos), que en
 * el plan de abaco sigue el ordenamiento del formulario IRE.
 *
 * Las raíces 06, 09, 12, 16, 18 y 20 son líneas FORMULADO (subtotales del
 * informe, no cuentas). Se importan para que el árbol en pantalla coincida con
 * el informe que usa el contador, con acepta_movimientos=false y saldo siempre
 * 0; el `tipo` que se les asigna es indiferente para los reportes.
 */
const TIPO_POR_RAIZ: Record<string, TipoErp> = {
  '01': 'ACTIVO',
  '02': 'PASIVO',
  '03': 'PATRIMONIO',
  '04': 'INGRESO',
  '05': 'COSTO',
  '06': 'INGRESO', // FORMULADO — Ganancias brutas en ventas
  '07': 'INGRESO',
  '08': 'INGRESO',
  '09': 'INGRESO', // FORMULADO — Ganancias brutas totales
  '10': 'GASTO',
  '11': 'GASTO',
  '12': 'INGRESO', // FORMULADO — Ganancias antes de gastos financieros
  '13': 'GASTO',
  '14': 'GASTO',
  '15': 'GASTO',
  '16': 'INGRESO', // FORMULADO — Ganancias operativas
  '17': 'INGRESO',
  '18': 'INGRESO', // FORMULADO — Ganancias antes de impuesto a la renta
  '19': 'GASTO',
  '20': 'INGRESO', // FORMULADO — Ganancias netas del ejercicio
};

const NATURALEZA_POR_TIPO: Record<TipoErp, Naturaleza> = {
  ACTIVO: 'DEUDORA',
  COSTO: 'DEUDORA',
  GASTO: 'DEUDORA',
  PASIVO: 'ACREEDORA',
  PATRIMONIO: 'ACREEDORA',
  INGRESO: 'ACREEDORA',
};

/**
 * Cuentas regularizadoras: abaco las prefija con "(-)" en la descripción
 * (ej. "(-) DEPRECIACIÓN ACUMULADA", "(-) DEVOLUCIONES").
 *
 * OJO con la naturaleza: el ERP calcula
 *     saldo = naturaleza === 'DEUDORA' ? debe - haber : haber - debe
 * y después suma los saldos por tipo (reportes.service.ts → getSaldosPorTipo).
 * Con eso, una regularizadora que conserva la naturaleza natural de su grupo ya
 * arroja saldo negativo por sí sola y resta del total. Invertirle la naturaleza
 * la haría SUMAR. Por eso la regla es: la naturaleza sale siempre del tipo.
 *
 * La única excepción es el Activo: el ERP tiene una regla explícita para
 * contra-activos (tipo ACTIVO + naturaleza ACREEDORA ⇒ niega el saldo), así que
 * las regularizadoras del Activo sí van ACREEDORA, como la Depreciación
 * Acumulada del seed.
 */
const esRegularizadora = (descripcion: string) => descripcion.startsWith('(-)');

const RE_CODIGO = /^\d{2}(\.\d{2})*$/;

interface CuentaExcel {
  codigo: string;
  descripcion: string;
  tipoExcel: string;
  tipo: TipoErp;
  naturaleza: Naturaleza;
  nivel: number;
  acepta_movimientos: boolean;
  fila: number;
}

const codigoPadre = (codigo: string) => codigo.split('.').slice(0, -1).join('.');

// ══════════════════════════════════════════════════════════════════════════
//  LECTURA DEL EXCEL
// ══════════════════════════════════════════════════════════════════════════
function leerExcel(ruta: string): { cuentas: CuentaExcel[]; descartadas: string[] } {
  const wb = XLSX.readFile(ruta);
  const hoja = wb.Sheets[wb.SheetNames[0]];
  const filas: any[][] = XLSX.utils.sheet_to_json(hoja, { header: 1, defval: '' });

  const cuentas: CuentaExcel[] = [];
  const descartadas: string[] = [];

  filas.forEach((fila, i) => {
    const codigo = String(fila[0] ?? '').trim();
    if (!codigo) return;

    // Encabezado, títulos y el pie "Creado por abaco.com.py"
    if (!RE_CODIGO.test(codigo)) {
      descartadas.push(`fila ${i + 1}: "${codigo}"`);
      return;
    }

    const descripcion = String(fila[1] ?? '').trim();
    const tipoExcel = String(fila[2] ?? '').trim().toUpperCase();
    const raiz = codigo.slice(0, 2);
    const tipo = TIPO_POR_RAIZ[raiz];

    if (!descripcion) throw new Error(`Fila ${i + 1}: cuenta ${codigo} sin descripción`);
    if (!tipo) throw new Error(`Fila ${i + 1}: raíz "${raiz}" (cuenta ${codigo}) no está en TIPO_POR_RAIZ`);
    if (!['MAYORIZADOR', 'IMPUTABLE', 'FORMULADO'].includes(tipoExcel)) {
      throw new Error(`Fila ${i + 1}: cuenta ${codigo} con TIPO desconocido "${tipoExcel}"`);
    }
    if (codigo.length > 20) throw new Error(`Fila ${i + 1}: código ${codigo} excede los 20 caracteres de la columna`);
    if (descripcion.length > 200) throw new Error(`Fila ${i + 1}: descripción de ${codigo} excede 200 caracteres`);

    // Contra-activo: única regularizadora que el ERP espera con naturaleza invertida.
    const naturaleza: Naturaleza =
      tipo === 'ACTIVO' && esRegularizadora(descripcion) ? 'ACREEDORA' : NATURALEZA_POR_TIPO[tipo];

    cuentas.push({
      codigo,
      descripcion,
      tipoExcel,
      tipo,
      naturaleza,
      nivel: codigo.split('.').length,
      acepta_movimientos: tipoExcel === 'IMPUTABLE',
      fila: i + 1,
    });
  });

  return { cuentas, descartadas };
}

/** Consistencia estructural del árbol antes de tocar la BD. */
function validar(cuentas: CuentaExcel[]) {
  const errores: string[] = [];
  const porCodigo = new Map(cuentas.map((c) => [c.codigo, c]));

  if (porCodigo.size !== cuentas.length) {
    const vistos = new Set<string>();
    for (const c of cuentas) {
      if (vistos.has(c.codigo)) errores.push(`Código duplicado: ${c.codigo} (fila ${c.fila})`);
      vistos.add(c.codigo);
    }
  }

  for (const c of cuentas) {
    if (c.nivel === 1) continue;
    const padre = porCodigo.get(codigoPadre(c.codigo));
    if (!padre) {
      errores.push(`Cuenta ${c.codigo} (fila ${c.fila}): falta la cuenta padre ${codigoPadre(c.codigo)}`);
      continue;
    }
    if (padre.acepta_movimientos) {
      errores.push(`Cuenta ${padre.codigo} es IMPUTABLE pero tiene hijas (ej. ${c.codigo})`);
    }
  }

  if (errores.length) {
    throw new Error(`El Excel no pasó la validación:\n  - ${errores.join('\n  - ')}`);
  }
}

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

async function main() {
  if (!RUC) 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}`);

  console.log('═'.repeat(78));
  console.log('  IMPORTACIÓN DE PLAN DE CUENTAS');
  console.log('═'.repeat(78));
  console.log(`  Archivo    : ${XLSX_FILE}`);
  console.log(`  RUC        : ${RUC}`);
  console.log(`  Modo       : ${DRY ? 'DRY-RUN (no escribe)' : 'REAL'}${REEMPLAZAR ? ' + REEMPLAZAR plan previo' : ''}`);
  console.log('═'.repeat(78));

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

  // ── 1. Leer y validar el Excel ────────────────────────────────────────
  const { cuentas, descartadas } = leerExcel(XLSX_FILE);
  validar(cuentas);

  const porNivel = cuentas.reduce<Record<number, number>>((acc, c) => {
    acc[c.nivel] = (acc[c.nivel] ?? 0) + 1;
    return acc;
  }, {});
  console.log(`▸ Excel: ${cuentas.length} cuentas válidas, ${descartadas.length} filas descartadas`);
  console.log(`  Niveles: ${Object.entries(porNivel).map(([n, q]) => `L${n}=${q}`).join('  ')}`);
  console.log(`  Imputables (aceptan movimientos): ${cuentas.filter((c) => c.acepta_movimientos).length}`);
  if (descartadas.length) console.log(`  Descartadas: ${descartadas.join(' | ')}`);

  // Candidatas a moneda_fija: se reportan, NO se setean. Fijar mal la moneda
  // bloquea los asientos de la cuenta, así que lo decide el contador.
  const candidatasMoneda = cuentas.filter(
    (c) => c.acepta_movimientos && /\b(USD|U\$S|U\$D|D[OÓ]LAR(ES)?)\b/i.test(c.descripcion),
  );

  // ── 2. Estado previo de la empresa ────────────────────────────────────
  const previas = await prisma.cont_plan_cuentas.findMany({
    where: { empresa_id: empresa.id },
    select: { id: true, codigo: true, descripcion: true },
  });
  console.log(`\n▸ Plan previo de la empresa: ${previas.length} cuentas`);

  if (previas.length && !REEMPLAZAR) {
    throw new Error(
      `La empresa ya tiene ${previas.length} cuentas. Corré con --reemplazar para borrarlas, ` +
        `o limpialas a mano si querés conservar alguna.`,
    );
  }

  let borradas = 0;
  if (previas.length) {
    const idsPrevios = previas.map((c) => c.id);

    // Guardas duras: si hay contabilidad registrada, no se borra nada.
    const conMovimientos = await prisma.cont_asientos_det.groupBy({
      by: ['cuenta_id'],
      where: { cuenta_id: { in: idsPrevios } },
      _count: { _all: true },
    });
    if (conMovimientos.length) {
      const detalle = conMovimientos
        .slice(0, 10)
        .map((m) => {
          const c = previas.find((p) => p.id === m.cuenta_id);
          return `${c?.codigo} ${c?.descripcion} (${m._count._all} mov.)`;
        })
        .join('\n    - ');
      throw new Error(
        `ABORTADO: ${conMovimientos.length} cuentas del plan previo tienen asientos registrados.\n` +
          `    - ${detalle}${conMovimientos.length > 10 ? '\n    - ...' : ''}\n` +
          `  Migrar el plan borraría contabilidad histórica. Resolvelo antes de continuar.`,
      );
    }

    const cxp = await prisma.cuentas_pagar.count({ where: { cuenta_pasivo_id: { in: idsPrevios } } });
    if (cxp) {
      throw new Error(
        `ABORTADO: ${cxp} documentos de cuentas a pagar tienen congelada una cuenta pasivo del plan previo.`,
      );
    }

    // Guardas blandas: configuración que se rehace después de la migración.
    const mapeos = await prisma.cont_mapeo_cuentas.count({ where: { cuenta_id: { in: idsPrevios } } });
    const mapeosMedio = await prisma.cont_mapeo_medio_pago.count({ where: { cuenta_contable_id: { in: idsPrevios } } });
    const tiposGasto = await prisma.tipo_gasto.count({ where: { cuenta_contable_id: { in: idsPrevios } } });
    const proveedores = await prisma.proveedores.count({ where: { cuenta_contable_id: { in: idsPrevios } } });

    console.log('  Configuración que apunta al plan previo y se va a limpiar:');
    console.log(`    cont_mapeo_cuentas    : ${mapeos}   (se borran — el mapeo se rehace a mano)`);
    console.log(`    cont_mapeo_medio_pago : ${mapeosMedio}   (se borran)`);
    console.log(`    tipo_gasto            : ${tiposGasto}   (cuenta_contable_id → null)`);
    console.log(`    proveedores (override): ${proveedores}   (cuenta_contable_id → null)`);

    if (!DRY) {
      await prisma.$transaction(async (tx) => {
        await tx.cont_mapeo_cuentas.deleteMany({ where: { cuenta_id: { in: idsPrevios } } });
        await tx.cont_mapeo_medio_pago.deleteMany({ where: { cuenta_contable_id: { in: idsPrevios } } });
        await tx.tipo_gasto.updateMany({
          where: { cuenta_contable_id: { in: idsPrevios } },
          data: { cuenta_contable_id: null },
        });
        await tx.proveedores.updateMany({
          where: { cuenta_contable_id: { in: idsPrevios } },
          data: { cuenta_contable_id: null },
        });
        // De hoja a raíz, para no violar el Restrict de cuenta_padre_id.
        const ordenadas = [...previas].sort(
          (a, b) => b.codigo.split('.').length - a.codigo.split('.').length,
        );
        for (const c of ordenadas) {
          await tx.cont_plan_cuentas.delete({ where: { id: c.id } });
        }
      }, { timeout: 120_000 });
    }
    borradas = previas.length;
    console.log(`  ${DRY ? '[dry] se borrarían' : 'Borradas'}: ${borradas} cuentas`);
  }

  // ── 3. Insertar nivel a nivel (el padre tiene que existir antes) ──────
  const idsPorCodigo = new Map<string, string>();
  let insertadas = 0;
  const nivelMax = Math.max(...cuentas.map((c) => c.nivel));

  console.log('\n▸ Insertando:');
  for (let nivel = 1; nivel <= nivelMax; nivel++) {
    const delNivel = cuentas.filter((c) => c.nivel === nivel);
    for (const c of delNivel) {
      const padreId = nivel === 1 ? null : idsPorCodigo.get(codigoPadre(c.codigo)) ?? null;
      if (nivel > 1 && !padreId && !DRY) {
        throw new Error(`Cuenta ${c.codigo}: no se resolvió el id del padre ${codigoPadre(c.codigo)}`);
      }

      if (DRY) {
        idsPorCodigo.set(c.codigo, `dry-${c.codigo}`);
      } else {
        const creada = await prisma.cont_plan_cuentas.create({
          data: {
            empresa_id: empresa.id,
            codigo: c.codigo,
            descripcion: c.descripcion,
            tipo: c.tipo,
            naturaleza: c.naturaleza,
            nivel: c.nivel,
            cuenta_padre_id: padreId,
            acepta_movimientos: c.acepta_movimientos,
            acepta_cc: false,
            moneda_fija: null,
            active: true,
            // Plan propio del cliente: el contador tiene que poder editarlo.
            is_sistema: false,
          },
          select: { id: true },
        });
        idsPorCodigo.set(c.codigo, creada.id);
      }
      insertadas++;
    }
    console.log(`  Nivel ${nivel}: ${delNivel.length} cuentas`);
  }

  // ── 4. Resumen ────────────────────────────────────────────────────────
  console.log('\n' + '═'.repeat(78));
  console.log('  RESUMEN');
  console.log('═'.repeat(78));
  console.log(`  Cuentas borradas  : ${borradas}`);
  console.log(`  Cuentas insertadas: ${insertadas}`);

  const mapeosRestantes = DRY
    ? 0
    : await prisma.cont_mapeo_cuentas.count({ where: { empresa_id: empresa.id } });
  console.log(`\n  ⚠ Mapeo de conceptos contables: ${mapeosRestantes} configurados.`);
  console.log('    Los asientos automáticos (ventas, compras, cobros, pagos) NO van a generarse');
  console.log('    hasta que se mapee cada concepto a una cuenta del plan nuevo, desde');
  console.log('    Contabilidad › Configuración › Mapeo de cuentas.');

  if (candidatasMoneda.length) {
    console.log(`\n  ⚠ ${candidatasMoneda.length} cuentas imputables parecen ser en moneda extranjera.`);
    console.log('    Se importaron SIN moneda_fija; revisar y fijarla desde la pantalla si corresponde:');
    candidatasMoneda.slice(0, 15).forEach((c) => console.log(`      ${c.codigo}  ${c.descripcion}`));
    if (candidatasMoneda.length > 15) console.log(`      ... y ${candidatasMoneda.length - 15} más`);
  }

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

main()
  .catch((e) => {
    console.error(`\n✖ ${e.message}\n`);
    process.exitCode = 1;
  })
  .finally(() => prisma.$disconnect());
