/**
 * One-shot: normaliza el banco del cheque contra el catálogo `bancos`, en las
 * dos tablas donde se guarda como texto:
 *
 *   tes_cheques.banco_emisor            VARCHAR(100) → nombre completo
 *   recibo_cobro_detalle.banco_emisor   VARCHAR(20)  → `nombre_corto`, + banco_id
 *
 * `factura_forma_pagos.dbcoemi` NO se toca: son documentos fiscales ya emitidos
 * y enviados a SIFEN; cambiarles un campo los desincroniza de lo que el SET
 * tiene registrado. (Hoy además está vacío.)
 *
 * Por qué: el campo era texto libre y hoy conviven 27 grafías para ~12 bancos
 * ("BANCO FAMILIAR", "Banco Familiar S.A.E.C.A.", "BCO. FAMILIAR", "BANCO
 * FAMILAR"...). Eso rompe el filtro por banco, el agrupado en los reportes y
 * las alertas del dashboard, que tratan cada variante como un banco distinto.
 * El alta ya usa el catálogo; esto alinea lo que quedó de antes.
 *
 * Cómo empareja: normaliza (mayúsculas, sin acentos, sin ruido tipo BANCO /
 * BCO. / S.A. / S.A.E.C.A.) y exige que los tokens del valor guardado estén
 * contenidos en los del banco del catálogo. Un único candidato → se aplica.
 * Cero o varios candidatos → se reporta y NO se toca.
 *
 * Antes de escribir guarda un JSON con los valores originales, para poder
 * volver atrás.
 *
 * Uso:
 *   pnpm exec ts-node scripts/backfill-normalizar-banco-emisor.ts [--empresa=<id>] [--apply]
 *
 * Sin --apply → dry-run (default seguro).
 */
import 'dotenv/config';
import { writeFileSync } from 'node:fs';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

function parseArgs(argv: string[]) {
  const empresaArg = argv.find((a) => a.startsWith('--empresa='));
  return {
    apply: argv.includes('--apply'),
    empresa: empresaArg ? empresaArg.split('=')[1] : undefined,
  };
}

// Palabras que no distinguen un banco de otro y sólo agregan ruido al comparar.
const RUIDO = new Set([
  'BANCO', 'BCO', 'BANK', 'SA', 'SAECA', 'NA', 'SUCURSAL', 'DEL', 'DE', 'LA', 'EL',
]);

/** MAYÚSCULAS, sin acentos, sin puntuación, sin palabras de ruido. */
function tokens(valor: string): string[] {
  return valor
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .toUpperCase()
    .replace(/[^A-Z0-9 ]+/g, ' ')
    .split(/\s+/)
    .filter((t) => t && !RUIDO.has(t));
}

/** Distancia de edición, para tolerar un typo de una letra ("FAMILAR"). */
function distancia(a: string, b: string): number {
  const m = Array.from({ length: a.length + 1 }, (_, i) =>
    Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)),
  );
  for (let i = 1; i <= a.length; i++) {
    for (let j = 1; j <= b.length; j++) {
      m[i][j] = Math.min(
        m[i - 1][j] + 1,
        m[i][j - 1] + 1,
        m[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
      );
    }
  }
  return m[a.length][b.length];
}

/** Un token del valor guardado "coincide" si está o difiere en una letra. */
function tokenCoincide(t: string, candidatos: string[]): boolean {
  return candidatos.some((c) => c === t || (t.length >= 5 && distancia(t, c) <= 1));
}

