// international-results.csv → public/data/ratings.json
// Walks every men's international 1872→now in date order, maintaining a
// World-Football-Elo rating per team. Then calibrates the goals model
// (goals-per-Elo slope, mean goals) on the modern era and MLE-fits Dixon-Coles
// rho. The runtime re-uses these ratings + params to predict and simulate.
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { canonicalTeam, ALL_TEAMS } from '../src/lib/teams';
import { HOME_ADV_ELO, importanceWeight, eloDelta, expectedHome } from '../src/lib/model/elo';
import { poissonPmf } from '../src/lib/model/poisson';

const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const CSV = join(ROOT, 'data', 'raw', 'international-results.csv');
const OUT_DIR = join(ROOT, 'public', 'data');

const START_ELO = 1500;
const CALIB_FROM = '2010-01-01'; // modern-era window for the goals calibration

interface Row {
  date: string; home: string; away: string; hs: number; as: number;
  tournament: string; neutral: boolean;
}

function parseCsv(text: string): Row[] {
  const lines = text.split('\n');
  const rows: Row[] = [];
  for (let i = 1; i < lines.length; i++) {
    const line = lines[i];
    if (!line) continue;
    const c = line.split(','); // this dataset has no embedded commas
    if (c.length < 9) continue;
    const hs = Number(c[3]);
    const as = Number(c[4]);
    if (!Number.isFinite(hs) || !Number.isFinite(as)) continue; // skip NA / future fixtures
    rows.push({
      date: c[0]!, home: c[1]!, away: c[2]!, hs, as,
      tournament: c[5]!, neutral: c[8]!.trim().toUpperCase() === 'TRUE',
    });
  }
  rows.sort((a, b) => a.date.localeCompare(b.date));
  return rows;
}

function dcTau(h: number, a: number, lh: number, la: number, rho: number): number {
  if (h === 0 && a === 0) return 1 - lh * la * rho;
  if (h === 0 && a === 1) return 1 + lh * rho;
  if (h === 1 && a === 0) return 1 + la * rho;
  if (h === 1 && a === 1) return 1 - rho;
  return 1;
}

function main(): void {
  const rows = parseCsv(readFileSync(CSV, 'utf8'));
  const elo = new Map<string, number>();
  const get = (t: string) => elo.get(t) ?? START_ELO;

  // Calibration accumulators (modern era only).
  let sumTotal = 0, nCalib = 0;
  let sxy = 0, sxx = 0; // regression of goal-diff on effective Elo diff
  const calib: { d: number; h: number; a: number }[] = [];

  let lastDate = '';
  for (const r of rows) {
    const home = canonicalTeam(r.home);
    const away = canonicalTeam(r.away);
    const eh = get(home);
    const ea = get(away);
    const homeAdv = r.neutral ? 0 : HOME_ADV_ELO;
    const effDiff = eh - ea + homeAdv;

    if (r.date >= CALIB_FROM) {
      const gd = r.hs - r.as;
      sumTotal += r.hs + r.as; nCalib++;
      sxy += effDiff * gd; sxx += effDiff * effDiff;
      calib.push({ d: effDiff, h: r.hs, a: r.as });
    }

    const k = importanceWeight(r.tournament);
    const delta = eloDelta(r.hs, r.as, eh, ea, k, homeAdv);
    elo.set(home, eh + delta);
    elo.set(away, ea - delta);
    lastDate = r.date;
  }

  const avgGoals = sumTotal / Math.max(1, nCalib);
  const goalsPerElo = sxy / Math.max(1e-9, sxx);

  // MLE-fit Dixon-Coles rho on the modern era (classic DC likelihood: the four
  // low-score cells, no renormalization). Grid search keeps it simple + robust.
  let bestRho = 0, bestLL = -Infinity;
  for (let rho = -0.2; rho <= 0.1 + 1e-9; rho += 0.01) {
    let ll = 0;
    for (const m of calib) {
      const sup = goalsPerElo * m.d;
      const lh = Math.min(8, Math.max(0.15, avgGoals / 2 + sup / 2));
      const la = Math.min(8, Math.max(0.15, avgGoals / 2 - sup / 2));
      const p = poissonPmf(lh, m.h) * poissonPmf(la, m.a) * dcTau(m.h, m.a, lh, la, rho);
      ll += Math.log(Math.max(1e-12, p));
    }
    if (ll > bestLL) { bestLL = ll; bestRho = rho; }
  }
  const rho = Math.round(bestRho * 100) / 100;

  // Sanity: home win rate the model expects vs reality, on the calib window.
  let expHome = 0, n2 = 0;
  for (const r of rows) {
    if (r.date < CALIB_FROM) continue;
    const homeAdv = r.neutral ? 0 : HOME_ADV_ELO;
    // recompute pre-match would need stored ratings; use final as a rough proxy
    expHome += expectedHome(get(canonicalTeam(r.home)), get(canonicalTeam(r.away)), homeAdv);
    n2++;
  }

  const ratings: Record<string, number> = {};
  for (const [team, r] of [...elo.entries()].sort((a, b) => b[1] - a[1])) {
    ratings[team] = Math.round(r * 10) / 10;
  }
  // Ensure every WC team has a rating (default for any never seen).
  for (const t of ALL_TEAMS) if (!(t in ratings)) ratings[t] = START_ELO;

  const out = {
    generatedAt: new Date().toISOString(),
    asOf: lastDate,
    matchesProcessed: rows.length,
    calibrationFrom: CALIB_FROM,
    calibrationMatches: nCalib,
    params: {
      goalsPerElo: Math.round(goalsPerElo * 1e6) / 1e6,
      avgGoals: Math.round(avgGoals * 1000) / 1000,
      rho,
      homeAdvElo: HOME_ADV_ELO,
    },
    ratings,
  };

  mkdirSync(OUT_DIR, { recursive: true });
  writeFileSync(join(OUT_DIR, 'ratings.json'), JSON.stringify(out));

  const top = Object.entries(ratings).slice(0, 12).map(([t, r]) => `${t} ${Math.round(r)}`).join(', ');
  console.log(`ratings → public/data/ratings.json  (${rows.length} matches, asOf ${lastDate})`);
  console.log(`params: goalsPerElo=${out.params.goalsPerElo} avgGoals=${out.params.avgGoals} rho=${rho} homeAdvElo=${HOME_ADV_ELO}`);
  console.log(`top: ${top}`);
}

main();
