/* ============================================================
   engine-bridge.jsx — the integration layer between the site's
   window-globals world and the standalone engine (on window.Engine,
   set by engine-adapter.js).

   This is the LAST <script type="text/babel"> in index.html, so by the
   time it runs every site global (CAP_2026, useDerived, …) AND
   window.Engine are defined. New engine call sites that need BOTH
   worlds land HERE first, then graduate into the components.

   For now: a read-only PARITY GUARD — does the site's hard-coded
   CAP_2026 still agree with the engine's canonical figures? Pure
   diagnostics; changes no behavior. (handoff sec.8 — replace hard-coded
   numbers with engine reads; this is where drift shows up first.)
   ============================================================ */
(function engineBridge() {
  const E  = window.Engine;
  const EC = window.ENGINE_CBA;
  const S  = window.CAP_2026;
  if (!E || !EC) { console.error("[engine-bridge] window.Engine missing — engine-adapter.js did not load."); return; }
  if (!S)        { console.warn("[engine-bridge] window.CAP_2026 not found — parity check skipped."); return; }

  // League-SET thresholds (literals on both sides) — must be exactly equal.
  const exact = [
    ["cap",       S.cap,     EC.cap],
    ["tax",       S.taxLine, EC.taxLevel],
    ["1st apron", S.apron1,  EC.firstApron],
    ["2nd apron", S.apron2,  EC.secondApron],
  ];
  const drift = exact.filter(function (row) { return Math.round(row[1]) !== Math.round(row[2]); });
  if (drift.length) {
    console.warn("[engine-bridge] CAP parity DRIFT (site vs engine): " +
      drift.map(function (r) { return r[0] + ": site=" + r[1] + " engine=" + r[2]; }).join(" · "));
  } else {
    console.info("[engine-bridge] CAP parity OK — 4 league-set thresholds match the engine.");
  }

  // DERIVED figures the site hard-codes (handoff: read from derive() instead).
  // Room MLE is the first candidate to switch to an engine read.
  const sRoom = S.roomMle;
  const eRoom = Math.round(EC.exceptions.roomMle);
  if (sRoom != null && Math.round(sRoom) !== eRoom) {
    console.info("[engine-bridge] derived-figure note · roomMle: site hard-codes " + sRoom +
      ", engine derives " + eRoom + " (Δ " + (sRoom - eRoom) + "). Candidate to replace with an engine read.");
  }

  window.__engineBridge = { parityOk: drift.length === 0 };
})();

/* ------------------------------------------------------------
   window.__tradeShadow() — run the canonical engine on the trade the
   user currently has in the Trade Machine, and return its verdict.
   READ-ONLY diagnostic: reads the live trade straight from
   localStorage["capmvp-tm-trade"] (which trade-machine.jsx already saves
   on every change) + the team data file — so it needs NO edit to the
   trade machine and changes nothing on screen. Call it from the console
   after building a trade. Adapter ported from
   Private/_integration/trade-harness.mjs + adapter-spec.md.

   v1 = the ENGINE's verdict on the live trade (the canonical take, to
   eyeball against the site's own verdict pill). Picks are skipped in v1
   (round/year aren't persisted; $0 matching). A fully-AUTOMATED
   site-vs-engine compare needs the 1-line __tmShadow hook
   (adapter-spec §7) — staged, not applied.
   ------------------------------------------------------------ */
