// Runs in the page's MAIN world at document_start. Monkey-patches network
// and messaging APIs so request/response bodies, WebSocket frames, SSE
// events, beacons, and postMessage traffic are captured. Results are
// forwarded to bridge.js (isolated world) via a tagged window.postMessage
// using the ORIGINAL postMessage reference captured before patching.

(() => {
  const TAG = "__casino_org_inspector__";
  const MAX_BODY = 8192;

  // Capture originals BEFORE any patching so our bridge channel is safe.
  const origPostMessage = window.postMessage.bind(window);
  const origJSONStringify = JSON.stringify;

  function send(payload) {
    try {
      origPostMessage({ [TAG]: true, payload }, "*");
    } catch (_) {
      // ignore
    }
  }

  function truncate(s) {
    if (typeof s !== "string") return s;
    if (s.length <= MAX_BODY) return s;
    return s.slice(0, MAX_BODY) + `... [truncated, ${s.length - MAX_BODY} more chars]`;
  }

  function safeStringify(v) {
    try {
      return origJSONStringify(v);
    } catch (_) {
      try {
        return String(v);
      } catch (_) {
        return "[unserializable]";
      }
    }
  }

  function describeBody(body) {
    if (body == null) return null;
    try {
      if (typeof body === "string") return { kind: "text", data: truncate(body) };
      if (body instanceof FormData) {
        const out = {};
        for (const [k, v] of body.entries()) {
          out[k] = typeof v === "string"
            ? truncate(v)
            : `[File ${v.name || ""} ${v.type || ""} ${v.size || 0}B]`;
        }
        return { kind: "formdata", data: out };
      }
      if (body instanceof URLSearchParams) {
        return { kind: "urlencoded", data: truncate(body.toString()) };
      }
      if (body instanceof Blob) {
        return { kind: "blob", data: `[Blob ${body.type} ${body.size}B]` };
      }
      if (body instanceof ArrayBuffer) {
        return { kind: "binary", data: `[ArrayBuffer ${body.byteLength}B]` };
      }
      if (ArrayBuffer.isView(body)) {
        return { kind: "binary", data: `[${body.constructor.name} ${body.byteLength}B]` };
      }
      if (typeof body === "object") {
        return { kind: "json", data: truncate(safeStringify(body)) };
      }
      return { kind: "other", data: truncate(String(body)) };
    } catch (e) {
      return { kind: "error", data: String(e) };
    }
  }

  function headersToObj(headers) {
    if (!headers) return {};
    try {
      if (typeof Headers !== "undefined" && headers instanceof Headers) {
        const o = {};
        headers.forEach((v, k) => (o[k] = v));
        return o;
      }
      if (Array.isArray(headers)) {
        const o = {};
        headers.forEach((p) => {
          if (Array.isArray(p) && p.length >= 2) o[p[0]] = p[1];
        });
        return o;
      }
      if (typeof headers === "object") return { ...headers };
      return {};
    } catch (_) {
      return {};
    }
  }

  function parseRawHeaders(raw) {
    const out = {};
    if (!raw) return out;
    raw.split(/\r?\n/).forEach((line) => {
      const i = line.indexOf(":");
      if (i > -1) out[line.slice(0, i).trim().toLowerCase()] = line.slice(i + 1).trim();
    });
    return out;
  }

  // ---------------------------------------------------------------- fetch
  const origFetch = window.fetch;
  if (origFetch) {
    window.fetch = async function patchedFetch(input, init) {
      const startTs = Date.now();
      const isRequest = typeof Request !== "undefined" && input instanceof Request;
      const url = isRequest ? input.url : String(input);
      const method = (init && init.method) || (isRequest ? input.method : "GET");
      const requestHeaders = headersToObj((init && init.headers) || (isRequest ? input.headers : null));
      const requestBody = describeBody(init && init.body);

      let response;
      try {
        response = await origFetch.apply(this, arguments);
      } catch (err) {
        send({
          kind: "fetch",
          phase: "error",
          url, method, requestHeaders, requestBody,
          error: String(err),
          startTs, endTs: Date.now()
        });
        throw err;
      }

      let responseBody = null;
      try {
        const cloned = response.clone();
        const ct = (response.headers && response.headers.get("content-type")) || "";
        if (/^(text\/|application\/(json|xml|javascript|x-www-form-urlencoded)|application\/.*\+json)/i.test(ct) || ct === "") {
          const txt = await cloned.text();
          responseBody = { kind: "text", data: truncate(txt) };
        } else {
          const blob = await cloned.blob();
          responseBody = { kind: "blob", data: `[Blob ${blob.type} ${blob.size}B]` };
        }
      } catch (e) {
        responseBody = { kind: "error", data: String(e) };
      }

      send({
        kind: "fetch",
        phase: "complete",
        url: response.url || url,
        method,
        requestHeaders,
        requestBody,
        status: response.status,
        statusText: response.statusText,
        responseHeaders: headersToObj(response.headers),
        responseBody,
        startTs,
        endTs: Date.now()
      });
      return response;
    };
  }

  // ------------------------------------------------------------------ XHR
  const XHR = window.XMLHttpRequest;
  if (XHR) {
    const origOpen = XHR.prototype.open;
    const origSend = XHR.prototype.send;
    const origSetHeader = XHR.prototype.setRequestHeader;

    XHR.prototype.open = function (method, url) {
      this.__casinoMeta = { method, url, headers: {}, startTs: Date.now() };
      return origOpen.apply(this, arguments);
    };

    XHR.prototype.setRequestHeader = function (k, v) {
      try { if (this.__casinoMeta) this.__casinoMeta.headers[k] = v; } catch (_) {}
      return origSetHeader.apply(this, arguments);
    };

    XHR.prototype.send = function (body) {
      const meta = this.__casinoMeta || { method: "GET", url: "", headers: {}, startTs: Date.now() };
      meta.requestBody = describeBody(body);

      this.addEventListener("loadend", () => {
        let responseBody;
        try {
          const rt = this.responseType;
          if (rt === "" || rt === "text") {
            responseBody = { kind: "text", data: truncate(this.responseText || "") };
          } else if (rt === "json") {
            responseBody = { kind: "json", data: truncate(safeStringify(this.response)) };
          } else if (rt === "blob" && this.response) {
            responseBody = { kind: "blob", data: `[Blob ${this.response.type} ${this.response.size}B]` };
          } else if (rt === "arraybuffer" && this.response) {
            responseBody = { kind: "binary", data: `[ArrayBuffer ${this.response.byteLength}B]` };
          } else if (rt === "document" && this.response) {
            responseBody = { kind: "document", data: "[Document response]" };
          } else {
            responseBody = { kind: "other", data: `[responseType=${rt}]` };
          }
        } catch (e) {
          responseBody = { kind: "error", data: String(e) };
        }

        let responseHeaders = {};
        try { responseHeaders = parseRawHeaders(this.getAllResponseHeaders()); } catch (_) {}

        send({
          kind: "xhr",
          phase: this.status === 0 ? "error" : "complete",
          method: meta.method,
          url: this.responseURL || meta.url,
          requestHeaders: meta.headers,
          requestBody: meta.requestBody,
          status: this.status,
          statusText: this.statusText,
          responseHeaders,
          responseBody,
          startTs: meta.startTs,
          endTs: Date.now()
        });
      });

      return origSend.apply(this, arguments);
    };
  }

  // ------------------------------------------------------------ WebSocket
  const OrigWS = window.WebSocket;
  if (OrigWS) {
    function describeFrame(d) {
      if (typeof d === "string") return { kind: "text", data: truncate(d) };
      if (typeof Blob !== "undefined" && d instanceof Blob) return { kind: "blob", data: `[Blob ${d.type} ${d.size}B]` };
      if (d instanceof ArrayBuffer) return { kind: "binary", data: `[ArrayBuffer ${d.byteLength}B]` };
      if (ArrayBuffer.isView(d)) return { kind: "binary", data: `[${d.constructor.name} ${d.byteLength}B]` };
      return { kind: "other", data: truncate(String(d)) };
    }

    try {
      window.WebSocket = new Proxy(OrigWS, {
        construct(target, args) {
          const url = args[0];
          const protocols = args[1];
          const ws = new target(...args);
          send({ kind: "ws", phase: "open-init", url: String(url), protocols: protocols || null, startTs: Date.now() });
          ws.addEventListener("open",  () => send({ kind: "ws", phase: "open",  url: String(url), ts: Date.now() }));
          ws.addEventListener("close", (e) => send({ kind: "ws", phase: "close", url: String(url), code: e.code, reason: e.reason, wasClean: e.wasClean, ts: Date.now() }));
          ws.addEventListener("error", () => send({ kind: "ws", phase: "error", url: String(url), ts: Date.now() }));
          ws.addEventListener("message", (e) => {
            send({ kind: "ws", phase: "recv", url: String(url), data: describeFrame(e.data), ts: Date.now() });
          });
          const origWsSend = ws.send.bind(ws);
          ws.send = function (d) {
            try { send({ kind: "ws", phase: "send", url: String(url), data: describeFrame(d), ts: Date.now() }); } catch (_) {}
            return origWsSend(d);
          };
          return ws;
        }
      });
    } catch (_) {
      // page froze the global — leave it alone
    }
  }

  // ---------------------------------------------------------- EventSource
  const OrigES = window.EventSource;
  if (OrigES) {
    try {
      window.EventSource = new Proxy(OrigES, {
        construct(target, args) {
          const url = args[0];
          const es = new target(...args);
          send({ kind: "sse", phase: "open-init", url: String(url), startTs: Date.now() });
          es.addEventListener("open",  () => send({ kind: "sse", phase: "open",  url: String(url), ts: Date.now() }));
          es.addEventListener("error", () => send({ kind: "sse", phase: "error", url: String(url), ts: Date.now() }));
          es.addEventListener("message", (e) => {
            send({
              kind: "sse",
              phase: "message",
              url: String(url),
              lastEventId: e.lastEventId || null,
              data: { kind: "text", data: truncate(String(e.data)) },
              ts: Date.now()
            });
          });
          return es;
        }
      });
    } catch (_) {}
  }

  // -------------------------------------------------------- sendBeacon
  if (navigator && typeof navigator.sendBeacon === "function") {
    const origBeacon = navigator.sendBeacon.bind(navigator);
    try {
      navigator.sendBeacon = function (url, data) {
        send({
          kind: "beacon",
          url: String(url),
          data: describeBody(data),
          ts: Date.now()
        });
        return origBeacon(url, data);
      };
    } catch (_) {}
  }

  // -------------------------------------------------------- postMessage
  // Outgoing: patch window.postMessage so we see what the page (and iframes
  // via window references) try to send. We must skip our own bridge frames.
  try {
    const desc = Object.getOwnPropertyDescriptor(window, "postMessage") ||
                 Object.getOwnPropertyDescriptor(Window.prototype, "postMessage");
    const patched = function patchedPostMessage(message, targetOrigin, transfer) {
      try {
        if (!(message && typeof message === "object" && message[TAG])) {
          const preview = typeof message === "string" ? truncate(message) : truncate(safeStringify(message));
          send({
            kind: "postmessage",
            phase: "out",
            targetOrigin: targetOrigin || null,
            data: { kind: typeof message === "string" ? "text" : "json", data: preview },
            ts: Date.now()
          });
        }
      } catch (_) {}
      // Third-party widgets (Hotjar, Stripe, hCaptcha, ...) sometimes
      // call `window.postMessage(msg, "<their-origin>")` on the
      // hosting page — i.e. they target their own iframe's origin
      // but post to the page window itself. The recipient origin
      // (the page) doesn't match `targetOrigin`, so Chromium's
      // platform implementation prints
      //   "Failed to execute 'postMessage' on 'DOMWindow': ..."
      // straight to the dev console BEFORE the JS SecurityError
      // surfaces. That console line is emitted by C++ and cannot be
      // suppressed from JS — even a try/catch wrapped around the
      // call leaves the line in the console with our
      // `patchedPostMessage` at the top of the stack, which makes it
      // look like the extension is at fault.
      //
      // Fix (chat 2026-05-23): pre-validate `targetOrigin` against
      // this window's origin and short-circuit if they can't
      // possibly match. The original call would have thrown anyway
      // (and the message would have gone nowhere), so returning
      // `undefined` here is observationally identical from the
      // page's POV minus the unsolicited console noise. We still
      // catch any unexpected throw for safety.
      try {
        if (typeof targetOrigin === "string" &&
            targetOrigin !== "*" &&
            targetOrigin !== "/" &&
            targetOrigin !== location.origin) {
          return undefined;
        }
        return origPostMessage(message, targetOrigin, transfer);
      } catch (e) {
        try {
          console.warn("[CASINO] page postMessage failed:", e && e.message || e,
                       "targetOrigin=", targetOrigin);
        } catch (_) {}
        return undefined;
      }
    };
    if (desc && desc.configurable) {
      Object.defineProperty(window, "postMessage", { value: patched, configurable: true, writable: true });
    } else {
      window.postMessage = patched;
    }
  } catch (_) {}

  // Incoming: capture every received message that isn't ours. Use the
  // capture phase to run before page handlers.
  window.addEventListener("message", (e) => {
    try {
      const d = e.data;
      if (d && typeof d === "object" && d[TAG]) return; // our own bridge frame
      const preview = typeof d === "string" ? truncate(d) : truncate(safeStringify(d));
      send({
        kind: "postmessage",
        phase: "in",
        origin: e.origin || null,
        data: { kind: typeof d === "string" ? "text" : "json", data: preview },
        ts: Date.now()
      });
    } catch (_) {}
  }, true);
})();
