/**
 * ╔══════════════════════════════════════════════════════════════╗
 * ║   MIGRACIÓN: Lubricantes + stock inicial desde xlsx           ║
 * ╠══════════════════════════════════════════════════════════════╣
 * ║  Hermano de migrate-productos-agogo.ts, para la planilla       ║
 * ║  manual "PLANILLA DE LUBRICANTES.xlsx". Crea/actualiza         ║
 * ║  productos y carga su stock en el depósito marcado como        ║
 * ║  principal de la sucursal 001 de la empresa.                   ║
 * ║                                                                ║
 * ║  Hoja "lUBRICANTES" (sí, con l minúscula). La fila 1 es el      ║
 * ║  título del reporte; el encabezado real está en la fila 2:     ║
 * ║    Codigo | Descripcion | Stock | Costo | Venta                ║
 * ║                                                                ║
 * ║  PRECIOS EN MILES: la planilla anota los importes en miles de  ║
 * ║  guaraníes ("10" = 10.000 Gs). Se multiplican por --escala     ║
 * ║  (default 1000) antes de guardar. La evidencia está en las     ║
 * ║  propias celdas: las que vinieron como texto sí traen el       ║
 * ║  importe completo ("17.000V/19.000R") y conviven con vecinas   ║
 * ║  numéricas del mismo orden (15.5, 17). Además los productos    ║
 * ║  que ya tiene la empresa están cargados en guaraníes enteros.  ║
 * ║                                                                ║
 * ║  Upsert por (empresa_id, cod_producto). El stock SOLO se carga ║
 * ║  cuando el producto se CREA en esta corrida: reejecutarlo no   ║
 * ║  vuelve a sumar cantidades.                                    ║
 * ╠══════════════════════════════════════════════════════════════╣
 * ║  Uso:                                                          ║
 * ║  1. Dry-run (no escribe nada, muestra los precios ya           ║
 * ║     convertidos para poder revisarlos):                        ║
 * ║     npx ts-node --transpile-only -r tsconfig-paths/register \  ║
 * ║       --project tsconfig.scripts.json \                        ║
 * ║       scripts/migrate-access/migrate-lubricantes-agogo.ts \    ║
 * ║       --empresa=<uuid> --dry                                   ║
 * ║  2. Real (mismo comando sin --dry)                             ║
 * ║  3. Opcionales: --archivo=ruta.xlsx  --hoja=NOMBRE             ║
 * ║                 --escala=1000                                  ║
 * ║                                                                ║
 * ║  --empresa es OBLIGATORIO: correr esto contra la empresa       ║
 * ║  equivocada crea productos y stock reales.                     ║
 * ╚══════════════════════════════════════════════════════════════╝
 */

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

// ══════════════════════════════════════════════════════════════════
//  CONFIG — catálogos globales (sin empresa_id, iguales para todos)
// ══════════════════════════════════════════════════════════════════
const CATALOGO = {
  AFECTACION_ID: '7b634413-a746-4152-9941-4d586ed75825', // Gravado IVA (global)
  UNIDAD_MEDIDA_ID: '4f6a1318-1a42-454d-874e-a72f6cda04df', // Unidad (global)
  IVA_DEFAULT: 10,
  PROPORCION_IVA_DEFAULT: 100,
  TIPO_PRODUCTO: 'producto' as const,

  // El stock va a la sucursal con este punto de establecimiento, en su
  // depósito marcado como principal.
  PUNTO_ESTABLECIMIENTO: '001',

  DEFAULT_XLSX_PATH: path.resolve(
    __dirname,
    '../../scripts/data_script/cliente-agogo/PLANILLA DE LUBRICANTES.xlsx',
  ),
  HOJA: 'lUBRICANTES',
  // La fila 0 es el título ("CONTROL DE STOCK DE LUBRICANTES" + fecha).
  // El encabezado real arranca en la fila 1.
  FILA_ENCABEZADO: 1,
  ESCALA_PRECIO: 1000,
};

