// ═══════════════════════════════════════════════════════════════
// Outcome-Logger & CLV-Tracker — Foundation für Self-Calibration
// Persistiert jeden Pick + Quoten zum Pickzeitpunkt + Closing-Line.
// Ohne Tracker keine Kalibrierung, ohne Kalibrierung kein Edge.
// ═══════════════════════════════════════════════════════════════

// ── Validation Helper ──────────────────────────────────────────
// Garbage-In zerstört Kalibrierung silent: NaN bei confidence verzerrt
// reliabilityBins, oddsAtPick=0 sprengt CLV (log(0) = -Infinity), und
// ein fehlender matchId macht resolveMatch wirkungslos. Wir clampen
// stillschweigend (Defensive in Depth) und werfen nur, wenn der Pick
// gar nicht eindeutig zuordenbar wäre.
const _isFiniteNum = (x) => typeof x === 'number' && Number.isFinite(x);
const _clamp = (x, lo, hi) => Math.max(lo, Math.min(hi, x));

const tracker = {
  KEY: 'fai_predictions_v1',
  MAX: 2000,

  load() {
    try {
      const raw = JSON.parse(localStorage.getItem(this.KEY) || '[]');
      return Array.isArray(raw) ? raw : [];
    } catch { return []; }
  },
  save(arr) {
    try { localStorage.setItem(this.KEY, JSON.stringify(arr.slice(-this.MAX))); }
    catch {}
  },

  // Bei jedem neuen Pick aufrufen (manuell, KI-Ticket, Master-Decision)
  logPick({ matchId, source = 'unknown', market, pick, confidence = null,
            oddsAtPick = null, kickoff = null, label = null, meta = null }) {
    if (matchId == null || matchId === '') {
      console.warn('[tracker] logPick ohne matchId → Pick wird verworfen');
      return null;
    }
    if (!market || !pick) {
      console.warn('[tracker] logPick ohne market/pick →', { market, pick });
      return null;
    }
    // confidence ∈ [0, 100] — alles andere ist Bug
    let conf = null;
    if (confidence != null) {
      if (!_isFiniteNum(confidence)) {
        console.warn('[tracker] confidence nicht finit:', confidence);
      } else {
        conf = _clamp(confidence, 0, 100);
      }
    }
    // oddsAtPick > 1 (Decimal-Quote 1.01..1000); 0/Negativ/Inf killt ROI+CLV-Math
    let odds = null;
    if (oddsAtPick != null) {
      if (!_isFiniteNum(oddsAtPick) || oddsAtPick <= 1 || oddsAtPick > 1000) {
        console.warn('[tracker] oddsAtPick außerhalb [1.01, 1000]:', oddsAtPick);
      } else {
        odds = oddsAtPick;
      }
    }
    const arr = this.load();
    const id = `pk_${Date.now()}_${Math.random().toString(36).slice(2,8)}`;
    arr.push({
      id, matchId: String(matchId), source: String(source || 'unknown'),
      market: String(market), pick: String(pick),
      confidence: conf, oddsAtPick: odds,
      kickoff, label, meta, timestamp: Date.now(),
      outcome: null, closingOdds: null, actualScore: null,
    });
    this.save(arr);
    return id;
  },

  // Quoten-Snapshot kurz vor Kickoff (Closing Line)
  snapshotClosing(matchId, closingOdds) {
    const arr = this.load();
    let touched = 0;
    arr.forEach(p => {
      if (p.matchId === matchId && !p.closingOdds) { p.closingOdds = closingOdds; touched++; }
    });
    if (touched) this.save(arr);
    return touched;
  },

  // Nach Spielende Resultat einbuchen + alle offenen Picks für dieses Match auflösen
  resolveMatch(matchId, score, teamIds = null) {
    if (!score || score.home == null || score.away == null) return 0;
    const arr = this.load();
    let resolved = 0;
    arr.forEach(p => {
      if (p.matchId !== matchId || p.outcome) return;
      p.actualScore = { home: score.home, away: score.away };
      p.outcome = this.evalPick(p.market, p.pick, score);
      resolved++;
    });
    if (resolved) this.save(arr);
    // Elo-Update wenn Team-IDs bekannt
    if (teamIds?.home && teamIds?.away && window.elo) {
      window.elo.update(teamIds.home, teamIds.away, score.home, score.away);
    }
    return resolved;
  },

  evalPick(market, pick, score) {
    const h = +score.home, a = +score.away;
    const total = h + a;
    if (!market) return 'unknown';
    const m = market.toUpperCase();
    if (m === '1X2' || m === 'H2H') {
      const w = h > a ? '1' : h < a ? '2' : 'X';
      return String(pick).toUpperCase() === w ? 'win' : 'lose';
    }
    if (m.startsWith('O') || m.startsWith('U')) {
      const line = parseFloat(m.replace(/[OU]/i,'').replace(',','.')) || 2.5;
      const overWon = total > line;
      const isOver = String(pick).toLowerCase().includes('o') || String(pick).toLowerCase()==='over';
      if (Math.abs(total - line) < 0.001) return 'push';
      return (overWon === isOver) ? 'win' : 'lose';
    }
    if (m === 'BTTS') {
      const yes = h > 0 && a > 0;
      const isYes = String(pick).toLowerCase().includes('y') || String(pick)==='1';
      return (yes === isYes) ? 'win' : 'lose';
    }
    return 'unknown';
  },

  // Aggregat-Stats: Hit-Rate, ROI, Brier, CLV (per filter)
  getStats(filter = {}) {
    let arr = this.load().filter(p => p.outcome && p.outcome !== 'unknown');
    if (filter.source) arr = arr.filter(p => p.source === filter.source);
    if (filter.market) arr = arr.filter(p => p.market === filter.market);
    const wins = arr.filter(p => p.outcome === 'win').length;
    const losses = arr.filter(p => p.outcome === 'lose').length;
    const pushes = arr.filter(p => p.outcome === 'push').length;
    const decided = wins + losses;

    const roi = arr.length ? arr.reduce((a, p) => {
      if (p.outcome === 'win' && p.oddsAtPick) return a + (p.oddsAtPick - 1);
      if (p.outcome === 'lose') return a - 1;
      return a;
    }, 0) / arr.length : 0;

    const brierArr = arr.filter(p => typeof p.confidence === 'number');
    const brier = brierArr.length ? brierArr.reduce((a, p) => {
      const exp = (p.confidence || 0) / 100;
      const act = p.outcome === 'win' ? 1 : 0;
      return a + (exp - act) ** 2;
    }, 0) / brierArr.length : null;

    // CLV: log(oddsAtPick / closingOdds) — positiv = du hast die Closing-Line geschlagen
    const clvArr = arr.filter(p => p.closingOdds && p.oddsAtPick);
    const clv = clvArr.length ? clvArr.reduce((a, p) => {
      const co = p.market?.toUpperCase()==='1X2'
        ? (p.pick==='1' ? p.closingOdds.home : p.pick==='2' ? p.closingOdds.away : p.closingOdds.draw)
        : null;
      if (!co || co <= 0) return a;
      return a + Math.log(p.oddsAtPick / co);
    }, 0) / clvArr.length : null;

    return {
      total: arr.length, wins, losses, pushes,
      hitRate: decided ? wins / decided : null,
      roi, brier, clv,
      sampleSize: arr.length,
    };
  },

  // Pro-Source-Breakdown (für Calibrator)
  getBySource() {
    const sources = [...new Set(this.load().map(p => p.source))];
    return Object.fromEntries(sources.map(s => [s, this.getStats({ source: s })]));
  },

  // ─── Source-Brier aus master_quant.meta.sources ────────────────
  // master_quant logs pro Pick die Wahrscheinlichkeit pro Sub-Source.
  // Für jede Source: durchschnittlicher Brier auf dem getroffenen Outcome.
  sourceBrier() {
    const arr = this.load().filter(p =>
      p.source === 'master_quant' &&
      p.outcome && p.outcome !== 'unknown' && p.outcome !== 'push' &&
      p.meta?.sources
    );
    const buckets = {}; // { poisson: {sum, n}, elo: {...}, llm: {...} }
    arr.forEach(p => {
      const won = p.outcome === 'win' ? 1 : 0;
      Object.entries(p.meta.sources).forEach(([src, prob]) => {
        if (typeof prob !== 'number') return;
        if (!buckets[src]) buckets[src] = { sum: 0, n: 0 };
        buckets[src].sum += (prob - won) ** 2;
        buckets[src].n++;
      });
    });
    const out = {};
    for (const [src, b] of Object.entries(buckets)) {
      out[src] = { brier: b.n > 0 ? b.sum / b.n : null, n: b.n };
    }
    return out;
  },

  // Liefert Gewichte {poisson, elo, llm} im Verhältnis 1/Brier — bei wenig Daten Default
  // Default = aktuelles statisches Schema 60/25/15
  sourceWeights({ minSample = 20, defaults = { poisson: 0.60, elo: 0.25, llm: 0.15 } } = {}) {
    const stats = this.sourceBrier();
    const usable = Object.entries(stats).filter(([k, v]) =>
      v.n >= minSample && v.brier > 0 && defaults[k] != null
    );
    if (usable.length < 2) return { ...defaults, learned: false, sample: stats };
    const inv = usable.map(([k, v]) => [k, 1 / v.brier]);
    const sum = inv.reduce((a, [, w]) => a + w, 0);
    const out = { learned: true, sample: stats };
    Object.keys(defaults).forEach(k => { out[k] = defaults[k]; }); // start with defaults
    inv.forEach(([k, w]) => { out[k] = w / sum; });
    // Re-Normalisierung über alle drei (falls eine Source zu wenig Samples hatte)
    const total = (out.poisson || 0) + (out.elo || 0) + (out.llm || 0);
    if (total > 0) { out.poisson /= total; out.elo /= total; out.llm /= total; }
    return out;
  },

  // ─── Kalibrierung: Reliability-Diagramm + Isotonic-Fit ─────────
  // Gibt 10 Bins zurück: { bin: [0..1], avgPredicted, hitRate, n }
  reliabilityBins(filter = {}) {
    let arr = this.load().filter(p =>
      p.outcome && p.outcome !== 'unknown' && p.outcome !== 'push' &&
      typeof p.confidence === 'number'
    );
    if (filter.source) arr = arr.filter(p => p.source === filter.source);
    const bins = Array.from({ length: 10 }, (_, i) => ({
      lo: i * 10, hi: (i + 1) * 10,
      sumPred: 0, sumAct: 0, n: 0,
    }));
    arr.forEach(p => {
      const c = Math.max(0, Math.min(99.99, p.confidence));
      const idx = Math.min(9, Math.floor(c / 10));
      bins[idx].sumPred += c / 100;
      bins[idx].sumAct += p.outcome === 'win' ? 1 : 0;
      bins[idx].n++;
    });
    return bins.map(b => ({
      lo: b.lo, hi: b.hi,
      avgPredicted: b.n > 0 ? b.sumPred / b.n : null,
      hitRate: b.n > 0 ? b.sumAct / b.n : null,
      n: b.n,
    }));
  },

  // Isotonic Regression via Pool-Adjacent-Violators (PAV)
  // Fittet eine monotone Mapping pred → calibrated.
  // Liefert Schritt-Funktion {x, y, n}[] und Funktion calibrate(p)
  isotonicFit(minSample = 30) {
    const arr = this.load().filter(p =>
      p.outcome && p.outcome !== 'unknown' && p.outcome !== 'push' &&
      typeof p.confidence === 'number'
    );
    if (arr.length < minSample) return { ready: false, n: arr.length };
    const data = arr.map(p => ({ x: p.confidence / 100, y: p.outcome === 'win' ? 1 : 0 }));
    data.sort((a, b) => a.x - b.x);
    // Initialisiere Blocks — jeder Punkt sein eigener Block
    const blocks = data.map(d => ({ x: d.x, sumY: d.y, n: 1 }));
    // PAV: merge angrenzende Blocks solange Vorgänger > Nachfolger
    let i = 0;
    while (i < blocks.length - 1) {
      const a = blocks[i], b = blocks[i + 1];
      if (a.sumY / a.n > b.sumY / b.n) {
        a.sumY += b.sumY;
        a.n += b.n;
        a.x = Math.max(a.x, b.x); // upper bound
        blocks.splice(i + 1, 1);
        if (i > 0) i--;
      } else {
        i++;
      }
    }
    const steps = blocks.map(b => ({ x: b.x, y: b.sumY / b.n, n: b.n }));
    const calibrate = (p) => {
      if (steps.length === 0) return p;
      // finde ersten Block mit upper-bound >= p
      for (const s of steps) if (s.x >= p) return s.y;
      return steps[steps.length - 1].y;
    };
    return { ready: true, steps, calibrate, n: arr.length };
  },

  clear() { localStorage.removeItem(this.KEY); },
};

Object.assign(window, { tracker });
