/**
 * content-circuit.js — the Circuit ⇄ Nexus Connect bridge.
 *
 * Injected ONLY on the Circuit web app (thecircuit-app.vercel.app + localhost dev,
 * see manifest.json). Lets the hosted Circuit render a live "My engine" panel
 * (extension paired? quests queued? XP earned?) without knowing the extension
 * ID — sideloaded unpacked extensions have per-machine IDs, so
 * externally_connectable can't work here; window.postMessage can.
 *
 * Protocol (both directions tagged with a `source` so neither side ever
 * confuses its own messages for the other's):
 *
 *   page → bridge   { source: "circuit-page",  id, type: "nx-ping" }
 *   page → bridge   { source: "circuit-page",  id, type: "nx-api", action }
 *   bridge → page   { source: "nexus-connect", id, ok, data?, error?, build? }
 *   bridge → page   { source: "nexus-connect", type: "nx-bridge-ready", build }   (announce on load)
 *
 * SECURITY:
 *  - The page is UNTRUSTED. Only the read-only actions in NX_BRIDGE_ALLOWED
 *    are forwarded to the service worker — this must never become a generic
 *    proxy (the dashboard's action map includes mutating routes).
 *  - The connect token never crosses this boundary; the service worker adds
 *    it when proxying to the local bot API.
 *  - Replies are posted with the page's own origin as targetOrigin.
 */

(() => {
  "use strict";

  // Read-only quest-seam views only. Adding a mutating action here is a bug.
  const NX_BRIDGE_ALLOWED = new Set(["quests-summary", "engine-health", "quest-grants"]);

  // MUTATING actions the page may REQUEST (type "nx-mutate"). These never run
  // directly — the service worker runs each with THIS install's own connect
  // token; execute-on-your-accounts + outward-facing actions are additionally
  // gated behind an in-extension approval (consent.html). Keep this tight; it
  // is NOT a generic write proxy.
  //   quest-subscribe : run someone's published campaign on YOUR accounts (consent)
  //   search-activate : materialise YOUR funnel selections into a draft Playbook
  //   playbook-publish: publish YOUR playbook as a public community quest (confirm)
  const NX_BRIDGE_ALLOWED_MUTATING = new Set(["quest-subscribe", "search-activate", "playbook-publish", "revoke-grant"]);

  // Explicit origin allow-list — the bridge exposes authenticated engine reads
  // and consent-gated writes, so it must only ever wire up on the real Circuit.
  // The manifest match already limits injection, but a bare
  // `ev.origin === window.location.origin` check would still trust ANY page the
  // script lands on (e.g. any other http://localhost app). Gating on this set
  // means a widened match can never silently re-expose the bridge.
  // MUST stay in sync with manifest.json's content_scripts match for this file,
  // with CIRCUIT_ORIGINS in background.js, and with CIRCUIT_APP_URL in
  // dashboard.js. manifest.json is JSON and cannot import a constant, so the
  // duplication is unavoidable — circuit-origin.drift.test.js asserts all four
  // agree. Until r94 they did NOT: every one of them said
  // "thecircuit.vercel.app", but the app is served from thecircuit-APP, so this
  // allow-list never matched and the bridge never wired up in production.
  const ALLOWED_ORIGINS = new Set([
    "https://thecircuit-app.vercel.app",
    "http://localhost:3000", // Circuit `next dev`
  ]);

  const ORIGIN = window.location.origin;
  if (!ALLOWED_ORIGINS.has(ORIGIN)) return; // not the Circuit — never expose the bridge

  function reply(id, payload) {
    window.postMessage({ source: "nexus-connect", id: id ?? null, ...payload }, ORIGIN);
  }

  window.addEventListener("message", (ev) => {
    // Same-window messages from the page only (postMessage from iframes or
    // other windows has a different source), and only from an allow-listed origin.
    if (ev.source !== window || ev.origin !== ORIGIN) return;
    const m = ev.data;
    if (!m || m.source !== "circuit-page" || typeof m.type !== "string") return;

    if (m.type === "nx-ping") {
      // Presence + build probe. Build comes from the SW so the page can tell
      // a stale bridge from a live one.
      chrome.runtime.sendMessage({ type: "li-build-version" }, (r) => {
        reply(m.id, { ok: true, bridge: true, build: r?.build ?? null });
      });
      return;
    }

    if (m.type === "nx-api") {
      if (!NX_BRIDGE_ALLOWED.has(m.action)) {
        reply(m.id, { ok: false, error: "action not allowed over the bridge" });
        return;
      }
      chrome.runtime.sendMessage({ action: m.action }, (resp) => {
        const err = chrome.runtime.lastError?.message;
        if (err) reply(m.id, { ok: false, error: err });
        else reply(m.id, { ok: !resp?.error, data: resp ?? null, error: resp?.error });
      });
      return;
    }

    if (m.type === "nx-mutate") {
      // Gated write. The SW shows an approval prompt (approve-once-per-quest) and
      // runs it with the extension's own token — the page never sees the token
      // and can't fake the approval. payload is forwarded as-is (SW re-validates).
      if (!NX_BRIDGE_ALLOWED_MUTATING.has(m.action)) {
        reply(m.id, { ok: false, error: "action not allowed over the bridge" });
        return;
      }
      chrome.runtime.sendMessage({ type: "circuit-mutate", action: m.action, payload: m.payload || {} }, (resp) => {
        const err = chrome.runtime.lastError?.message;
        if (err) reply(m.id, { ok: false, error: err });
        else reply(m.id, { ...(resp || {}), ok: !!resp?.ok });
      });
      return;
    }
  });

  // Announce presence so a page that rendered before pinging can react.
  try {
    chrome.runtime.sendMessage({ type: "li-build-version" }, (r) => {
      window.postMessage({ source: "nexus-connect", type: "nx-bridge-ready", build: r?.build ?? null }, ORIGIN);
    });
  } catch { /* SW asleep — the page's ping will wake it */ }
})();