// ══════════════════════════════════════════════════════════════════
//  ARGUMENTOS DE LÍNEA DE COMANDOS
// ══════════════════════════════════════════════════════════════════
function argValue(flag: string): string | undefined {
  const arg = process.argv.find((a) => a.startsWith(`--${flag}=`));
  return arg ? arg.split('=').slice(1).join('=') : undefined;
}

const DRY = process.argv.includes('--dry');
const archivoArg = argValue('archivo');
const XLSX_PATH = archivoArg ? path.resolve(process.cwd(), archivoArg) : CATALOGO.DEFAULT_XLSX_PATH;
const HOJA = argValue('hoja') ?? CATALOGO.HOJA;

const escalaArg = argValue('escala');
const ESCALA = escalaArg ? Number(escalaArg) : CATALOGO.ESCALA_PRECIO;
if (!Number.isFinite(ESCALA) || ESCALA <= 0) {
  console.error(`✗✗FATAL --escala inválida: "${escalaArg}". Tiene que ser un número mayor a 0.`);
  process.exit(1);
}

const empresaArg = argValue('empresa');
if (!empresaArg) {
  console.error('✗✗FATAL Falta --empresa=<uuid>. Este script no corre contra una empresa por defecto.');
  console.error('        Ejemplo: ... migrate-lubricantes-agogo.ts --empresa=7d3ce403-9487-4289-b21f-2e5d3477f5b5 --dry');
  process.exit(1);
}
const EMPRESA_ID: string = empresaArg;

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

type LogLevel = 'INFO' | 'OK' | 'SKIP' | 'WARN' | 'ERROR' | 'FATAL' | '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`,
    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 raw = (m: string) => log('RAW', m);

// ══════════════════════════════════════════════════════════════════
//  HELPERS
// ══════════════════════════════════════════════════════════════════
function toStr(val: any): string {
  if (val === null || val === undefined) return '';
  return String(val).trim();
}

/**
 * Número de una celda, o null si no se puede leer con confianza.
 *
 * A diferencia del script hermano, acá NO se limpian puntos como separador de
 * miles: en esta planilla el punto es decimal ("6.8" son 6,8 miles = 6.800 Gs),
 * y borrarlo convertiría 6,8 en 68. Las celdas numéricas llegan de XLSX ya como
 * number; sólo hay que decidir qué hacer con las de texto.
 *
 * Devuelve null —y no 0— cuando la celda trae algo que no es un número solo
 * (hay dos casos en la planilla con dos precios en la misma celda, tipo
 * "17.000V/19.000R"). Adivinar cuál de los dos vale sería inventar un precio.
 */
function toNumOrNull(val: any): number | null {
  if (val === null || val === undefined || val === '') return null;
  if (typeof val === 'number') return Number.isFinite(val) ? val : null;
  const s = String(val).trim().replace(',', '.');
  if (!/^-?\d+(\.\d+)?$/.test(s)) return null;
  const n = parseFloat(s);
  return Number.isFinite(n) ? n : null;
}

/** Importe final en guaraníes enteros. Guaraní no usa decimales. */
function aGuaranies(val: number | null): number {
  if (val === null) return 0;
  return Math.max(0, Math.round(val * ESCALA));
}

const fmtGs = (n: number) => `Gs. ${n.toLocaleString('es-PY')}`;

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

type Anomalia = { motivo: string; detalle: string };

