/**
 * Compara modelos para el extractor (plan §6.5) sobre evals/extractor-casos.json.
 *
 *   pnpm build && pnpm eval:extractor -- claude-opus-5-5 claude-haiku-5-5
 *
 * Se califica el PLAN que resulta (qué consultaría el agente), no la intención
 * literal: dos intenciones distintas que llevan a la misma consulta son igual de
 * buenas. Mide además latencia y costo. Gasta tokens reales (centavos).
 */
import 'dotenv/config';
import { readFileSync, mkdirSync, writeFileSync } from 'fs';
import { join } from 'path';
import { normalizarReferencia } from 'src/catalogo/normalizar-referencia';
import { AnthropicService } from 'src/llm/anthropic.service';
import { costoUsd } from 'src/llm/precios';
import { ExtractorService } from 'src/pipeline/extractor.service';
import { decidirPlan, Plan } from 'src/pipeline/maquina-estados';
import { ESTADO_INICIAL, Extraccion } from 'src/pipeline/tipos';

type Alternativa = 'pide_referencia' | 'sin_consulta' | { herramienta: string; referencia?: string; tipo?: string };
interface Caso {
  texto: string;
  plan: Alternativa[];
}

interface Resultado {
  texto: string;
  ok: boolean;
  obtenido: string;
  extraccion: Extraccion;
  ms: number;
  costo: number;
}

const CONCURRENCIA = 4;

function describir(plan: Plan): string {
  const c = plan.consultas[0];
  if (!c) return plan.pideReferencia ? 'pide_referencia' : 'sin_consulta';
  const ref = typeof c.input.referencia === 'string' ? ` ${c.input.referencia}` : '';
  const tipo = typeof c.input.tipo === 'string' && c.input.tipo !== 'CUALQUIERA' ? ` ${c.input.tipo}` : '';
  return `${c.herramienta}${ref}${tipo}`;
}

/** Número sin ceros a la izquierda, para comparar "0000593" con "001-002-0000593". */
function numero(ref: unknown): string | null {
  if (typeof ref !== 'string') return null;
  const r = normalizarReferencia(ref);
  return r?.clase === 'numero' ? String(Number(r.numero)) : (r?.cdc ?? null);
}

function coincide(plan: Plan, alternativas: Alternativa[]): boolean {
  const c = plan.consultas[0];
  return alternativas.some((alt) => {
    if (alt === 'pide_referencia') return !c && !!plan.pideReferencia;
    if (alt === 'sin_consulta') return !c && !plan.pideReferencia;
    if (!c || c.herramienta !== alt.herramienta) return false;
    if (alt.referencia && numero(c.input.referencia) !== numero(alt.referencia)) return false;
    if (alt.tipo && c.input.tipo !== alt.tipo) return false;
    return true;
  });
}

async function enLotes<T, R>(items: T[], n: number, fn: (x: T) => Promise<R>): Promise<R[]> {
  const salida: R[] = new Array(items.length);
  let i = 0;
  await Promise.all(
    Array.from({ length: n }, async () => {
      while (i < items.length) {
        const j = i++;
        salida[j] = await fn(items[j]);
      }
    }),
  );
  return salida;
}

const percentil = (xs: number[], p: number) => {
  const o = [...xs].sort((a, b) => a - b);
  return o[Math.min(o.length - 1, Math.floor((p / 100) * o.length))] ?? 0;
};

async function main() {
  const modelos = process.argv.slice(2).filter((a) => a !== '--');
  if (modelos.length === 0)
    throw new Error('Pasá los modelos: pnpm eval:extractor -- claude-opus-5-5 claude-haiku-5-5');
  const { casos } = JSON.parse(readFileSync(join(process.cwd(), 'evals/extractor-casos.json'), 'utf8')) as {
    casos: Caso[];
  };
  const extractor = new ExtractorService(new AnthropicService());
  const informe: Record<string, Resultado[]> = {};

  for (const modelo of modelos) {
    informe[modelo] = await enLotes(casos, CONCURRENCIA, async (caso) => {
      const r = await extractor.extraer([], caso.texto, modelo);
      const plan = decidirPlan(r.extraccion, caso.texto, { ...ESTADO_INICIAL });
      return {
        texto: caso.texto,
        ok: coincide(plan, caso.plan),
        obtenido: describir(plan),
        extraccion: r.extraccion,
        ms: r.ms,
        costo: r.uso ? costoUsd(modelo, r.uso) : 0,
      };
    });
  }

  console.log(`\n${casos.length} casos · effort ${process.env.EXTRACTOR_EFFORT ?? 'low'}\n`);
  console.log(
    'modelo'.padEnd(20),
    'aciertos'.padStart(9),
    'p50'.padStart(8),
    'p95'.padStart(8),
    'costo/1000'.padStart(12),
  );
  for (const [modelo, rs] of Object.entries(informe)) {
    const ms = rs.map((r) => r.ms);
    const aciertos = rs.filter((r) => r.ok).length;
    const costoMil = (rs.reduce((a, r) => a + r.costo, 0) / rs.length) * 1000;
    console.log(
      modelo.padEnd(20),
      `${aciertos}/${rs.length}`.padStart(9),
      `${(percentil(ms, 50) / 1000).toFixed(2)}s`.padStart(8),
      `${(percentil(ms, 95) / 1000).toFixed(2)}s`.padStart(8),
      `US$ ${costoMil.toFixed(2)}`.padStart(12),
    );
  }
  for (const [modelo, rs] of Object.entries(informe)) {
    const fallos = rs.filter((r) => !r.ok);
    if (fallos.length === 0) continue;
    console.log(`\nFallos de ${modelo}:`);
    for (const f of fallos) {
      const esperado = casos.find((c) => c.texto === f.texto)?.plan;
      console.log(
        `  ✗ "${f.texto}"\n      obtuvo: ${f.obtenido} · extracción ${JSON.stringify(f.extraccion)}\n      esperaba: ${JSON.stringify(esperado)}`,
      );
    }
  }

  const dir = join(process.cwd(), 'evals/resultados');
  mkdirSync(dir, { recursive: true });
  const archivo = join(dir, `extractor-${new Date().toISOString().replace(/[:.]/g, '-')}.json`);
  writeFileSync(archivo, JSON.stringify(informe, null, 2));
  console.log(`\nDetalle: ${archivo}`);
}

main().catch((err) => {
  console.error(err);
  process.exitCode = 1;
});
