/**
 * ╔══════════════════════════════════════════════════════════════════╗
 * ║   SETUP DE RUBROS PARA AGOGO (rubros + categorías + productos +   ║
 * ║   proveedores)                                                    ║
 * ╠══════════════════════════════════════════════════════════════════╣
 * ║  Deja la empresa lista para operar separada por rubro, en un solo  ║
 * ║  paso y de forma repetible. Ver docs/plan-rubros-multi-sucursal.md ║
 * ║                                                                    ║
 * ║  Qué hace, en orden:                                               ║
 * ║   1. RUBROS      crea/actualiza los rubros y los liga a su         ║
 * ║                  sucursal (se ubica por punto_establecimiento).     ║
 * ║   2. CATEGORÍAS  crea una categoría raíz por rubro, para que el     ║
 * ║                  árbol no arranque vacío y se puedan colgar         ║
 * ║                  subcategorías. NO reasigna productos: eso llega    ║
 * ║                  con los Excel (ver --asignar-categoria).           ║
 * ║   3. PRODUCTOS   marca productos.rubro_id releyendo las MISMAS      ║
 * ║                  fuentes de la migración original. Un código que    ║
 * ║                  aparece en las dos fuentes se desempata por        ║
 * ║                  stock: si sólo tiene existencia en una sucursal,   ║
 * ║                  es de ese rubro. Sin evidencia, queda compartido.  ║
 * ║   4. PROVEEDORES asigna proveedor_rubros según para qué sucursal    ║
 * ║                  se le compró. Con compras en las dos, compartido.  ║
 * ║                                                                    ║
 * ║  POR QUÉ HAY UN OVERRIDE EN EL PRODUCTO: agogo tiene 11.799        ║
 * ║  productos y ninguna categoría, así que heredar el rubro de la     ║
 * ║  categoría no separaba nada. `productos.rubro_id` en NULL sigue    ║
 * ║  significando "heredar de la categoría", que es el caso normal.    ║
 * ║                                                                    ║
 * ║  Idempotente: sólo hace upsert y update por id. Se puede correr    ║
 * ║  las veces que haga falta.                                         ║
 * ╠══════════════════════════════════════════════════════════════════╣
 * ║  Uso:                                                              ║
 * ║    npx ts-node --transpile-only -r tsconfig-paths/register \       ║
 * ║      --project tsconfig.scripts.json \                            ║
 * ║      scripts/migrate-access/setup-rubros-agogo.ts \               ║
 * ║      --empresa=<uuid> --dry                                       ║
 * ║                                                                    ║
 * ║  Flags:                                                            ║
 * ║    --dry                 no escribe nada, sólo informa             ║
 * ║    --reset               limpia rubro de productos y proveedores   ║
 * ║                          antes de reasignar                        ║
 * ║    --dir=<ruta>          carpeta de los archivos de migración      ║
 * ║    --asignar-categoria   además pone la categoría raíz del rubro   ║
 * ║                          a los productos que no tienen ninguna     ║
 * ║    --solo=rubros|categorias|productos|proveedores                  ║
 * ╚══════════════════════════════════════════════════════════════════╝
 */

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

// ══════════════════════════════════════════════════════════════════
//  CONFIG — todo lo específico de agogo vive acá arriba
// ══════════════════════════════════════════════════════════════════
const CONFIG = {
  DEFAULT_DIR: path.resolve(__dirname, '../../scripts/data_script/cliente-agogo'),
  PREFIJO_COLISION: 'TALLER-',
  CSV_ENCODING: 'latin1' as BufferEncoding, // export de Windows, no UTF-8
  CSV_SEP: ';',
  CSV_COL_CODIGO: 1, // mismo índice que usa migrate-productos-taller (COL.CODIGO)

  RUBROS: [
    {
      codigo: 'REGALOS',
      descripcion: 'Regalería',
      color: '#ec4899',
      icono: 'lucide:gift',
      orden: 1,
      // La sucursal se ubica por punto de establecimiento, que es estable.
      establecimiento: '003',
      categoriaRaiz: 'Regalería',
      // Su migración no prefijó nada: sus códigos son los pelados.
      usaPrefijoColision: false,
      // Fuentes XLSX: [archivo, hoja, columna del código, fila de encabezado]
      fuentesXlsx: [
        { archivo: 'Productos2026.xlsx', hoja: 'Productos', columna: 'Item', encabezado: 0 },
        { archivo: 'Entradas09012026.xlsx', hoja: 'Productos', columna: 'Item', encabezado: 0 },
        { archivo: 'PLANILLA DE LUBRICANTES.xlsx', hoja: 'lUBRICANTES', columna: 'Codigo', encabezado: 1 },
      ],
      fuentesCsv: [] as string[],
    },
    {
      codigo: 'TALLER',
      descripcion: 'Taller',
      color: '#f59e0b',
      icono: 'lucide:wrench',
      orden: 2,
      establecimiento: '001',
      categoriaRaiz: 'Taller',
      // migrate-productos-taller renombró a "TALLER-<cod>" los códigos que
      // chocaban con un producto ya existente. Sólo este rubro los usa.
      usaPrefijoColision: true,
      fuentesXlsx: [] as { archivo: string; hoja: string; columna: string; encabezado: number }[],
      fuentesCsv: [path.join('taller', 'REPORTE_INVENTARIO.csv')],
    },
  ],
};

