/* ============================================================
   gp-verify.jsx — the linkage proofs.
   Pure functions that re-derive the engine's claims from the raw
   candidate field, so the UI never has to trust a pre-baked "winner"
   flag — it proves (or disproves) it live.

   Three checks, plus a robustness read:
     1. COMPOSITE   composite_score == sharpe * win_rate_frac   (objective)
     2. FLOOR       winner.win_rate_frac >= group win_rate_floor
     3. TOP-RANK    no candidate outscores the stamped winner
     4. ROBUSTNESS  how many near-by combos also score highly (plateau vs spike)
   ============================================================ */

(function () {
  const EPS = 0.0015; // tolerance for display-rounding on composite

  const METRICS = [
    { key: "composite_score", label: "Composite", short: "COMP", fmt: (v) => v == null ? "—" : v.toFixed(3), higher: true, hero: true },
    { key: "sharpe", label: "Sharpe", short: "SHRP", fmt: (v) => v == null ? "—" : v.toFixed(2), higher: true },
    { key: "win_rate_frac", label: "Win rate", short: "WIN", fmt: (v) => v == null ? "—" : (v * 100).toFixed(1) + "%", higher: true },
    { key: "total_return", label: "Total return", short: "RET", fmt: (v) => v == null ? "—" : (v > 0 ? "+" : "") + (v * 100).toFixed(1) + "%", higher: true },
    { key: "max_drawdown", label: "Max drawdown", short: "MDD", fmt: (v) => v == null ? "—" : (v * 100).toFixed(1) + "%", higher: false },
  ];

  function recomposite(c) {
    if (c.sharpe == null || c.win_rate_frac == null) return null;
    return c.sharpe * c.win_rate_frac;
  }

  // ---- the three checks for one decision ----
  function checkDecision(d) {
    const winner = d.winner;
    const cands = d.sweep.candidates;
    const floor = d.win_rate_floor;
    const valid = cands.filter((c) => c.composite_score != null);

    // 1. composite recompute
    const recomputed = recomposite(winner);
    const compositeOk = recomputed != null &&
      Math.abs(recomputed - winner.composite_score) <= EPS;
    const composite = {
      id: "composite",
      label: "Composite = Sharpe × Win-rate",
      pass: compositeOk,
      stored: winner.composite_score,
      recomputed,
      detail: recomputed == null
        ? "winner has no computable score"
        : `${winner.sharpe.toFixed(2)} × ${winner.win_rate_frac.toFixed(3)} = ${recomputed.toFixed(3)}`,
      mismatch: recomputed == null ? null : winner.composite_score - recomputed,
    };

    // 2. floor
    const floorOk = winner.win_rate_frac != null && winner.win_rate_frac >= floor - 1e-9;
    const floorChk = {
      id: "floor",
      label: `Clears group floor (≥ ${(floor * 100).toFixed(0)}%)`,
      pass: floorOk,
      detail: winner.win_rate_frac == null
        ? "no win-rate"
        : `${(winner.win_rate_frac * 100).toFixed(1)}% vs ${(floor * 100).toFixed(0)}% floor`,
      margin: winner.win_rate_frac == null ? null : winner.win_rate_frac - floor,
    };

    // 3. top-rank — does anything beat the stamped winner on composite?
    const maxComposite = Math.max(...valid.map((c) => c.composite_score));
    const beaters = valid
      .filter((c) => c.composite_score > winner.composite_score + EPS)
      .sort((a, b) => b.composite_score - a.composite_score);
    // rank of winner among all valid candidates by composite
    const sortedDesc = [...valid].sort((a, b) => b.composite_score - a.composite_score);
    const winnerRank = sortedDesc.findIndex((c) =>
      c.composite_score === winner.composite_score &&
      JSON.stringify(c.params) === JSON.stringify(winner.params)) + 1;
    const topRankOk = beaters.length === 0;
    const topRank = {
      id: "toprank",
      label: `Top-ranked of ${valid.length} scored combos`,
      pass: topRankOk,
      rank: winnerRank > 0 ? winnerRank : 1,
      total: valid.length,
      beaters,
      detail: topRankOk
        ? `#1 by composite of ${valid.length}`
        : `${beaters.length} combo${beaters.length > 1 ? "s" : ""} outscore the stamped winner (best ${maxComposite.toFixed(3)})`,
    };

    const checks = [composite, floorChk, topRank];
    const verdict = checks.every((c) => c.pass) ? "VERIFIED" : "FLAGGED";
    const failed = checks.filter((c) => !c.pass);

    return { checks, verdict, failed, winnerRank: topRank.rank, validCount: valid.length };
  }

  // ---- robustness: is the winner a plateau or a lone spike? ----
  function robustness(d) {
    const winner = d.winner;
    const cands = d.sweep.candidates;
    const valid = cands.filter((c) => c.composite_score != null);
    if (winner.composite_score == null || !valid.length) {
      return { kind: "UNKNOWN", neighborsHigh: 0, neighborsTotal: 0, pctOfPeak: 0, meanNeighbor: 0, clearFloorHigh: 0 };
    }
    const peak = winner.composite_score;
    const winnerKey = JSON.stringify(winner.params);
    // "near the top" = within 11% of peak composite
    const band = peak * 0.89;
    const strong = valid.filter((c) => c.composite_score >= band);
    // neighbors = strong combos OTHER than the winner itself
    const neighbors = strong.filter((c) => JSON.stringify(c.params) !== winnerKey);
    const clearFloorNeighbors = neighbors.filter((c) => c.win_rate_frac >= d.win_rate_floor);
    const meanStrong = strong.reduce((s, c) => s + c.composite_score, 0) / strong.length;
    const pctOfPeak = meanStrong / peak;
    const supporters = neighbors.length;
    let kind;
    if (supporters >= 6 && clearFloorNeighbors.length >= 4) kind = "PLATEAU";
    else if (supporters >= 3) kind = "SUPPORTED";
    else if (supporters >= 1) kind = "NARROW";
    else kind = "SPIKE";
    return {
      kind,
      neighborsHigh: supporters,
      neighborsTotal: valid.length - 1,
      clearFloorHigh: clearFloorNeighbors.length,
      pctOfPeak,
      meanNeighbor: meanStrong,
      band,
    };
  }

  // ---- 1-D slice along the primary param for the landscape view ----
  function paramSlice(d) {
    const strat = window.GP.STRATEGIES.find((s) => s.id === d.strategy_id);
    const param = strat.primaryParam;
    const cands = d.sweep.candidates.filter((c) => c.composite_score != null);
    // group by primary param value -> best composite at each value (envelope)
    const byVal = new Map();
    for (const c of cands) {
      const v = c.params[param];
      const cur = byVal.get(v);
      if (!cur || c.composite_score > cur.best) byVal.set(v, { best: c.composite_score, mean: 0, n: 0 });
    }
    // means
    const acc = new Map();
    for (const c of cands) {
      const v = c.params[param];
      const a = acc.get(v) || { sum: 0, n: 0, max: -1, all: [] };
      a.sum += c.composite_score; a.n++; a.max = Math.max(a.max, c.composite_score);
      a.all.push(c.composite_score);
      acc.set(v, a);
    }
    const space = strat.paramSpace[param];
    const points = space.map((v) => {
      const a = acc.get(v);
      return {
        value: v,
        best: a ? a.max : null,
        mean: a ? a.sum / a.n : null,
        n: a ? a.n : 0,
        isWinner: d.winner.params[param] === v,
      };
    });
    return { param, points };
  }

  // ---- summary across all decisions ----
  function summary(decisions) {
    let verified = 0, flagged = 0;
    const flags = [];
    for (const d of decisions) {
      const r = checkDecision(d);
      if (r.verdict === "VERIFIED") verified++;
      else { flagged++; flags.push({ id: d.id, asset: d.asset, failed: r.failed.map((f) => f.id) }); }
    }
    return { total: decisions.length, verified, flagged, flags };
  }

  window.GP = window.GP || {};
  window.GP.verify = {
    METRICS, EPS, recomposite, checkDecision, robustness, paramSlice, summary,
    metricByKey: (k) => METRICS.find((m) => m.key === k),
  };
})();
