// Team-level Dixon-Coles: every nation gets multiplicative attack/defence
// strengths, fit by weighted MLE (Maher-style iterative updates) with
// exponential time-decay and shrinkage toward the mean for thin histories.
// This is the second, Elo-independent member of the ensemble.

export interface TeamDcParams {
  /** Mean goals per team per match (league baseline). */
  mu: number;
  /** Home-ground multiplier applied to the home side's λ (1 on neutral). */
  homeMult: number;
  attack: Record<string, number>;
  defence: Record<string, number>;
}

export interface DcMatch {
  /** Days before the fit date (for decay weighting). */
  daysAgo: number;
  home: string;
  away: string;
  homeGoals: number;
  awayGoals: number;
  neutral: boolean;
}

/**
 * Fit attack/defence strengths.
 * @param xi    decay per year (weight = exp(-xi * daysAgo/365)); 0 = no decay
 * @param shrinkK pseudo-observations pulling strengths toward 1.0
 */
export function fitTeamDc(matches: DcMatch[], xi: number, shrinkK: number, iterations = 25): TeamDcParams {
  const w = matches.map((m) => Math.exp((-xi * m.daysAgo) / 365));
  const teams = new Set<string>();
  for (const m of matches) { teams.add(m.home); teams.add(m.away); }

  // initial values
  let mu = 0;
  {
    let g = 0, n = 0;
    for (let i = 0; i < matches.length; i++) {
      g += w[i]! * (matches[i]!.homeGoals + matches[i]!.awayGoals);
      n += w[i]! * 2;
    }
    mu = n > 0 ? g / n : 1.3;
  }
  let homeMult = 1.15;
  const att = new Map<string, number>();
  const def = new Map<string, number>();
  for (const t of teams) { att.set(t, 1); def.set(t, 1); }

  for (let it = 0; it < iterations; it++) {
    // attack updates
    const num = new Map<string, number>();
    const den = new Map<string, number>();
    for (let i = 0; i < matches.length; i++) {
      const m = matches[i]!;
      const wi = w[i]!;
      const hF = m.neutral ? 1 : homeMult;
      num.set(m.home, (num.get(m.home) ?? 0) + wi * m.homeGoals);
      den.set(m.home, (den.get(m.home) ?? 0) + wi * mu * def.get(m.away)! * hF);
      num.set(m.away, (num.get(m.away) ?? 0) + wi * m.awayGoals);
      den.set(m.away, (den.get(m.away) ?? 0) + wi * mu * def.get(m.home)!);
    }
    for (const t of teams) {
      att.set(t, ((num.get(t) ?? 0) + shrinkK * mu) / ((den.get(t) ?? 0) + shrinkK * mu));
    }

    // defence updates
    num.clear(); den.clear();
    for (let i = 0; i < matches.length; i++) {
      const m = matches[i]!;
      const wi = w[i]!;
      const hF = m.neutral ? 1 : homeMult;
      // home defence faces away attack
      num.set(m.home, (num.get(m.home) ?? 0) + wi * m.awayGoals);
      den.set(m.home, (den.get(m.home) ?? 0) + wi * mu * att.get(m.away)!);
      // away defence faces home attack (with home boost)
      num.set(m.away, (num.get(m.away) ?? 0) + wi * m.homeGoals);
      den.set(m.away, (den.get(m.away) ?? 0) + wi * mu * att.get(m.home)! * hF);
    }
    for (const t of teams) {
      def.set(t, ((num.get(t) ?? 0) + shrinkK * mu) / ((den.get(t) ?? 0) + shrinkK * mu));
    }

    // normalize so mean(att) = mean(def) = 1 (identifiability)
    const aMean = [...att.values()].reduce((s, x) => s + x, 0) / att.size;
    const dMean = [...def.values()].reduce((s, x) => s + x, 0) / def.size;
    for (const t of teams) { att.set(t, att.get(t)! / aMean); def.set(t, def.get(t)! / dMean); }
    mu = mu * aMean * dMean;

    // home multiplier from non-neutral games
    let hNum = 0, hDen = 0;
    for (let i = 0; i < matches.length; i++) {
      const m = matches[i]!;
      if (m.neutral) continue;
      hNum += w[i]! * m.homeGoals;
      hDen += w[i]! * mu * att.get(m.home)! * def.get(m.away)!;
    }
    if (hDen > 0) homeMult = Math.min(1.6, Math.max(1, hNum / hDen));
  }

  return {
    mu,
    homeMult,
    attack: Object.fromEntries(att),
    defence: Object.fromEntries(def),
  };
}

/** λs for a fixture; unseen teams fall back to average (1.0) strengths. */
export function teamDcLambdas(
  p: TeamDcParams,
  home: string,
  away: string,
  neutral: boolean,
): { lambdaHome: number; lambdaAway: number } {
  const ah = p.attack[home] ?? 1;
  const dh = p.defence[home] ?? 1;
  const aa = p.attack[away] ?? 1;
  const da = p.defence[away] ?? 1;
  const hF = neutral ? 1 : p.homeMult;
  return {
    lambdaHome: Math.min(8, Math.max(0.15, p.mu * ah * da * hF)),
    lambdaAway: Math.min(8, Math.max(0.15, p.mu * aa * dh)),
  };
}

/** Cell-wise log-opinion pool of two score matrices: mA^w · mB^(1-w), renormalized. */
export function poolMatrices(mA: number[][], mB: number[][], w: number): number[][] {
  const out: number[][] = [];
  let total = 0;
  for (let i = 0; i < mA.length; i++) {
    out[i] = [];
    for (let j = 0; j < mA[i]!.length; j++) {
      const v = Math.pow(Math.max(1e-12, mA[i]![j]!), w) * Math.pow(Math.max(1e-12, mB[i]![j]!), 1 - w);
      out[i]![j] = v;
      total += v;
    }
  }
  for (const row of out) for (let j = 0; j < row.length; j++) row[j]! /= total;
  return out;
}