// ══════════════════════════════════════════════════════════════════
//  ARGUMENTOS
// ══════════════════════════════════════════════════════════════════
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 RESET = process.argv.includes('--reset');
const ASIGNAR_CATEGORIA = process.argv.includes('--asignar-categoria');
const DIR = argValue('dir') ? path.resolve(process.cwd(), argValue('dir')!) : CONFIG.DEFAULT_DIR;
const PREFIJO = argValue('prefijo') ?? CONFIG.PREFIJO_COLISION;
const SOLO = (argValue('solo') ?? '').split(',').map((s) => s.trim()).filter(Boolean);
const hacer = (paso: string) => SOLO.length === 0 || SOLO.includes(paso);

const empresaArg = argValue('empresa');
if (!empresaArg) {
  console.error('✗✗FATAL Falta --empresa=<uuid>. Este script no corre contra una empresa por defecto.');
  process.exit(1);
}
const EMPRESA_ID: string = empresaArg;

// ══════════════════════════════════════════════════════════════════
//  LOGGER
// ══════════════════════════════════════════════════════════════════
type LogLevel = 'INFO' | 'OK' | 'WARN' | 'FATAL';
const lineas: string[] = [];
function raw(msg: string) {
  console.log(msg);
  lineas.push(msg);
}
function log(level: LogLevel, msg: string) {
  const prefix: Record<LogLevel, string> = { INFO: 'ℹ ', OK: '✓ ', WARN: '⚠ ', FATAL: '✗✗' };
  raw(prefix[level] + msg);
}
const info = (m: string) => log('INFO', m);
const ok = (m: string) => log('OK', m);
const warn = (m: string) => log('WARN', m);