window.__tradeShadow = function tradeShadow() {
  const E = window.Engine;
  if (!E) return Promise.resolve({ error: "window.Engine missing" });

  // 1) the live trade — now scenarios[active].draftTrade inside capmvp-state
  //    (Phase 4, plain objects). The legacy capmvp-tm-trade store is retired.
  let slots;
  try {
    const st = JSON.parse(localStorage.getItem("capmvp-state") || "null");
    const sc = (st && st.scenarios && st.activeScenario) ? st.scenarios[st.activeScenario] : null;
    slots = (sc && sc.draftTrade && Array.isArray(sc.draftTrade.slots)) ? sc.draftTrade.slots : null;
  } catch (e) { return Promise.resolve({ error: "could not parse capmvp-state: " + e.message }); }
  if (!slots || !slots.length) return Promise.resolve({ note: "no in-progress trade (scenarios[active].draftTrade is empty) — open the Trade Machine and build one first." });
  const visible = slots.filter(function (s) { return s && !s.hidden; });

  // 2) team data (cached after first fetch)
  const dataReady = window.__tmData
    ? Promise.resolve(window.__tmData)
    : fetch("data/all-teams-detail.json", { cache: "no-store" }).then(function (r) { return r.json(); }).then(function (j) { window.__tmData = j; return j; });

  return dataReady.then(function (data) {
    const TEAMS = (data && data.teams) || {};
    const STATUS = { under_contract: "guaranteed", non_guaranteed: "non_guaranteed", team_option: "team_option", player_option: "player_option", two_way: "two_way", partially_guaranteed: "guaranteed" };
    // Use the 2026-27 season salary (the season the app models) from seasons[] — NOT
    // currentSalary, which is the 2025-26 figure (that bug made this diagnostic disagree
    // with the trade machine; see data-discrepancy.md).
    const sal2627 = function (p) { const s = (p.seasons || []).find(function (x) { return x.season === "2026-27"; }); return s ? (Number(s.salary) || 0) : (Number(p.currentSalary) || 0); };
    const isOnContract = function (p) { return p.contractType !== "draft_pick" && p.offseasonStatus !== "UFA" && p.offseasonStatus !== "RFA" && Number.isFinite(Number(p.currentSalary)); };   // exclude FAs (cap holds, not contracts) — matches the trade machine
    const toAsset = function (p) {
      return { kind: "player", id: p.name, name: p.name, salary: sal2627(p),
               status: STATUS[p.offseasonStatus] || "guaranteed",
               years: Array.isArray(p.seasons) ? p.seasons.map(function (s) { return Number(s.salary) || 0; }) : undefined,
               unlikelyBonus: Number(p.unlikelyBonus) || 0 };
    };
    const rosterOf = function (code) { return (((TEAMS[code] || {}).players) || []).filter(isOnContract).map(toAsset); };
    const preOf = function (code) { return rosterOf(code).reduce(function (a, p) { return a + p.salary; }, 0); };
    const faRow = function (code, name) { return (((TEAMS[code] || {}).players) || []).find(function (p) { return p.name === name && (p.offseasonStatus === "UFA" || p.offseasonStatus === "RFA"); }); };

    // 3) slots → moves
    const moves = []; let skippedPicks = 0;
    visible.forEach(function (slot) {
      const code = slot.code;
      const players = slot.players || {};
      Object.keys(players).forEach(function (name) {
        const m = players[name];
        if (m.declined || !m.dest) return;
        if (m.salaryOverride != null) moves.push({ asset: { kind: "player", id: name, name: name, salary: m.salaryOverride, status: "guaranteed" }, from: code, to: m.dest });
        else moves.push({ assetName: name, from: code, to: m.dest });
      });
      const fas = slot.fas || {};
      Object.keys(fas).forEach(function (name) {
        const m = fas[name];
        if (!m.dest) return;
        const fa = faRow(code, name);
        moves.push({ asset: { kind: "player", name: name, salary: (m.sntSalary != null ? m.sntSalary : (fa ? Number(fa.capHold) : 0)),
                              isSignAndTrade: true, birdRights: (fa && fa.birdRights) ? fa.birdRights : "full", signAndTrade: { years: m.sntYears || 3 } }, from: code, to: m.dest });
      });
      if (slot.cash && slot.cash.out > 0 && slot.cash.dest) moves.push({ asset: { kind: "cash", salary: slot.cash.out }, from: code, to: slot.cash.dest });
      const picks = slot.picks || {}; skippedPicks += Object.keys(picks).length;
    });

    // 4) adapter → engine {teams, movements} (ported siteTradeToMovements)
    const codes = new Set();
    visible.forEach(function (s) { codes.add(s.code); });
    moves.forEach(function (m) { codes.add(m.from); codes.add(m.to); });
    const teams = {};
    codes.forEach(function (code) { const pre = preOf(code); teams[code] = { code: code, preTeamSalary: pre, preApronSalary: pre, roster: rosterOf(code), tpes: [] }; });
    const movements = []; const unresolved = [];
    moves.forEach(function (m) {
      if (m.asset && typeof m.asset === "object") {
        if ((m.asset.kind || "player") === "player") {
          const r = teams[m.from].roster;
          if (!r.some(function (a) { return a.id === m.asset.id || a.name === m.asset.name; })) r.push(Object.assign({}, m.asset));
        }
        movements.push({ asset: m.asset, from: m.from, to: m.to });
      } else {
        const r = rosterOf(m.from);
        const a = r.find(function (p) { return p.name === m.assetName; }) || r.find(function (p) { return p.name.toLowerCase().indexOf((m.assetName || "").toLowerCase()) >= 0; });
        if (!a) { unresolved.push(m.assetName + " on " + m.from); return; }
        movements.push({ asset: a.id, from: m.from, to: m.to });
      }
    });

    // 5) run the engine
    if (!movements.length) return { note: "no resolvable movements", unresolved: unresolved, skippedPicks: skippedPicks };
    const res = E.evaluateTradeFromMovements({ teams: teams, movements: movements }, E.CBA_2026_27);

    // 6) compact verdict
    const out = {
      engineLegal: res.legal,
      indeterminate: !!res.indeterminate,
      conservationOk: !!(res.conservation && res.conservation.ok),
      gatedLegal: !!(res.legal && res.conservation && res.conservation.ok && !res.indeterminate),
      hardCaps: Array.from(new Set((res.sides || []).reduce(function (a, s) { return a.concat(s.hardCaps || []); }, []))),
      sides: (res.sides || []).map(function (s) {
        return { code: s.code, legal: s.legal, O: Math.round(s.O), I: Math.round(s.I), postBase: Math.round(s.postBase), reasons: (s.reasons || []).slice(0, 3), flags: (s.flags || []).slice(0, 2) };
      }),
      conservationIssues: (res.conservation && res.conservation.issues) || [],
      teamsInDeal: Array.from(codes), skippedPicks: skippedPicks, unresolved: unresolved,
    };

    // SITE comparison: run the site's EXPORTED trade logic (window.evaluateTeamSide —
    // a subset of the UI's private evaluator: no cash/TPE, post = pre - O + I) on the
    // SAME preSalary inputs, to isolate RULE differences (where a flip would change the
    // verdict). adapter-spec GAP-2: a faithful UI-verdict compare needs the staged
    // __tmShadow hook; this compares the exported logic, which still uses the 125% band
    // even over the apron — so over-apron deals are where it diverges from the engine.
    if (typeof window.evaluateTeamSide === "function") {
      const nameOf = function (m) { return (m.asset && m.asset.name) || m.assetName; };
      const salOf = function (m) {
        if (m.asset && typeof m.asset === "object") return (m.asset.kind || "player") === "player" ? (Number(m.asset.salary) || 0) : null;
        const r = rosterOf(m.from);
        const a = r.find(function (p) { return p.name === m.assetName; }) || r.find(function (p) { return p.name.toLowerCase().indexOf((m.assetName || "").toLowerCase()) >= 0; });
        return a ? a.salary : 0;
      };
      const cmp = Array.from(codes).map(function (code) {
        const outs = moves.filter(function (m) { return m.from === code; }).map(function (m) { return { name: nameOf(m), salary: salOf(m), noTrade: false }; }).filter(function (x) { return x.salary != null; });
        const ins = moves.filter(function (m) { return m.to === code; }).map(function (m) { return { name: nameOf(m), salary: salOf(m), noTrade: false }; }).filter(function (x) { return x.salary != null; });
        let sv = null; try { sv = window.evaluateTeamSide({ code: code, out: outs, in: ins, preSalary: preOf(code) }); } catch (e) { sv = { error: e.message }; }
        const es = (res.sides || []).find(function (s) { return s.code === code; }) || {};
        return { code: code, siteLegal: sv ? sv.legal : null, engineLegal: es.legal,
                 agree: !!(sv && sv.legal === es.legal),
                 siteO: sv ? Math.round(sv.O) : null, engineO: es.O != null ? Math.round(es.O) : null,
                 siteI: sv ? Math.round(sv.I) : null, engineI: es.I != null ? Math.round(es.I) : null };
      });
      out.siteCompare = cmp;
      out.siteVsEngineAgree = cmp.every(function (c) { return c.agree; });
    }

    console.info("[tradeShadow]", out);
    return out;
  }).catch(function (e) { return { error: "tradeShadow failed: " + e.message }; });
};

