// ═══════════════════════════════════════════════════════════════
// Dixon-Coles Goal Model — Quant-Anker für 1X2 / O-U / BTTS
// Closed-form auf 16×16 Score-Grid (Poisson-PMF × τ-Korrektur).
// Korrigiert die Poisson-Schwäche bei 0-0 / 1-0 / 0-1 / 1-1.
// ~1ms pro Match, deterministisch (kein Sampling-Rauschen).
// ═══════════════════════════════════════════════════════════════

// ── Validation Helpers ────────────────────────────────────────
// NaN/Infinity kann durch fehlende xG-Daten oder kaputte API-Antworten in
// die Pipeline laufen. Wir clampen alle λ-Werte in [LAMBDA_MIN, LAMBDA_MAX]
// und filtern Quoten, die mathematisch keinen Sinn ergeben (≤ 1.0).
const LAMBDA_MIN = 0.1;
const LAMBDA_MAX = 8.0;
const ODDS_MIN = 1.01;
const ODDS_MAX = 1000;
const _isFinite = (x) => typeof x === 'number' && Number.isFinite(x);
const _safeLambda = (x) => _isFinite(x) ? Math.max(LAMBDA_MIN, Math.min(LAMBDA_MAX, x)) : LAMBDA_MIN;
const _safeOdd = (x) => (_isFinite(x) && x >= ODDS_MIN && x <= ODDS_MAX) ? x : null;