async function main() {
  raw('');
  raw('╔══════════════════════════════════════════════════╗');
  raw(`║  MIGRACIÓN LUBRICANTES + STOCK ${DRY ? '[ DRY RUN ]  ' : '[ REAL—ESCRIBE ]'}║`);
  raw(`║  Empresa: ${EMPRESA_ID.padEnd(39)}║`);
  raw(`║  Archivo: ${path.basename(XLSX_PATH).slice(0, 39).padEnd(39)}║`);
  raw(`║  Hoja:    ${HOJA.padEnd(39)}║`);
  raw(`║  Escala precios: x${String(ESCALA).padEnd(31)}║`);
  raw(`║  Log:     ${path.basename(LOG_FILE).slice(0, 39).padEnd(39)}║`);
  raw('╚══════════════════════════════════════════════════╝');
  raw('');

  // ── Resolver empresa, sucursal 001 y depósito principal ─────────
  const empresa = await prisma.empresas.findUnique({
    where: { id: EMPRESA_ID },
    select: { id: true, razon_social: true },
  });
  if (!empresa) {
    log('FATAL', `No existe ninguna empresa con id ${EMPRESA_ID}`);
    process.exit(1);
  }
  info(`Empresa: ${empresa.razon_social} (${empresa.id})`);

  const sucursal = await prisma.empresas_sucursales.findFirst({
    where: { empresa_id: EMPRESA_ID, punto_establecimiento: CATALOGO.PUNTO_ESTABLECIMIENTO },
    select: { id: true, descripcion: true },
  });
  if (!sucursal) {
    log(
      'FATAL',
      `La empresa no tiene ninguna sucursal con punto_establecimiento='${CATALOGO.PUNTO_ESTABLECIMIENTO}'. Creala antes de correr este script.`,
    );
    process.exit(1);
  }
  info(`Sucursal ${CATALOGO.PUNTO_ESTABLECIMIENTO}: ${sucursal.descripcion} (${sucursal.id})`);

  const deposito = await prisma.depositos.findFirst({
    where: { sucursal_id: sucursal.id, es_principal: true },
    select: { id: true, descripcion: true },
  });
  if (!deposito) {
    log(
      'FATAL',
      `La sucursal ${CATALOGO.PUNTO_ESTABLECIMIENTO} no tiene ningún depósito marcado como principal (es_principal=true). Marcá uno antes de correr este script.`,
    );
    process.exit(1);
  }
  info(`Depósito principal: ${deposito.descripcion} (${deposito.id})`);

  const tipoEntrada = await prisma.tipo_movimiento_inventario.findUnique({
    where: { codigo: 'ENTRADA' },
    select: { id: true },
  });
  if (!tipoEntrada) {
    log('FATAL', `No existe el tipo de movimiento 'ENTRADA' en tipo_movimiento_inventario.`);
    process.exit(1);
  }
  raw('');

  // ── Leer Excel ────────────────────────────────────────────────
  if (!fs.existsSync(XLSX_PATH)) {
    log('FATAL', `Archivo no encontrado: ${XLSX_PATH}`);
    process.exit(1);
  }

  const wb = XLSX.readFile(XLSX_PATH);
  if (!wb.SheetNames.includes(HOJA)) {
    log('FATAL', `El archivo no tiene una hoja llamada "${HOJA}". Hojas encontradas: ${wb.SheetNames.join(', ')}`);
    process.exit(1);
  }
  const ws = wb.Sheets[HOJA];
  const rows: any[] = XLSX.utils.sheet_to_json(ws, {
    range: CATALOGO.FILA_ENCABEZADO,
    defval: null,
  });

  info(`Filas leídas del Excel : ${rows.length}`);
  info(`Columnas detectadas    : ${Object.keys(rows[0] ?? {}).join(', ')}`);

  const faltantes = ['Codigo', 'Descripcion', 'Stock', 'Costo', 'Venta'].filter(
    (c) => !(c in (rows[0] ?? {})),
  );
  if (faltantes.length > 0) {
    log(
      'FATAL',
      `Faltan columnas esperadas en la hoja "${HOJA}": ${faltantes.join(', ')}. ` +
        `Revisá que --hoja y la fila de encabezado sean las correctas.`,
    );
    process.exit(1);
  }
  raw('');

  // ── Contadores ────────────────────────────────────────────────
  let creados = 0;
  let actualizados = 0;
  let notasOmitidas = 0;
  let conStockCargado = 0;
  let stockNegativoOmitido = 0;
  const erroresList: string[] = [];
  const anomalias: Anomalia[] = [];

  // ── Procesar filas ────────────────────────────────────────────
  for (const r of rows) {
    const codProducto = toStr(r['Codigo']);
    const descripcion = toStr(r['Descripcion']);
    const stockCrudo = toNumOrNull(r['Stock']);
    const costoCrudo = toNumOrNull(r['Costo']);
    const ventaCrudo = toNumOrNull(r['Venta']);

    // Filas de cierre / notas al pie: no traen ningún dato numérico. En esta
    // planilla es el caso de "TOTAL GARR ... hasta el 04 de SEPTIEMBRE 2026".
    if (stockCrudo === null && costoCrudo === null && ventaCrudo === null) {
      if (descripcion) skip(`Nota al pie omitida: "${descripcion}"`);
      notasOmitidas++;
      continue;
    }

    // Sin código no hay forma de identificar el producto ni de que la
    // reejecución sea idempotente. Se reporta para cargarlo a mano.
    if (!codProducto || !descripcion) {
      anomalias.push({
        motivo: 'SIN CÓDIGO O SIN DESCRIPCIÓN — no se importó',
        detalle: `código: "${codProducto}" | descripción: "${descripcion}" | stock: ${stockCrudo ?? '—'}`,
      });
      warn(`Fila omitida — sin código o descripción: ${JSON.stringify(r)}`);
      continue;
    }

    const label = `[${codProducto}] ${descripcion}`;
    const precioCosto = aGuaranies(costoCrudo);
    const precio = aGuaranies(ventaCrudo);
    const stock = stockCrudo ?? 0;

    // ── Chequeos de sanidad sobre los precios ya convertidos ────
    if (costoCrudo === null && r['Costo'] !== null) {
      anomalias.push({
        motivo: 'COSTO NO NUMÉRICO — se guardó en 0, cargalo a mano',
        detalle: `${label} | celda Costo: "${toStr(r['Costo'])}"`,
      });
    } else if (costoCrudo === null) {
      anomalias.push({ motivo: 'COSTO VACÍO — se guardó en 0', detalle: label });
    }
    if (ventaCrudo === null) {
      anomalias.push({
        motivo: 'PRECIO DE VENTA VACÍO O NO NUMÉRICO — se guardó en 0',
        detalle: `${label} | celda Venta: "${toStr(r['Venta'])}"`,
      });
    }
    // Costo mayor que la venta casi siempre significa que esa fila está en
    // otra unidad que el resto (en esta planilla pasa con "BIDON DE AUXILIO",
    // que trae el costo en guaraníes y la venta en miles).
    if (precioCosto > 0 && precio > 0 && precioCosto > precio) {
      anomalias.push({
        motivo: 'COSTO MAYOR QUE LA VENTA — revisar la escala de esa fila',
        detalle: `${label} | costo ${fmtGs(precioCosto)} > venta ${fmtGs(precio)} (planilla: ${costoCrudo} / ${ventaCrudo})`,
      });
    }

    if (DRY) {
      const notaStock =
        stock > 0 ? `stock ${stock}` : stock < 0 ? `stock negativo (${stock}) — NO se cargaría` : 'sin stock';
      info(`[DRY] ${label} | costo: ${fmtGs(precioCosto)} | venta: ${fmtGs(precio)} | ${notaStock}`);
      creados++;
      if (stock > 0) conStockCargado++;
      if (stock < 0) stockNegativoOmitido++;
      continue;
    }

    try {
      const existing = await prisma.productos.findFirst({
        where: { empresa_id: EMPRESA_ID, cod_producto: codProducto },
        select: { id: true },
      });

      const data = {
        descripcion,
        cod_producto: codProducto,
        precio_costo: precioCosto,
        precio,
        afectacion_id: CATALOGO.AFECTACION_ID,
        unidad_medida_id: CATALOGO.UNIDAD_MEDIDA_ID,
        porcentaje_iva: CATALOGO.IVA_DEFAULT,
        proporcion_iva: CATALOGO.PROPORCION_IVA_DEFAULT,
        tipo: CATALOGO.TIPO_PRODUCTO,
        maneja_inventario: true,
        maneja_lote: false,
      };

      let productoId: string;
      let esNuevo: boolean;

      if (existing) {
        await prisma.productos.update({ where: { id: existing.id }, data });
        productoId = existing.id;
        esNuevo = false;
        skip(`Actualizado (sin tocar stock) | ${label}`);
        actualizados++;
      } else {
        const creado = await prisma.productos.create({
          data: { ...data, empresa_id: EMPRESA_ID },
          select: { id: true },
        });
        productoId = creado.id;
        esNuevo = true;
        ok(`Creado | ${label} | costo ${fmtGs(precioCosto)} | venta ${fmtGs(precio)}`);
        creados++;
      }

      // El stock inicial sólo se carga la primera vez que el producto se crea.
      // Si ya existía (reejecución), no se vuelve a sumar.
      if (esNuevo && stock > 0) {
        await prisma.$transaction([
          prisma.stock_deposito.upsert({
            where: { deposito_id_producto_id: { deposito_id: deposito.id, producto_id: productoId } },
            create: { deposito_id: deposito.id, producto_id: productoId, cantidad_disponible: stock },
            update: { cantidad_disponible: { increment: stock } },
          }),
          prisma.movimientos_inventario.create({
            data: {
              deposito_origen_id: deposito.id,
              producto_id: productoId,
              tipo_movimiento_id: tipoEntrada.id,
              cantidad: stock,
              documento_origen: 'migracion_inicial',
              observaciones: `Carga inicial de stock — migración desde ${path.basename(XLSX_PATH)}`,
            },
          }),
        ]);
        ok(`  └─ stock inicial ${stock} en ${deposito.descripcion}`);
        conStockCargado++;
      } else if (esNuevo && stock < 0) {
        // Misma regla que POST /stock/ajustar: nunca se deja stock negativo.
        warn(`  └─ stock negativo en el archivo (${stock}) — NO se cargó, queda en 0`);
        stockNegativoOmitido++;
        anomalias.push({ motivo: 'STOCK NEGATIVO — quedó en 0', detalle: `${label} | archivo: ${stock}` });
      }
    } catch (e: any) {
      const msg = `${label} | ${e.message}`;
      erroresList.push(msg);
      err(msg);
    }
  }

  // ── Resumen ───────────────────────────────────────────────────
  raw('');
  raw('╔══════════════════════════════════════════════════╗');
  raw('║                  RESUMEN FINAL                   ║');
  raw('╠══════════════════════════════════════════════════╣');
  raw(`║  Total filas Excel         : ${String(rows.length).padStart(6)}             ║`);
  raw(`║  Creados                   : ${String(creados).padStart(6)}             ║`);
  raw(`║  Actualizados              : ${String(actualizados).padStart(6)}             ║`);
  raw(`║  Con stock inicial cargado : ${String(conStockCargado).padStart(6)}             ║`);
  raw(`║  Stock negativo (sin cargar): ${String(stockNegativoOmitido).padStart(5)}             ║`);
  raw(`║  Notas al pie omitidas     : ${String(notasOmitidas).padStart(6)}             ║`);
  raw(`║  Anomalías a revisar       : ${String(anomalias.length).padStart(6)}             ║`);
  raw(`║  Errores                   : ${String(erroresList.length).padStart(6)}             ║`);
  raw('╚══════════════════════════════════════════════════╝');

  if (anomalias.length > 0) {
    raw('');
    raw('── ANOMALÍAS (revisar a mano) ───────────────────');
    const porMotivo = new Map<string, string[]>();
    for (const a of anomalias) {
      const arr = porMotivo.get(a.motivo) ?? [];
      arr.push(a.detalle);
      porMotivo.set(a.motivo, arr);
    }
    for (const [motivo, detalles] of porMotivo) {
      raw(`  ${motivo}  (${detalles.length})`);
      detalles.forEach((d) => raw(`    · ${d}`));
    }
  }

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

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

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