/**
 * Motor de conciliación PURO (sin DB, sin efectos secundarios).
 * autoMatch cruza movimientos propios vs banco por monto + tipo compatible +
 * fecha ±N días, priorizando referencia/descripción coincidentes. Portado del
 * scoring de TesConciliacionService.findBestMovimiento/scoreCandidato.
 */

export interface MovMatchable {
  id: string;
  fecha: string; // YYYY-MM-DD
  tipo: string; // DEBITO | CREDITO
  importe: number;
  concepto?: string | null;
  referencia?: string | null;
  confianza_ia?: number;
}

export interface MatchResult {
  movimiento_propio_id: string;
  movimiento_banco_id: string;
  tipo_match: 'AUTO';
  diferencia_importe: number;
}

export interface AutoMatchOpts {
  toleranciaDias?: number;
  toleranciaMonto?: number;
  /**
   * Tolerancia porcentual sobre el importe (0.01 = 1%). Permite conciliar
   * movimientos cuyo importe difiere poco (comisión/ITF incluidos por el banco)
   * y marcarlos como "con diferencia". Se toma el máximo entre `toleranciaMonto`
   * y `importe * toleranciaPct`, con tope `toleranciaPctCap`.
   */
  toleranciaPct?: number;
  toleranciaPctCap?: number;
  /** Movimientos con confianza < este umbral no se auto-concilian. */
  umbralConfianza?: number;
}

function normalizeText(s: string): string {
  return (s || '')
    .toLowerCase()
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .replace(/[^a-z0-9\s]/g, ' ')
    .replace(/\s+/g, ' ')
    .trim();
}

function scoreCandidato(c: { diffMonto: number; diffDias: number; refIgual: boolean; descCoincide: boolean }): number {
  let score = c.diffMonto * 1000 + c.diffDias * 10;
  if (c.refIgual) score -= 10;
  if (c.descCoincide) score -= 5;
  return score;
}

export interface Sugerencia {
  movimiento_propio_id: string;
  movimiento_banco_id: string;
  motivo: string;
  confianza: number; // 0..1
  diferencia_importe: number;
}

/**
 * Propone pares plausibles entre movimientos NO conciliados (tolerancia amplia:
 * fecha ±7 días y monto ±5%). Para los "casi cruzan" que el auto-match exacto
 * descartó. Devuelve las mejores sugerencias, una por movimiento propio.
 */
export function sugerirPares(pendientesPropio: MovMatchable[], pendientesBanco: MovMatchable[]): Sugerencia[] {
  const usados = new Set<string>();
  const sugerencias: Sugerencia[] = [];
  const ms = (f: string) => new Date(f).getTime();

  for (const p of pendientesPropio) {
    const candidatos = pendientesBanco
      .filter((b) => !usados.has(b.id) && b.tipo === p.tipo)
      .map((b) => {
        const diffMonto = Math.abs(b.importe - p.importe);
        const pctMonto = p.importe ? diffMonto / p.importe : 1;
        const diffDias = Math.abs(ms(b.fecha) - ms(p.fecha)) / 86_400_000;
        const refP = normalizeText(p.referencia || '');
        const refB = normalizeText(b.referencia || '');
        const descP = normalizeText(p.concepto || '');
        const descB = normalizeText(b.concepto || '');
        const refIgual = Boolean(refP && refB && refP === refB);
        const descCoincide = Boolean(descP && descB) && (descP.includes(descB.slice(0, 10)) || descB.includes(descP.slice(0, 10)));
        return { b, diffMonto, pctMonto, diffDias, refIgual, descCoincide };
      })
      .filter((c) => c.diffDias <= 7 && (c.pctMonto <= 0.05 || c.refIgual || c.descCoincide));

    if (!candidatos.length) continue;
    candidatos.sort((a, b) => scoreCandidato(a) - scoreCandidato(b));
    const mejor = candidatos[0];
    usados.add(mejor.b.id);

    const motivos: string[] = [];
    if (mejor.refIgual) motivos.push('misma referencia');
    if (mejor.descCoincide) motivos.push('concepto similar');
    if (mejor.diffMonto === 0) motivos.push('mismo importe');
    else if (mejor.pctMonto <= 0.05) motivos.push(`importe casi igual (dif. ${Math.round(mejor.diffMonto)})`);
    if (mejor.diffDias >= 1) motivos.push(`fecha corrida ${Math.round(mejor.diffDias)} día(s)`);

    // Confianza: alta si ref/desc coincide o monto exacto; baja cuanto más se aleja.
    const confianza = Math.max(0.4, Math.min(0.95, 1 - mejor.pctMonto * 4 - mejor.diffDias * 0.03 + (mejor.refIgual ? 0.2 : 0)));
    sugerencias.push({
      movimiento_propio_id: p.id,
      movimiento_banco_id: mejor.b.id,
      motivo: motivos.join(' · ') || 'posible correspondencia',
      confianza: Math.round(confianza * 100) / 100,
      diferencia_importe: Math.round((p.importe - mejor.b.importe) * 100) / 100,
    });
  }
  return sugerencias;
}

export function autoMatch(propios: MovMatchable[], banco: MovMatchable[], opts: AutoMatchOpts = {}): MatchResult[] {
  const toleranciaDias = opts.toleranciaDias ?? 2;
  const toleranciaMonto = opts.toleranciaMonto ?? 1;
  const toleranciaPct = opts.toleranciaPct ?? 0.01; // 1%
  const toleranciaPctCap = opts.toleranciaPctCap ?? 50_000;
  const umbralConfianza = opts.umbralConfianza ?? 0.5;

  const usados = new Set<string>();
  const matches: MatchResult[] = [];
  const ms = (f: string) => new Date(f).getTime();

  for (const p of propios) {
    if ((p.confianza_ia ?? 1) < umbralConfianza) continue; // baja confianza no auto-concilia

    const refP = normalizeText(p.referencia || '');
    const descP = normalizeText(p.concepto || '');
    const fechaP = ms(p.fecha);
    // Diferencia máxima aceptada para este movimiento (fija o % del importe, con tope).
    const maxDiff = Math.max(toleranciaMonto, Math.min(Math.abs(p.importe) * toleranciaPct, toleranciaPctCap));

    const candidatos = banco
      .filter((b) => !usados.has(b.id))
      .filter((b) => b.tipo === p.tipo)
      .map((b) => {
        const diffMonto = Math.abs(b.importe - p.importe);
        const diffDias = Math.abs(ms(b.fecha) - fechaP) / 86_400_000;
        const refB = normalizeText(b.referencia || '');
        const descB = normalizeText(b.concepto || '');
        const refIgual = Boolean(refP && refB && refP === refB);
        const descCoincide =
          Boolean(descP && descB) &&
          (descP.includes(descB.slice(0, 12)) || descB.includes(descP.slice(0, 12)));
        return { b, diffMonto, diffDias, refIgual, descCoincide };
      })
      .filter((c) => c.diffDias <= toleranciaDias)
      .filter((c) => c.diffMonto <= maxDiff);

    if (!candidatos.length) continue;

    candidatos.sort((a, b) => {
      const sa = scoreCandidato(a);
      const sb = scoreCandidato(b);
      if (sa !== sb) return sa - sb;
      return a.diffDias - b.diffDias;
    });

    const mejor = candidatos[0];
    usados.add(mejor.b.id);
    matches.push({
      movimiento_propio_id: p.id,
      movimiento_banco_id: mejor.b.id,
      tipo_match: 'AUTO',
      diferencia_importe: Math.round((p.importe - mejor.b.importe) * 100) / 100,
    });
  }

  return matches;
}