const limpiarCodigo = (v: unknown) => String(v ?? '').replace(/^'+/, '').trim();

// ══════════════════════════════════════════════════════════════════
//  LECTURA DE FUENTES
// ══════════════════════════════════════════════════════════════════
function leerXlsx(archivo: string, hoja: string, columna: string, encabezado: number): string[] {
  const ruta = path.join(DIR, archivo);
  if (!fs.existsSync(ruta)) {
    warn(`Fuente no encontrada, se omite: ${archivo}`);
    return [];
  }
  const wb = XLSX.readFile(ruta);
  if (!wb.SheetNames.includes(hoja)) {
    warn(`${archivo}: no tiene la hoja "${hoja}" (hojas: ${wb.SheetNames.join(', ')}). Se omite.`);
    return [];
  }
  const filas: any[] = XLSX.utils.sheet_to_json(wb.Sheets[hoja], { range: encabezado, defval: null });
  if (filas.length && !(columna in filas[0])) {
    warn(`${archivo}[${hoja}]: no tiene la columna "${columna}". Se omite.`);
    return [];
  }
  const codigos = filas.map((f) => limpiarCodigo(f[columna])).filter(Boolean);
  info(`  ${archivo}[${hoja}]: ${codigos.length} códigos`);
  return codigos;
}

function leerCsv(relativo: string): string[] {
  const ruta = path.join(DIR, relativo);
  if (!fs.existsSync(ruta)) {
    warn(`Fuente no encontrada, se omite: ${relativo}`);
    return [];
  }
  const codigos = fs
    .readFileSync(ruta, CONFIG.CSV_ENCODING)
    .split(/\r?\n/)
    .slice(1) // encabezado
    .filter((l) => l.trim())
    .map((l) => limpiarCodigo(l.split(CONFIG.CSV_SEP)[CONFIG.CSV_COL_CODIGO]))
    .filter(Boolean);
  info(`  ${relativo}: ${codigos.length} códigos`);
  return codigos;
}

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

async function main() {
  raw('╔════════════════════════════════════════════════════════════╗');
  raw('║   SETUP DE RUBROS                                          ║');
  raw(`║   Empresa: ${EMPRESA_ID.padEnd(47)}║`);
  raw(`║   Modo:    ${(DRY ? 'DRY-RUN (no escribe)' : 'APLICAR').padEnd(47)}║`);
  if (SOLO.length) raw(`║   Sólo:    ${SOLO.join(', ').padEnd(47)}║`);
  raw('╚════════════════════════════════════════════════════════════╝');

  const empresa = await prisma.empresas.findUnique({
    where: { id: EMPRESA_ID },
    select: { razon_social: true },
  });
  if (!empresa) {
    log('FATAL', `No existe ninguna empresa con id ${EMPRESA_ID}`);
    process.exit(1);
  }
  info(`Empresa: ${empresa.razon_social}`);

  const sucursales = await prisma.empresas_sucursales.findMany({
    where: { empresa_id: EMPRESA_ID },
    select: { id: true, descripcion: true, punto_establecimiento: true },
  });

  // ── 1. RUBROS ──────────────────────────────────────────────────
  raw('\n── 1. Rubros ──');
  const rubroPorCodigo = new Map<string, string>();
  for (const def of CONFIG.RUBROS) {
    const sucursal = sucursales.find((s) => s.punto_establecimiento === def.establecimiento);
    if (!sucursal) {
      log('FATAL',
        `No hay sucursal con punto_establecimiento="${def.establecimiento}" para el rubro ${def.codigo}. ` +
        `Sucursales: ${sucursales.map((s) => `${s.punto_establecimiento}=${s.descripcion}`).join(', ')}`);
      process.exit(1);
    }

    if (DRY || !hacer('rubros')) {
      const existente = await prisma.rubros.findFirst({
        where: { empresa_id: EMPRESA_ID, codigo: def.codigo },
        select: { id: true },
      });
      if (existente) rubroPorCodigo.set(def.codigo, existente.id);
      info(`${def.descripcion} (${def.codigo}) → ${sucursal.descripcion}  ${existente ? '[ya existe]' : '[se crearía]'}`);
      continue;
    }

    const rubro = await prisma.rubros.upsert({
      where: { empresa_id_codigo: { empresa_id: EMPRESA_ID, codigo: def.codigo } },
      // El update no pisa color/icono: si alguien los cambió desde el ABM, manda eso.
      update: { descripcion: def.descripcion, activo: true },
      create: {
        empresa_id: EMPRESA_ID,
        codigo: def.codigo,
        descripcion: def.descripcion,
        color: def.color,
        icono: def.icono,
        orden: def.orden,
      },
    });
    rubroPorCodigo.set(def.codigo, rubro.id);
    await prisma.sucursal_rubros.upsert({
      where: { sucursal_id_rubro_id: { sucursal_id: sucursal.id, rubro_id: rubro.id } },
      update: {},
      create: { sucursal_id: sucursal.id, rubro_id: rubro.id },
    });
    ok(`${def.descripcion} (${def.codigo}) → ${sucursal.descripcion}`);
  }

  // ── 2. CATEGORÍAS RAÍZ ─────────────────────────────────────────
  const categoriaPorRubro = new Map<string, string>();
  if (hacer('categorias')) {
    raw('\n── 2. Categorías raíz ──');
    for (const def of CONFIG.RUBROS) {
      const rubroId = rubroPorCodigo.get(def.codigo);
      if (!rubroId) { warn(`${def.categoriaRaiz}: sin rubro todavía, se omite`); continue; }

      const existente = await prisma.categorias.findFirst({
        where: { empresa_id: EMPRESA_ID, descripcion: def.categoriaRaiz, deleted_at: false },
        select: { id: true, rubro_id: true },
      });
      if (DRY) {
        info(`${def.categoriaRaiz} → ${def.descripcion}  ${existente ? '[ya existe]' : '[se crearía]'}`);
        if (existente) categoriaPorRubro.set(def.codigo, existente.id);
        continue;
      }
      const cat = existente
        ? await prisma.categorias.update({
            where: { id: existente.id },
            data: { rubro_id: rubroId, color: def.color, icono: def.icono },
          })
        : await prisma.categorias.create({
            data: {
              empresa_id: EMPRESA_ID,
              descripcion: def.categoriaRaiz,
              rubro_id: rubroId,
              color: def.color,
              icono: def.icono,
              activo: true,
              deleted_at: false,
            },
          });
      categoriaPorRubro.set(def.codigo, cat.id);
      ok(`${def.categoriaRaiz} → ${def.descripcion}`);
    }
  }

  // ── 3. PRODUCTOS ───────────────────────────────────────────────
  if (hacer('productos')) {
    raw('\n── 3. Productos ──');
    const productos = await prisma.productos.findMany({
      where: { empresa_id: EMPRESA_ID, deleted: false },
      select: { id: true, cod_producto: true, categoria_id: true },
    });
    const porCodigo = new Map(productos.filter((p) => p.cod_producto).map((p) => [p.cod_producto as string, p]));
    info(`Productos en el sistema: ${productos.length}`);

    // Códigos declarados por cada rubro en sus fuentes.
    const codigosPorRubro = new Map<string, Set<string>>();
    for (const def of CONFIG.RUBROS) {
      info(`Fuentes de ${def.descripcion}:`);
      const crudos = [
        ...def.fuentesXlsx.flatMap((f) => leerXlsx(f.archivo, f.hoja, f.columna, f.encabezado)),
        ...def.fuentesCsv.flatMap((f) => leerCsv(f)),
      ];
      // El prefijado sólo lo reclama el rubro cuya migración lo aplicó; para el
      // otro, su código es el pelado. Reclamarlo desde los dos lados hacía que
      // el producto original quedara sin dueño y el prefijado, disputado.
      const resueltos = new Set<string>();
      let prefijados = 0;
      for (const cod of new Set(crudos)) {
        const conPrefijo = `${PREFIJO}${cod}`;
        if (def.usaPrefijoColision && porCodigo.has(conPrefijo)) {
          resueltos.add(conPrefijo);
          prefijados++;
        } else if (porCodigo.has(cod)) {
          resueltos.add(cod);
        }
      }
      if (prefijados) info(`  resueltos con prefijo "${PREFIJO}": ${prefijados}`);
      codigosPorRubro.set(def.codigo, resueltos);
    }

    // Desempate por stock: un producto declarado en dos fuentes pertenece al
    // rubro donde efectivamente tiene existencia, si sólo la tiene en uno.
    const stock = await prisma.$queryRaw<{ producto_id: string; establecimiento: string }[]>`
      SELECT DISTINCT sd.producto_id, s.punto_establecimiento AS establecimiento
      FROM stock_deposito sd
      JOIN depositos d ON d.id = sd.deposito_id
      JOIN empresas_sucursales s ON s.id = d.sucursal_id
      WHERE d.empresa_id = ${EMPRESA_ID}::uuid`;
    const establecimientosPorProducto = new Map<string, Set<string>>();
    for (const r of stock) {
      if (!establecimientosPorProducto.has(r.producto_id)) establecimientosPorProducto.set(r.producto_id, new Set());
      establecimientosPorProducto.get(r.producto_id)!.add(r.establecimiento);
    }

    const asignaciones = new Map<string, string[]>(); // codigoRubro → ids
    CONFIG.RUBROS.forEach((d) => asignaciones.set(d.codigo, []));
    let compartidos = 0, sinFuente = 0, desempatados = 0;

    for (const prod of productos) {
      const cod = prod.cod_producto ?? '';
      const declarados = CONFIG.RUBROS.filter((d) => codigosPorRubro.get(d.codigo)!.has(cod));

      if (declarados.length === 1) { asignaciones.get(declarados[0].codigo)!.push(prod.id); continue; }
      if (declarados.length === 0) { sinFuente++; continue; }

      // Declarado en más de una fuente: decide el stock.
      const ests = establecimientosPorProducto.get(prod.id) ?? new Set<string>();
      const candidatos = declarados.filter((d) => ests.has(d.establecimiento));
      if (candidatos.length === 1) {
        asignaciones.get(candidatos[0].codigo)!.push(prod.id);
        desempatados++;
      } else {
        // Sin stock, o con stock en las dos: no se adivina.
        compartidos++;
      }
    }

    for (const def of CONFIG.RUBROS) info(`${def.descripcion}: ${asignaciones.get(def.codigo)!.length}`);
    if (desempatados) info(`Desempatados por stock exclusivo: ${desempatados}`);
    if (compartidos) warn(`En más de una fuente sin stock que desempate → quedan compartidos: ${compartidos}`);
    if (sinFuente) warn(`Sin fuente conocida → quedan compartidos: ${sinFuente}`);
    const cubiertos = CONFIG.RUBROS.reduce((n, d) => n + asignaciones.get(d.codigo)!.length, 0);
    info(`Cobertura: ${cubiertos}/${productos.length} (${((cubiertos / productos.length) * 100).toFixed(1)}%)`);

    if (!DRY) {
      if (RESET) {
        const r = await prisma.productos.updateMany({
          where: { empresa_id: EMPRESA_ID, rubro_id: { not: null } },
          data: { rubro_id: null },
        });
        info(`Reset: ${r.count} productos quedaron sin rubro`);
      }
      // En lotes: un IN con 10.000 uuids revienta el límite de parámetros.
      const LOTE = 500;
      for (const def of CONFIG.RUBROS) {
        const ids = asignaciones.get(def.codigo)!;
        const rubroId = rubroPorCodigo.get(def.codigo);
        if (!rubroId || !ids.length) continue;
        let n = 0;
        for (let i = 0; i < ids.length; i += LOTE) {
          const r = await prisma.productos.updateMany({
            where: { id: { in: ids.slice(i, i + LOTE) } },
            data: { rubro_id: rubroId },
          });
          n += r.count;
        }
        ok(`${def.descripcion}: ${n} productos marcados`);

        if (ASIGNAR_CATEGORIA) {
          const catId = categoriaPorRubro.get(def.codigo);
          if (catId) {
            let c = 0;
            for (let i = 0; i < ids.length; i += LOTE) {
              const r = await prisma.productos.updateMany({
                where: { id: { in: ids.slice(i, i + LOTE) }, categoria_id: null },
                data: { categoria_id: catId },
              });
              c += r.count;
            }
            ok(`${def.descripcion}: ${c} productos sin categoría → "${def.categoriaRaiz}"`);
          }
        }
      }
    }
  }

  // ── 4. PROVEEDORES ─────────────────────────────────────────────
  if (hacer('proveedores')) {
    raw('\n── 4. Proveedores ──');
    const filas = await prisma.$queryRaw<{ proveedor_id: string; establecimiento: string }[]>`
      SELECT DISTINCT c.proveedor_id, s.punto_establecimiento AS establecimiento
      FROM compra_cab c
      JOIN empresas_sucursales s ON s.id = c.sucursal_id
      WHERE c.empresa_id = ${EMPRESA_ID}::uuid AND c.proveedor_id IS NOT NULL`;

    const estsPorProveedor = new Map<string, Set<string>>();
    for (const f of filas) {
      if (!estsPorProveedor.has(f.proveedor_id)) estsPorProveedor.set(f.proveedor_id, new Set());
      estsPorProveedor.get(f.proveedor_id)!.add(f.establecimiento);
    }

    const aAsignar: { proveedor_id: string; rubro_id: string; rubro: string }[] = [];
    let ambiguos = 0;
    for (const [proveedor_id, ests] of estsPorProveedor) {
      const rubros = CONFIG.RUBROS.filter((d) => ests.has(d.establecimiento));
      if (rubros.length !== 1) { ambiguos++; continue; }
      const rubro_id = rubroPorCodigo.get(rubros[0].codigo);
      if (rubro_id) aAsignar.push({ proveedor_id, rubro_id, rubro: rubros[0].descripcion });
    }
    const total = await prisma.proveedores.count({ where: { empresa_id: EMPRESA_ID } });
    const sinCompras = total - estsPorProveedor.size;
    info(`Proveedores: ${total} | con compras: ${estsPorProveedor.size} | sin compras (compartidos): ${sinCompras}`);
    if (ambiguos) warn(`Compraron para más de un rubro → quedan compartidos: ${ambiguos}`);

    if (DRY) {
      info(`Se asignarían: ${aAsignar.length}`);
    } else {
      if (RESET) {
        const r = await prisma.proveedor_rubros.deleteMany({
          where: { rubros: { empresa_id: EMPRESA_ID } },
        });
        info(`Reset: ${r.count} asignaciones de proveedor borradas`);
      }
      for (const a of aAsignar) {
        await prisma.proveedor_rubros.upsert({
          where: { proveedor_id_rubro_id: { proveedor_id: a.proveedor_id, rubro_id: a.rubro_id } },
          update: {},
          create: { proveedor_id: a.proveedor_id, rubro_id: a.rubro_id },
        });
      }
      ok(`Proveedores asignados: ${aAsignar.length}`);
    }
  }

  if (DRY) raw('\n── DRY-RUN: no se escribió nada ──');

  const archivoLog = path.resolve(
    __dirname,
    `reporte-setup-rubros-${new Date().toISOString().replace(/[:.]/g, '-')}.log`,
  );
  fs.writeFileSync(archivoLog, lineas.join('\n'), 'utf8');
  console.log(`\nReporte: ${archivoLog}`);
}

main()
  .catch((e) => {
    log('FATAL', e instanceof Error ? e.message : String(e));
    process.exitCode = 1;
  })
  .finally(async () => {
    await prisma.$disconnect();
  });