const poisson = {
  // Poisson-PMF: P(X=k | λ)
  pmf(k, lambda) {
    if (!_isFinite(lambda) || lambda <= 0) return k === 0 ? 1 : 0;
    // log-space gegen Underflow bei großem k
    let logP = -lambda + k * Math.log(lambda);
    for (let i = 2; i <= k; i++) logP -= Math.log(i);
    return Math.exp(logP);
  },

  // Dixon-Coles τ-Faktor — korrigiert Low-Score-Bias
  // ρ ∈ [-0.2, 0]: typisch -0.10 bei Top-Ligen, -0.05 bei Underdog-Heavy
  // (h,a) = (0,0): 1 - λ_h*λ_a*ρ
  // (h,a) = (0,1): 1 + λ_h*ρ
  // (h,a) = (1,0): 1 + λ_a*ρ
  // (h,a) = (1,1): 1 - ρ
  // sonst: 1
  tau(h, a, lh, la, rho) {
    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;
  },

  // λ-Raten aus Match-Daten — Priorität: xG > real goals/game > odds-heuristic
  computeRates(match) {
    let homeXG = match.xG?.home;
    let awayXG = match.xG?.away;
    let source = 'xg';

    // Fallback 1: reale Tore pro Spiel aus FBref-Team-Stats
    if (homeXG == null || awayXG == null) {
      const ts = match.teamStats;
      if (ts?.status === 'ok' && ts.home?.gp > 0 && ts.away?.gp > 0) {
        const hg = ts.home.goals != null ? ts.home.goals / ts.home.gp : null;
        const ag = ts.away.goals != null ? ts.away.goals / ts.away.gp : null;
        if (hg != null && ag != null) { homeXG = hg; awayXG = ag; source = 'real-goals'; }
      }
    }

    // Fallback 2: avgGoals des Matches (gleicher Wert für beide → Heimvorteil entscheidet)
    if ((homeXG == null || awayXG == null) && match.avgGoals) {
      homeXG = match.avgGoals / 2;
      awayXG = match.avgGoals / 2;
      source = 'avg-goals';
    }

    // Fallback 3: Quoten → implizite Probas → Tor-Schätzung
    if (homeXG == null || awayXG == null) {
      const o = match.odds || {};
      const ph = o.home ? 1 / o.home : 0.4;
      const pa = o.away ? 1 / o.away : 0.4;
      const total = ph + pa + (o.draw ? 1 / o.draw : 0.25);
      const norm = (x) => x / total;
      homeXG = 1.0 + norm(ph) * 1.5;
      awayXG = 1.0 + norm(pa) * 1.5;
      source = 'odds-heuristic';
    }

    // Heimvorteil ~18%
    const HOME_ADV = 0.18;
    let homeRate = homeXG * (1 + HOME_ADV);
    let awayRate = awayXG;

    // Motivation-Adjustment: max ±10% xG je Team
    if (match.motivation?.status === 'ok') {
      const motToFactor = (s) => 1 + ((s - 50) / 500);
      homeRate *= motToFactor(match.motivation.home.score);
      awayRate *= motToFactor(match.motivation.away.score);
    }
    return {
      homeRate: _safeLambda(homeRate),
      awayRate: _safeLambda(awayRate),
      source,
    };
  },

  // Score-Wahrscheinlichkeitsmatrix mit Dixon-Coles
  // Liefert P[h][a] für h,a ∈ {0..MAX-1}
  scoreMatrix(lh, la, rho = -0.10, MAX = 16) {
    // PMFs einmal vorrechnen
    const pH = new Array(MAX);
    const pA = new Array(MAX);
    for (let k = 0; k < MAX; k++) { pH[k] = this.pmf(k, lh); pA[k] = this.pmf(k, la); }
    const M = [];
    let totalMass = 0;
    for (let h = 0; h < MAX; h++) {
      M[h] = new Array(MAX);
      for (let a = 0; a < MAX; a++) {
        const p = pH[h] * pA[a] * this.tau(h, a, lh, la, rho);
        M[h][a] = p;
        totalMass += p;
      }
    }
    // Re-Normalisierung (τ verändert die Gesamtmasse minimal + Cutoff bei MAX)
    if (totalMass > 0) {
      for (let h = 0; h < MAX; h++)
        for (let a = 0; a < MAX; a++) M[h][a] /= totalMass;
    }
    return M;
  },

  // ── Bivariate Poisson (Karlis & Ntzoufras, 2003) ───────────────────────────
  // Modelliert eine GEMEINSAME Toreszunahme λ₃ — z.B. wenn beide Teams in
  // einer Phase mehr/weniger Tore erzielen (Spielstand-Dynamik, offensiver
  // Spielstil beider). Marginal-Raten bleiben (λ_h + λ₃) und (λ_a + λ₃).
  //
  // Formel:  P(X=h, Y=a) = e^{-(λ₁+λ₂+λ₃)} · (λ₁^h)/h! · (λ₂^a)/a!
  //                       · Σ_{k=0..min(h,a)} C(h,k) C(a,k) k! (λ₃ / (λ₁ λ₂))^k
  //
  // λ₃=0 reduziert sich exakt zu unabhängigem Poisson (kein Dixon-Coles τ).
  // Typische Werte: 0.05–0.25 — Liga-spezifisch, lernbar über Backtest.
  //
  // Wir verwenden λ_h, λ_a aus computeRates als Marginal-Raten und ziehen
  // λ₃ pro Team ab, damit die Marginals stabil bleiben.
  scoreMatrixBivariate(lhMarginal, laMarginal, lambda3 = 0.10, MAX = 16) {
    const l3 = Math.max(0, Math.min(lhMarginal, laMarginal) - 0.05) > lambda3
      ? lambda3 : Math.max(0, Math.min(lhMarginal, laMarginal) - 0.05);
    const l1 = Math.max(0.01, lhMarginal - l3);
    const l2 = Math.max(0.01, laMarginal - l3);
    const expFactor = Math.exp(-(l1 + l2 + l3));

    // Faktorielle vorrechnen
    const fact = new Array(MAX + 1);
    fact[0] = 1;
    for (let i = 1; i <= MAX; i++) fact[i] = fact[i - 1] * i;
    const binom = (n, k) => fact[n] / (fact[k] * fact[n - k]);

    const M = [];
    let totalMass = 0;
    for (let h = 0; h < MAX; h++) {
      M[h] = new Array(MAX);
      for (let a = 0; a < MAX; a++) {
        // Σ-Term über min(h,a)
        let sum = 0;
        const kMax = Math.min(h, a);
        for (let k = 0; k <= kMax; k++) {
          sum += binom(h, k) * binom(a, k) * fact[k] *
                 Math.pow(l3 / (l1 * l2), k);
        }
        const p = expFactor * Math.pow(l1, h) / fact[h]
                            * Math.pow(l2, a) / fact[a] * sum;
        M[h][a] = p;
        totalMass += p;
      }
    }
    if (totalMass > 0) {
      for (let h = 0; h < MAX; h++)
        for (let a = 0; a < MAX; a++) M[h][a] /= totalMass;
    }
    return M;
  },

  // simulate() — gleiche API wie vorher; n-Parameter ignoriert (closed-form)
  // Default: Bivariate Poisson (modelliert Tor-Korrelation explizit). Optional
  // per `match.modelType: 'dixon-coles'` zurück auf das vorherige DC-Modell.
  simulate(match, _n = 10000) {
    const { homeRate, awayRate, source } = this.computeRates(match);
    const useDixonColes = match?.modelType === 'dixon-coles';
    // Liga-spezifischer ρ / λ₃ — kann später aus perfStore lernen
    const rho = -0.10;
    const lambda3 = 0.10;
    const M = useDixonColes
      ? this.scoreMatrix(homeRate, awayRate, rho)
      : this.scoreMatrixBivariate(homeRate, awayRate, lambda3);
    const MAX = M.length;

    let homeWin = 0, draw = 0, awayWin = 0, btts = 0, goalSum = 0;
    const lines = [0.5, 1.5, 2.5, 3.5, 4.5];
    const overSum = Object.fromEntries(lines.map(l => [l, 0]));
    const scoreList = [];

    for (let h = 0; h < MAX; h++) {
      for (let a = 0; a < MAX; a++) {
        const p = M[h][a]; if (p <= 0) continue;
        const total = h + a;
        goalSum += total * p;
        if (h > a) homeWin += p; else if (h < a) awayWin += p; else draw += p;
        if (h > 0 && a > 0) btts += p;
        lines.forEach(l => { if (total > l) overSum[l] += p; });
        const k = `${Math.min(h, 5)}-${Math.min(a, 5)}`;
        scoreList.push({ key: k, p });
      }
    }

    // Top-Scores aggregieren (durch Min-Cap auf 5 entstehen Duplikate)
    const scoreAgg = {};
    scoreList.forEach(({ key, p }) => { scoreAgg[key] = (scoreAgg[key] || 0) + p; });

    const probs = {
      home: homeWin,
      draw: draw,
      away: awayWin,
      btts: btts,
      avgGoals: +goalSum.toFixed(2),
      over: Object.fromEntries(Object.entries(overSum).map(([l, p]) => [l, p])),
      topScores: Object.entries(scoreAgg)
        .sort((a, b) => b[1] - a[1])
        .slice(0, 5)
        .map(([s, p]) => ({ score: s, prob: p })),
      lambdas: { home: +homeRate.toFixed(2), away: +awayRate.toFixed(2) },
      model: useDixonColes
        ? { type: 'dixon-coles', rho, source }
        : { type: 'bivariate-poisson', lambda3, source },
    };

    // Faire Quoten = 1/Probability
    probs.fair = {
      home: probs.home > 0 ? +(1 / probs.home).toFixed(2) : null,
      draw: probs.draw > 0 ? +(1 / probs.draw).toFixed(2) : null,
      away: probs.away > 0 ? +(1 / probs.away).toFixed(2) : null,
      btts_y: probs.btts > 0 ? +(1 / probs.btts).toFixed(2) : null,
      btts_n: (1 - probs.btts) > 0 ? +(1 / (1 - probs.btts)).toFixed(2) : null,
      o25: probs.over[2.5] > 0 ? +(1 / probs.over[2.5]).toFixed(2) : null,
      u25: (1 - probs.over[2.5]) > 0 ? +(1 / (1 - probs.over[2.5])).toFixed(2) : null,
    };

    // Edge gegen Markt: marketOdd × prob - 1 — positiv = Value
    if (match.odds) {
      // Markt-Wahrscheinlichkeiten (Vig-bereinigt) als Bayesian Prior
      const market = poisson.marketProbs(match);
      // Geblendete Probas: Modell + Markt im Logit-Raum (Markt-Gewicht 0.45)
      // Markt ist die präziseste Single-Prognose — Modell-Edge nur dort, wo es überzeugt
      const blend = (mp, modelP) => mp != null ? poisson.bayesBlend(modelP, mp, 0.45) : modelP;
      const blended = {
        home: blend(market.home, probs.home),
        draw: blend(market.draw, probs.draw),
        away: blend(market.away, probs.away),
        o25: blend(market.o25, probs.over[2.5]),
        u25: blend(market.u25, 1 - probs.over[2.5]),
        btts_y: blend(market.btts_y, probs.btts),
        btts_n: blend(market.btts_n, 1 - probs.btts),
      };
      probs.market = market;
      probs.blended = blended;

      // Edge nur wenn Quote sinnvoll (>1.01) und Probability finit & positiv
      const ed = (mo, p) => {
        const safeOdd = _safeOdd(mo);
        if (safeOdd == null || !_isFinite(p) || p <= 0 || p > 1) return null;
        return +(safeOdd * p - 1).toFixed(3);
      };
      probs.edge = {
        home: ed(match.odds.home, blended.home),
        draw: ed(match.odds.draw, blended.draw),
        away: ed(match.odds.away, blended.away),
        o25: ed(match.overUnder?.o25, blended.o25),
        u25: ed(match.overUnder?.u25, blended.u25),
        btts_y: ed(match.btts?.yes, blended.btts_y),
        btts_n: ed(match.btts?.no, blended.btts_n),
      };
      // Edges OHNE Prior — zur Konsens-Prüfung
      const rawEdge = {
        home: ed(match.odds.home, probs.home),
        draw: ed(match.odds.draw, probs.draw),
        away: ed(match.odds.away, probs.away),
        o25: ed(match.overUnder?.o25, probs.over[2.5]),
        u25: ed(match.overUnder?.u25, 1 - probs.over[2.5]),
        btts_y: ed(match.btts?.yes, probs.btts),
        btts_n: ed(match.btts?.no, 1 - probs.btts),
      };

      const MIN_EDGE = 0.04; // 4% — unter diesem Wert ist die Markt-Effizienz größer als unser Modell-Edge
      const candidates = [
        { market: '1X2', pick: '1', edge: probs.edge.home, rawEdge: rawEdge.home, prob: blended.home, modelProb: probs.home, marketProb: market.home, odd: match.odds.home, label: 'Heimsieg' },
        { market: '1X2', pick: 'X', edge: probs.edge.draw, rawEdge: rawEdge.draw, prob: blended.draw, modelProb: probs.draw, marketProb: market.draw, odd: match.odds.draw, label: 'Unentschieden' },
        { market: '1X2', pick: '2', edge: probs.edge.away, rawEdge: rawEdge.away, prob: blended.away, modelProb: probs.away, marketProb: market.away, odd: match.odds.away, label: 'Auswärtssieg' },
        { market: 'O2.5', pick: 'over', edge: probs.edge.o25, rawEdge: rawEdge.o25, prob: blended.o25, modelProb: probs.over[2.5], marketProb: market.o25, odd: match.overUnder?.o25, label: 'Über 2.5 Tore' },
        { market: 'O2.5', pick: 'under', edge: probs.edge.u25, rawEdge: rawEdge.u25, prob: blended.u25, modelProb: 1 - probs.over[2.5], marketProb: market.u25, odd: match.overUnder?.u25, label: 'Unter 2.5 Tore' },
        { market: 'BTTS', pick: 'yes', edge: probs.edge.btts_y, rawEdge: rawEdge.btts_y, prob: blended.btts_y, modelProb: probs.btts, marketProb: market.btts_y, odd: match.btts?.yes, label: 'Beide treffen — Ja' },
        { market: 'BTTS', pick: 'no', edge: probs.edge.btts_n, rawEdge: rawEdge.btts_n, prob: blended.btts_n, modelProb: 1 - probs.btts, marketProb: market.btts_n, odd: match.btts?.no, label: 'Beide treffen — Nein' },
      ].filter(c => c.edge != null && c.odd)
        // Konsens-Filter: Edge nach Markt-Blend > Schwelle UND auch ohne Prior positiv
        .map(c => ({ ...c, qualified: c.edge >= MIN_EDGE && (c.rawEdge ?? 0) > 0 }));

      candidates.sort((a, b) => b.edge - a.edge);
      // bestBets nur qualifizierte; topAll weiterhin alle (UI kann beides zeigen)
      probs.bestBets = candidates.filter(c => c.qualified).slice(0, 3);
      probs.topAll = candidates.slice(0, 5);
    }

    return probs;
  },

  // Markt-Probabilities aus Quoten — entfernt den Buchmacher-Vig
  // (1/odd ist nicht die echte Probability, weil Buchmacher Marge eingebaut haben)
  marketProbs(match) {
    const out = { home: null, draw: null, away: null, o25: null, u25: null, btts_y: null, btts_n: null };
    const o = match.odds;
    const oH = _safeOdd(o?.home), oD = _safeOdd(o?.draw), oA = _safeOdd(o?.away);
    if (oH && oD && oA) {
      const ph = 1 / oH, pd = 1 / oD, pa = 1 / oA;
      const sum = ph + pd + pa; // > 1 wegen Vig
      out.home = ph / sum; out.draw = pd / sum; out.away = pa / sum;
    }
    const oO = _safeOdd(match.overUnder?.o25), oU = _safeOdd(match.overUnder?.u25);
    if (oO && oU) {
      const po = 1 / oO, pu = 1 / oU;
      const s = po + pu;
      out.o25 = po / s; out.u25 = pu / s;
    }
    const oY = _safeOdd(match.btts?.yes), oN = _safeOdd(match.btts?.no);
    if (oY && oN) {
      const py = 1 / oY, pn = 1 / oN;
      const s = py + pn;
      out.btts_y = py / s; out.btts_n = pn / s;
    }
    return out;
  },

  // Bayesian Blend im Logit-Raum: w*logit(market) + (1-w)*logit(model)
  // Robust gegen p≈0/1; w=0.45 → Markt 45% Gewicht, Modell 55%
  bayesBlend(modelP, marketP, w = 0.45) {
    const eps = 1e-6;
    const clip = (p) => Math.min(1 - eps, Math.max(eps, p));
    const logit = (p) => Math.log(clip(p) / (1 - clip(p)));
    const z = w * logit(marketP) + (1 - w) * logit(modelP);
    return 1 / (1 + Math.exp(-z));
  },
};

Object.assign(window, { poisson });
