/* ============================================================
   gp-data.jsx — Game Plan Audit LIVE data adapter.

   Publishes the same `window.GP` contract the prototype's mock did,
   but sources `DATA.decisions` from the real engine artifact served by
   GET /api/strategy-rationale (data/daily_briefing.json), recomputing
   nothing — the verifier (gp-verify.jsx) re-derives every linkage proof
   client-side from the raw winner/runner_ups field.

   The live briefing carries only `winner` + `runner_ups` (NOT the mock's
   full 30-combo sweep grid). We therefore synthesize the minimal shape
   the views read:
     - sweep.candidates := [winner, ...runner_ups]   (verifier's input)
     - paramSpace        := union of observed candidate params per group
     - selection         := synthetic {rank, fit:null, regime:null, tags:[]}
   and flag the decision `partial:true` so detail views can show an honest
   "full sweep grid not in briefing artifact (Phase 2)" note.

   Radar / Selection / Execution are wired to live endpoints in later
   commits; here they publish empty-but-shaped placeholders so the tree
   stays working on every stage.

   Boot is async (§4.3): `window.GP.load()` returns a Promise that must
   resolve before React mounts. On 401/missing token it rejects with
   `err.status` so the shell can render the token-entry view.
   ============================================================ */

(function () {
  // ---- deterministic RNG (LCG) — kept as a pure helper for any view jitter ----
  function seededRng(s) {
    let x = (s | 0) || 1;
    return () => { x = (x * 1664525 + 1013904223) | 0; return ((x >>> 0) % 1e9) / 1e9; };
  }
  const rng = seededRng(20260604);

  // ---- formatting helpers shared across views (null-guarded for live gaps) ----
  const fmt = {
    pct: (v, d = 1) => (v == null ? "—" : (v * 100).toFixed(d) + "%"),
    signedPct: (v, d = 1) => (v == null ? "—" : (v > 0 ? "+" : "") + (v * 100).toFixed(d) + "%"),
    num: (v, d = 2) => (v == null ? "—" : (+v).toFixed(d)),
    sharpe: (v) => (v == null ? "—" : (+v).toFixed(2)),
    money: (v) => (v == null ? "—" : "$" + Math.round(v).toLocaleString("en-US")),
    price: (v) => {
      if (v == null) return "—";
      if (v >= 1000) return "$" + Math.round(v).toLocaleString("en-US");
      if (v >= 1) return "$" + v.toFixed(2);
      return "$" + v.toFixed(4);
    },
    moneyK: (v) => (v == null ? "—" : "$" + (v / 1000).toFixed(1) + "K"),
    time: (iso) => {
      if (!iso) return "—";
      const d = new Date(iso);
      if (isNaN(d)) return "—";
      const p = (n) => String(n).padStart(2, "0");
      return `${p(d.getUTCHours())}:${p(d.getUTCMinutes())}`;
    },
    ago: (iso, now) => {
      if (!iso) return "—";
      const t = new Date(iso).getTime();
      if (isNaN(t)) return "—";
      const m = Math.round((now - t) / 60000);
      if (m < 60) return m + "m";
      return (m / 60).toFixed(1) + "h";
    },
  };

  // ---- client-side fit heuristics (presentation-time, not engine output) ----
  // Kept for Selection (§4.3); they need indicator-bearing assets to run, so
  // they stay dormant until /api/universe carries indicators (Phase 2).
  function meanRevFit(a) {
    const rsiStretch = Math.max(0, Math.abs(a.rsi - 50) - 12) / 30;
    const bbStretch = Math.max(0, Math.abs(a.bbPctB - 0.5) - 0.25) / 0.7;
    const choppy = a.adx < 22 ? 1 : Math.max(0, 1 - (a.adx - 22) / 22);
    return rsiStretch * 0.4 + bbStretch * 0.34 + choppy * 0.26;
  }
  function trendFit(a) {
    const adxStrength = Math.min(1, Math.max(0, (a.adx - 18) / 26));
    const diSep = Math.min(1, Math.abs(a.diPlus - a.diMinus) / 22);
    const mom = Math.min(1, Math.abs(a.chg) / 0.07);
    return adxStrength * 0.48 + diSep * 0.32 + mom * 0.2;
  }

  const REGIMES = ["TRENDING_BULL", "TRENDING_BEAR", "CHOPPY", "RANGE_BOUND", "BREAKOUT"];

  // ---- per-group display metadata (labels/blurbs the artifact doesn't carry) ----
  // floor / objective / class come live from each decision; this only supplies
  // human-facing copy and the primary-param axis for the robustness slice.
  const STRATEGY_META = {
    MEAN_REV: {
      label: "Bollinger Mean Reversion",
      blurb: "Buys statistical extremes, exits toward the mean.",
      primaryParam: "window",
      fits: "choppy / range-bound regimes with stretched RSI",
    },
    TREND: {
      label: "ADX Trend Follower",
      blurb: "Rides strong directional momentum, ATR-stopped.",
      primaryParam: "adx_period",
      fits: "trending regimes with clean DI separation",
    },
  };

  // ---- auth-aware fetch helper (§4.1 — salvaged localStorage Bearer contract) ----
  const API = {
    base: () => localStorage.getItem("centaur.baseUrl") || location.origin,
    token: () => localStorage.getItem("centaur.token") || "",
    async get(path) {
      const r = await fetch(this.base() + path, {
        headers: { "Authorization": "Bearer " + this.token() },
      });
      if (!r.ok) {
        const err = new Error(path + " -> " + r.status);
        err.status = r.status;
        throw err;
      }
      return r.json();
    },
  };

  // collect distinct sorted numeric values for one param across a candidate set
  function axisValues(cands, key) {
    const seen = new Set();
    for (const c of cands) {
      const v = c.params ? c.params[key] : undefined;
      if (v != null) seen.add(v);
    }
    return [...seen].sort((a, b) => a - b);
  }

  // derive a STRATEGIES entry per group_id observed in the live briefing
  function buildStrategies(rawDecisions) {
    const byGroup = new Map();
    for (const d of rawDecisions) {
      const gid = d.group_id;
      if (!byGroup.has(gid)) byGroup.set(gid, { sample: d, cands: [] });
      const bucket = byGroup.get(gid);
      if (d.winner) bucket.cands.push(d.winner);
      for (const r of (d.runner_ups || [])) bucket.cands.push(r);
    }
    const out = [];
    for (const [gid, { sample, cands }] of byGroup) {
      const meta = STRATEGY_META[gid] || {};
      const paramKeys = sample.winner && sample.winner.params
        ? Object.keys(sample.winner.params) : [];
      const paramSpace = {};
      for (const k of paramKeys) paramSpace[k] = axisValues(cands, k);
      out.push({
        id: gid,
        cls: sample.strategy_class || gid,
        label: meta.label || gid,
        blurb: meta.blurb || "",
        floor: sample.win_rate_floor != null ? sample.win_rate_floor : 0,
        objective: sample.sweep_objective || "sharpe_x_winrate",
        primaryParam: meta.primaryParam && paramKeys.includes(meta.primaryParam)
          ? meta.primaryParam : (paramKeys[0] || null),
        paramSpace,
        fits: meta.fits || "",
      });
    }
    return out;
  }

  // map a live briefing decision onto the shape the views read
  function buildDecisions(rawDecisions) {
    const groupCount = {};
    return rawDecisions.map((d, i) => {
      const gid = d.group_id;
      groupCount[gid] = (groupCount[gid] || 0) + 1;
      const candidates = [d.winner, ...(d.runner_ups || [])].filter(Boolean);
      return {
        id: "D" + String(i + 1).padStart(2, "0"),
        ts_iso: d.ts_iso,
        strategy_class: d.strategy_class,
        strategy_id: gid,
        asset: d.asset,
        group_id: gid,
        win_rate_floor: d.win_rate_floor,
        sweep_objective: d.sweep_objective,
        bar_count: (d.winner && d.winner.bar_count) || null,
        // synthetic selection: rank within group; fit/regime/tags absent in the
        // artifact (engine doesn't serialize them) -> honest nulls, not fabricated.
        selection: {
          rank: groupCount[gid],
          fit: null,
          regime: null,
          tags: [],
          scanned: 0,
        },
        sweep: {
          started_at: d.ts_iso,
          finished_at: d.ts_iso,
          // the artifact carries only the survivor field, not the full grid
          sampled: candidates.length,
          coarse_grid_size: candidates.length,
          partial: true,
          candidates,
        },
        winner: d.winner,
        runner_ups: d.runner_ups || [],
      };
    });
  }

  // build the radar universe from /api/universe. The live snapshot is
  // symbols-only (list[str]) — every indicator (adx/rsi/regime/...) is genuinely
  // absent, so we publish nulls (rendered as "—"), never fabricated numbers.
  function buildRadar(universe, decisions) {
    // /api/universe serializes generated_at as a unix-seconds float; the
    // rationale feed uses ISO. Normalize to ISO so fmt.time/ago agree.
    const toIso = (g) => {
      if (g == null) return null;
      if (typeof g === "number") return new Date(g * 1000).toISOString();
      return g;
    };
    const raw = Array.isArray(universe.assets) ? universe.assets : [];
    const assets = raw.map((s) => {
      const sym = typeof s === "string" ? s : (s.symbol || s.sym || "");
      const pick = (k) => (s && typeof s === "object" && s[k] != null ? s[k] : null);
      return {
        sym, name: (sym.split("/")[0] || sym),
        px: pick("px"), chg: pick("chg"),
        adx: pick("adx"), diPlus: pick("diPlus"), diMinus: pick("diMinus"),
        rsi: pick("rsi"), bbPctB: pick("bbPctB"), regime: pick("regime"),
      };
    });
    return {
      scanned_at: toIso(universe.generated_at),
      count: assets.length,
      qualified: null,
      duration_ms: 0,
      age_human: universe.age_human || null,
      fresh: !!universe.fresh,
      // true only if the snapshot actually carries indicator fields (Phase 2)
      hasIndicators: assets.length > 0 && assets[0].adx != null,
      assets,
    };
  }

  // derive Selection from the committed decisions themselves — the engine's
  // real picks. A client-side fit ranking (meanRevFit/trendFit) needs indicators
  // the symbol-only universe lacks, so fit is null (labelled in-UI), not faked.
  function buildSelections(strategies, decisions, scanned) {
    return strategies.map((strat) => {
      const picks = decisions
        .filter((d) => d.strategy_id === strat.id)
        .sort((a, b) => a.selection.rank - b.selection.rank)
        .map((d) => ({
          sym: d.asset, rank: d.selection.rank, fit: null,
          regime: null, selected: true, decisionId: d.id,
        }));
      return { strategy: strat.id, picks, alternatives: [], scanned, derived: true };
    });
  }

  // build the Execution stage from live broker + ledger reality. The recorded
  // plan (target_weight / drift / notional_usd / live_price) is persisted by the
  // engine on each order row (centaur/ledger.py `orders` table) — we surface
  // those verbatim and NEVER re-derive them (no fabricated plan-vs-fill).
  // Broker state (positions / open orders) supplies the live fill side. When an
  // endpoint is unavailable (Alpaca creds absent / empty ledger) the caller has
  // already degraded it to an empty list, so this just renders fewer rows.
  function buildExecution(positions, openOrders, trades, decisions) {
    const decByAsset = new Map();
    for (const d of decisions) if (!decByAsset.has(d.asset)) decByAsset.set(d.asset, d);

    const posBySym = new Map();
    for (const p of positions) posBySym.set(p.symbol, p);

    const openBySym = new Map();
    for (const o of openOrders) {
      if (!openBySym.has(o.symbol)) openBySym.set(o.symbol, []);
      openBySym.get(o.symbol).push(o);
    }

    // `trades` is newest-first (ledger ORDER BY ts_utc DESC) — the first row per
    // symbol is the most recent recorded order, i.e. today's plan for that asset.
    const latestBySym = new Map();
    for (const t of trades) if (!latestBySym.has(t.symbol)) latestBySym.set(t.symbol, t);

    const symbols = new Set([
      ...posBySym.keys(), ...openBySym.keys(),
      ...latestBySym.keys(), ...decByAsset.keys(),
    ]);

    const rows = [];
    for (const sym of symbols) {
      const pos = posBySym.get(sym) || null;
      const ord = latestBySym.get(sym) || null;
      const opens = openBySym.get(sym) || [];
      const dec = decByAsset.get(sym) || null;

      // recorded plan — present only when the engine logged an order for `sym`.
      const targetWeight = ord && ord.target_weight != null ? ord.target_weight : null;
      const drift = ord && ord.drift != null ? ord.drift : null;
      const plannedUsd = ord && ord.notional_usd != null ? ord.notional_usd : null;
      const filledUsd = pos ? pos.market_value : null;
      const currentPx = pos ? pos.current_price : (ord ? ord.live_price : null);

      // status: read the recorded order outcome + live broker reality. Never
      // synthesize a verdict the data doesn't support -> UNKNOWN when neither.
      let status = "UNKNOWN";
      if (ord && ord.status === "SKIPPED") status = "SKIPPED";
      else if (ord && ord.status === "FAILED") status = "FAILED";
      else if (opens.length) status = "PENDING";
      else if (ord && ord.status === "SUBMITTED") status = "FILLED";
      else if (pos) status = "HELD";

      rows.push({
        symbol: sym,
        decisionId: dec ? dec.id : null,
        isPaper: pos ? pos.is_paper : (ord ? !!ord.is_paper : (opens[0] ? opens[0].is_paper : null)),
        side: ord ? ord.side : (opens[0] ? opens[0].side : null),
        status, targetWeight, drift, plannedUsd, filledUsd, currentPx,
        qty: pos ? pos.qty : (ord ? ord.qty : null),
        unrealizedPl: pos ? pos.unrealized_pl : null,
        unrealizedPlpc: pos ? pos.unrealized_plpc : null,
        orderTs: ord ? ord.ts_iso : null,
        orderStatus: ord ? ord.status : null,
        regime: ord ? ord.regime : null,
        openOrders: opens,
      });
    }
    rows.sort((a, b) => (b.filledUsd || b.plannedUsd || 0) - (a.filledUsd || a.plannedUsd || 0));
    return rows;
  }

  // fetch the three execution feeds, each degrading to [] on failure, and report
  // an honest link state so the view can explain *why* it's empty (broker creds
  // absent vs. simply no activity). Alpaca-not-configured surfaces as 500.
  async function loadExecution(decisions) {
    let positions = [], openOrders = [], trades = [];
    let brokerUp = true, ledgerUp = true, reason = null;
    try { positions = await API.get("/api/positions"); }
    catch (e) { brokerUp = false; reason = e.status === 500 ? "broker not connected" : ("positions → " + (e.status || "error")); }
    try { openOrders = await API.get("/api/orders/open"); }
    catch (e) { brokerUp = false; if (!reason) reason = "broker not connected"; }
    try { trades = await API.get("/api/trades?limit=200"); }
    catch (e) { ledgerUp = false; }
    positions = Array.isArray(positions) ? positions : [];
    openOrders = Array.isArray(openOrders) ? openOrders : [];
    trades = Array.isArray(trades) ? trades : [];
    return {
      rows: buildExecution(positions, openOrders, trades, decisions),
      meta: {
        brokerUp, ledgerUp, reason,
        positions: positions.length, openOrders: openOrders.length, orders: trades.length,
      },
    };
  }

  // safe per-asset lookup — live universe is symbols-only, so price/change are
  // genuinely unknown here (null), not faked. Detail header renders "—".
  function makeU(decisions) {
    const map = new Map();
    for (const d of decisions) {
      if (!map.has(d.asset)) {
        map.set(d.asset, { sym: d.asset, name: d.asset.split("/")[0], px: null, chg: null });
      }
    }
    return (sym) => map.get(sym) || { sym, name: (sym || "").split("/")[0], px: null, chg: null };
  }

  // ---- publish the synchronous skeleton; load() fills DATA before React mounts ----
  window.GP = {
    seededRng, rng, fmt, meanRevFit, trendFit,
    NOW: Date.now(),
    TODAY: new Date().toISOString().slice(0, 10),
    STRATEGIES: [],
    REGIMES,
    UNIVERSE: [],
    U: () => ({ sym: "", name: "", px: null, chg: null }),
    DATA: {
      generated_at: null,
      radar: { scanned_at: null, count: 0, qualified: 0, duration_ms: 0, assets: [] },
      selections: [],
      decisions: [],
      execution: [],
      execMeta: { brokerUp: false, ledgerUp: false, reason: null, positions: 0, openOrders: 0, orders: 0 },
    },
    available: false,
    async load() {
      const res = await API.get("/api/strategy-rationale?all=true");
      // universe is a secondary feed — a failure/empty there must not blank the
      // Plan core, so it's caught and degraded to an empty snapshot.
      const universe = await API.get("/api/universe").catch(() => ({
        assets: [], generated_at: null, age_human: "unavailable", fresh: false,
      }));
      const raw = Array.isArray(res.decisions) ? res.decisions : [];
      const strategies = buildStrategies(raw);
      const decisions = buildDecisions(raw);
      const radar = buildRadar(universe, decisions);
      const selections = buildSelections(strategies, decisions, radar.count);
      const exec = await loadExecution(decisions);
      this.NOW = Date.now();
      this.TODAY = (res.today) || new Date().toISOString().slice(0, 10);
      this.STRATEGIES = strategies;
      this.UNIVERSE = radar.assets;
      this.U = makeU(decisions);
      this.available = !!res.available && decisions.length > 0;
      this.DATA = {
        generated_at: res.generated_at || null,
        radar,
        selections,
        decisions,
        execution: exec.rows,
        execMeta: exec.meta,
      };
      return this;
    },
  };
})();