async function main() {
  const args = parseArgs(process.argv.slice(2));

  const bancos = await prisma.bancos.findMany({
    where: { activo: true },
    select: { id: true, descripcion: true, nombre_corto: true },
  });
  const catalogo = bancos
    .filter((b) => b.descripcion?.trim())
    .map((b) => ({
      id: b.id,
      nombre: b.descripcion!.trim(),
      corto: b.nombre_corto?.trim() || b.descripcion!.trim().slice(0, 20).trim(),
      tokens: tokens(b.descripcion!),
    }))
    // "Otro / Sin banco" no es un banco: nunca debe ganar un emparejamiento.
    .filter((b) => !/^OTRO/i.test(b.nombre));

  /** Único banco del catálogo compatible con `valor`, o null si hay 0 ó 2+. */
  const emparejar = (valor: string) => {
    const propios = tokens(valor);
    if (propios.length === 0) return null;
    const cand = catalogo.filter((b) => propios.every((t) => tokenCoincide(t, b.tokens)));
    return cand.length === 1 ? cand[0] : null;
  };

  const cheques = await prisma.tes_cheques.findMany({
    where: {
      banco_emisor: { not: null },
      ...(args.empresa ? { empresa_id: args.empresa } : {}),
    },
    select: { id: true, banco_emisor: true },
  });

  const canonicos = new Set(catalogo.map((b) => b.nombre));
  const cambios: { id: string; antes: string; despues: string }[] = [];
  const ambiguos = new Map<string, number>();

  for (const ch of cheques) {
    const actual = ch.banco_emisor!.trim();
    if (!actual || canonicos.has(actual)) continue; // ya está canónico

    const banco = emparejar(actual);
    if (banco) {
      cambios.push({ id: ch.id, antes: actual, despues: banco.nombre });
    } else {
      ambiguos.set(actual, (ambiguos.get(actual) ?? 0) + 1);
    }
  }

  const porMapeo = new Map<string, number>();
  for (const c of cambios) {
    const k = `${c.antes}  →  ${c.despues}`;
    porMapeo.set(k, (porMapeo.get(k) ?? 0) + 1);
  }

  console.log(`\n── tes_cheques ──`);
  console.log(`Cheques con banco cargado: ${cheques.length}`);
  console.log(`A normalizar: ${cambios.length}\n`);
  for (const [k, n] of [...porMapeo.entries()].sort((a, b) => b[1] - a[1])) {
    console.log(`  ${String(n).padStart(4)}  ${k}`);
  }

  if (ambiguos.size > 0) {
    console.log(`\nSin emparejar (quedan como están, revisalos a mano):`);
    for (const [v, n] of [...ambiguos.entries()].sort((a, b) => b[1] - a[1])) {
      console.log(`  ${String(n).padStart(4)}  "${v}"`);
    }
  }

  // ── Fase 2: recibo_cobro_detalle. Acá el campo es VARCHAR(20), así que se
  //    guarda `nombre_corto`, y además se completa `banco_id`, que la columna
  //    existe desde siempre y nunca se llenó (0 de 17 filas la tienen).
  const detalles = await prisma.recibo_cobro_detalle.findMany({
    where: { banco_emisor: { not: null } },
    select: { id: true, banco_emisor: true, banco_id: true },
  });

  const cambiosDet: { id: string; antes: string; despues: string; bancoId: string }[] = [];
  const ambiguosDet = new Map<string, number>();

  for (const d of detalles) {
    const actual = d.banco_emisor!.trim();
    if (!actual) continue;
    const banco = emparejar(actual);
    if (!banco) {
      ambiguosDet.set(actual, (ambiguosDet.get(actual) ?? 0) + 1);
      continue;
    }
    // Ya canónico y con id: nada que hacer.
    if (actual === banco.corto && d.banco_id === banco.id) continue;
    cambiosDet.push({ id: d.id, antes: actual, despues: banco.corto, bancoId: banco.id });
  }

  const porMapeoDet = new Map<string, number>();
  for (const c of cambiosDet) {
    const k = `${c.antes}  →  ${c.despues}`;
    porMapeoDet.set(k, (porMapeoDet.get(k) ?? 0) + 1);
  }

  console.log(`\n── recibo_cobro_detalle ──`);
  console.log(`Detalles con banco cargado: ${detalles.length}`);
  console.log(`A normalizar: ${cambiosDet.length}\n`);
  for (const [k, n] of [...porMapeoDet.entries()].sort((a, b) => b[1] - a[1])) {
    console.log(`  ${String(n).padStart(4)}  ${k}`);
  }
  if (ambiguosDet.size > 0) {
    console.log(`\nSin emparejar (quedan como están, revisalos a mano):`);
    for (const [v, n] of [...ambiguosDet.entries()].sort((a, b) => b[1] - a[1])) {
      console.log(`  ${String(n).padStart(4)}  "${v}"`);
    }
  }

  console.log(`\nfactura_forma_pagos.dbcoemi: no se toca (documentos fiscales ya emitidos).`);

  if (cambios.length === 0 && cambiosDet.length === 0) {
    console.log('\nNada que aplicar.\n');
    await prisma.$disconnect();
    return;
  }

  if (!args.apply) {
    console.log('\nDRY-RUN — no se escribieron cambios. Corré con --apply para aplicar.\n');
    await prisma.$disconnect();
    return;
  }

  const backup = `banco-emisor-backup-${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
  writeFileSync(
    backup,
    JSON.stringify({ tes_cheques: cambios, recibo_cobro_detalle: cambiosDet }, null, 2),
    'utf-8',
  );
  console.log(`\nBackup de los valores originales: ${backup}`);

  // Un update por valor distinto, no por fila: son ~25 queries en vez de ~380.
  let total = 0;
  const grupos = new Map<string, { despues: string; ids: string[] }>();
  for (const c of cambios) {
    const g = grupos.get(c.antes) ?? { despues: c.despues, ids: [] };
    g.ids.push(c.id);
    grupos.set(c.antes, g);
  }
  for (const [antes, g] of grupos) {
    const r = await prisma.tes_cheques.updateMany({
      where: { id: { in: g.ids }, banco_emisor: antes },
      data: { banco_emisor: g.despues, updated_at: new Date() },
    });
    total += r.count;
  }

  // Fase 2: fila por fila, porque cada una lleva además su banco_id.
  let totalDet = 0;
  for (const c of cambiosDet) {
    const r = await prisma.recibo_cobro_detalle.updateMany({
      where: { id: c.id, banco_emisor: c.antes },
      data: { banco_emisor: c.despues, banco_id: c.bancoId },
    });
    totalDet += r.count;
  }

  console.log(`\nAPLICADO ✅`);
  console.log(`  tes_cheques:           ${total} normalizados`);
  console.log(`  recibo_cobro_detalle:  ${totalDet} normalizados\n`);
  await prisma.$disconnect();
}

main()
  .then(() => process.exit(0))
  .catch((e) => {
    console.error('Falló el backfill:', e);
    process.exit(1);
  });
