// Bivariate-Poisson scoreline model with the Dixon-Coles low-score correction.
// Turns two teams' Elo ratings into goal expectations, then into a full matrix of
// scoreline probabilities, from which win/draw/loss and likely scores follow.

export interface ModelParams {
  /** Expected goal supremacy per Elo point of (effective) rating difference. */
  goalsPerElo: number;
  /** Mean total goals per match (league baseline). */
  avgGoals: number;
  /** Dixon-Coles low-score dependence parameter (small, typically negative). */
  rho: number;
  /** Home advantage expressed in Elo points (for hosts at home). */
  homeAdvElo: number;
}

const MAX_GOALS = 10;

function factorial(n: number): number {
  let f = 1;
  for (let i = 2; i <= n; i++) f *= i;
  return f;
}

export function poissonPmf(lambda: number, k: number): number {
  return (Math.exp(-lambda) * lambda ** k) / factorial(k);
}

/** Dixon-Coles dependence adjustment for the four lowest-scoring cells. */
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;
}

/** Expected goals for each side from effective Elo difference. */
export function lambdasFromElo(
  eloHome: number,
  eloAway: number,
  params: ModelParams,
  homeAdv: number,
): { lambdaHome: number; lambdaAway: number } {
  const diff = eloHome - eloAway + homeAdv;
  const supremacy = params.goalsPerElo * diff;
  const half = params.avgGoals / 2;
  return {
    lambdaHome: Math.min(8, Math.max(0.15, half + supremacy / 2)),
    lambdaAway: Math.min(8, Math.max(0.15, half - supremacy / 2)),
  };
}

/** Normalized matrix m[h][a] = P(home scores h, away scores a). */
export function scoreMatrix(lambdaHome: number, lambdaAway: number, rho: number): number[][] {
  const home = Array.from({ length: MAX_GOALS + 1 }, (_, k) => poissonPmf(lambdaHome, k));
  const away = Array.from({ length: MAX_GOALS + 1 }, (_, k) => poissonPmf(lambdaAway, k));
  const m: number[][] = [];
  let total = 0;
  for (let h = 0; h <= MAX_GOALS; h++) {
    m[h] = [];
    for (let a = 0; a <= MAX_GOALS; a++) {
      const p = home[h]! * away[a]! * dcTau(h, a, lambdaHome, lambdaAway, rho);
      m[h]![a] = p;
      total += p;
    }
  }
  for (let h = 0; h <= MAX_GOALS; h++) for (let a = 0; a <= MAX_GOALS; a++) m[h]![a]! /= total;
  return m;
}

export interface OutcomeProbs { home: number; draw: number; away: number; }

export function outcomeProbs(m: number[][]): OutcomeProbs {
  let home = 0, draw = 0, away = 0;
  for (let h = 0; h < m.length; h++) {
    for (let a = 0; a < m[h]!.length; a++) {
      const p = m[h]![a]!;
      if (h > a) home += p;
      else if (h < a) away += p;
      else draw += p;
    }
  }
  return { home, draw, away };
}

export interface Scoreline { home: number; away: number; p: number; }

export function topScorelines(m: number[][], n: number): Scoreline[] {
  const all: Scoreline[] = [];
  for (let h = 0; h < m.length; h++)
    for (let a = 0; a < m[h]!.length; a++) all.push({ home: h, away: a, p: m[h]![a]! });
  return all.sort((x, y) => y.p - x.p).slice(0, n);
}