/* ------------------------------------------------------------
   window.__capBaseline() — Phase 0 instrument for the unified-state
   ("one world" / #14) migration. Recomputes the DECISION-FREE, apron-view
   trade base for all 30 teams from data/all-teams-detail.json using the
   site's single salary truth (window.playerEffectiveSalary), reproducing
   buildTradeTables.teamCommitted EXACTLY (trade-machine.jsx). Read-only;
   changes nothing on screen.

   Purpose: after each migration phase (especially Phase 2, where
   derivedByTeam replaces the teamCommitted / trade-data-sum / patch
   trichotomy), call this in the console and diff its output against
   Private/_integration/phase0-baseline.json to prove opponent figures
   did not drift. A regression gate that needs no browser interaction
   beyond one eval.
   ------------------------------------------------------------ */
window.__capBaseline = function capBaseline() {
  const PES = window.playerEffectiveSalary;
  if (typeof PES !== "function") return Promise.resolve({ error: "playerEffectiveSalary missing" });
  const dataReady = window.__tmData
    ? Promise.resolve(window.__tmData)
    : fetch("data/all-teams-detail.json", { cache: "no-store" }).then(function (r) { return r.json(); }).then(function (j) { window.__tmData = j; return j; });
  return dataReady.then(function (data) {
    const teams = (data && data.teams) || {};
    // EXACT mirror of buildTradeTables.teamCommitted: two-ways excluded;
    // draft_pick -> +capHold; everyone else -> +PES(p, {}, "apron", false).
    const teamCommitted = function (dt) {
      const ps = (dt && dt.players) || [];
      let total = 0;
      for (let i = 0; i < ps.length; i++) {
        const p = ps[i];
        if (p.contractType === "two_way") continue;
        if (p.offseasonStatus === "draft_pick") { total += Number(p.capHold) || 0; continue; }
        const raw = PES(p, {}, "apron", false);
        total += Number.isFinite(raw) ? raw : 0;
      }
      return Math.round(total);
    };
    const out = {};
    Object.keys(teams).sort().forEach(function (code) { out[code] = teamCommitted(teams[code]); });
    console.info("[capBaseline] 30-team decision-free apron base", out);
    return out;
  }).catch(function (e) { return { error: "capBaseline failed: " + e.message }; });
};
