// Offscreen host for the on-device DYNAMICS-WEB student.
// transformers.js (bundled, no remote code) turns page text into a 384-d MiniLM
// sentence embedding (mean pool + L2 — identical to how the teacher labels were
// embedded), then a tiny pure-JS head maps 384 -> the 8 DYNAMICS-WEB axes.
// This computes DESCRIPTIVE page character only. The mapping M (person -> content)
// is never here, never in the bundle — it stays server side (trade-secret model).
import { pipeline, env } from "./vendor/transformers.min.js";

// All local, all bundled. No CDN, no telemetry — the privacy/data-minimisation story.
env.allowRemoteModels = false;
env.allowLocalModels = true;
env.localModelPath = "./models/";
// The model is bundled in the extension; transformers.js otherwise tries to Cache.put()
// the fetched files, which throws "scheme 'chrome-extension' is unsupported" in offscreen.
// We already have the files locally, so the browser cache is pointless here — turn it off.
env.useBrowserCache = false;
env.backends.onnx.wasm.wasmPaths = "./vendor/";
env.backends.onnx.wasm.numThreads = 1;   // no cross-origin isolation in an extension page
env.backends.onnx.wasm.proxy = false;

let pipePromise = null;
let headPromise = null;

function getPipe() {
  if (!pipePromise) pipePromise = pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2");
  return pipePromise;
}
function getHead() {
  if (!headPromise) headPromise = fetch(chrome.runtime.getURL("models/head.json")).then(r => r.json());
  return headPromise;
}

// erf via Abramowitz & Stegun 7.1.26 (max err ~1.5e-7) so GELU matches torch's exact (non-tanh) form.
function erf(x) {
  const s = x < 0 ? -1 : 1; x = Math.abs(x);
  const t = 1 / (1 + 0.3275911 * x);
  const y = 1 - (((((1.061405429 * t - 1.453152027) * t) + 1.421413741) * t - 0.284496736) * t + 0.254829592) * t * Math.exp(-x * x);
  return s * y;
}
function applyHead(model, emb) {
  let v = Array.from(emb);
  for (const L of model.layers) {
    if (L.type === "linear") {
      const out = new Array(L.w.length);
      for (let i = 0; i < L.w.length; i++) {
        let s = L.b[i]; const wi = L.w[i];
        for (let j = 0; j < wi.length; j++) s += wi[j] * v[j];
        out[i] = s;
      }
      v = out;
    } else if (L.type === "gelu") {
      v = v.map(z => 0.5 * z * (1 + erf(z / Math.SQRT2)));
    } else if (L.type === "sigmoid") {
      v = v.map(z => 1 / (1 + Math.exp(-z)));
    }
  }
  return v;
}

chrome.runtime.onMessage.addListener((msg, _sender, send) => {
  if (!msg || msg.target !== "offscreen" || msg.type !== "embed") return false;
  (async () => {
    try {
      const [pipe, head] = await Promise.all([getPipe(), getHead()]);
      const out = await pipe(msg.text || "", { pooling: "mean", normalize: true });
      const vals = applyHead(head, out.data);
      const axes = {};
      head.axes.forEach((a, i) => { axes[a] = Math.max(0, Math.min(1, vals[i])); });
      send({ ok: true, axes });
    } catch (e) {
      send({ ok: false, error: String(e && e.message || e) });
    }
  })();
  return true; // async response
});
