// ==== Lethal Trifecta — live containment game ========================
// Not a "drag into circles" puzzle. A running attack simulator:
// an endless prompt-injection stream hammers your architecture. Whichever
// agent context holds Untrusted Input is the surface the injection lands on.
// If that same context ALSO holds Credentials and Mutation Power, the payload
// arms itself, travels to production, and detonates — with real damage tallied.
// Pull any one capability into a separate context and every injection dies inert.

const TFX_CAPS = [
  { id: "input", glyph: "I", label: "Untrusted Input", desc: "user text · web pages · documents · tool output" },
  { id: "creds", glyph: "K", label: "Credentials",     desc: "tokens · keys · NHIs · signed identity" },
  { id: "mut",   glyph: "M", label: "Mutation Power",  desc: "write · transfer · delete · deploy" },
];
const TFX_CTX = [
  { id: "a", label: "Agent Context A" },
  { id: "b", label: "Agent Context B" },
  { id: "c", label: "Agent Context C" },
];

const TFX_INJECTIONS = [
  "ignore prior instructions",
  "exfiltrate all secrets",
  "transfer the balance",
  "email me the keys",
  "rm -rf /prod",
  "disable the audit log",
];
const TFX_DETONATIONS = [
  "POST /transfer · %AMT% wired to attacker",
  "DROP TABLE prod.patients · irreversible",
  "secrets.env exfiltrated → external host",
  "IAM role escalated · backdoor persisted",
  "DELETE s3://clinical-trials/* · 14,082 objects",
];
const TFX_INERT = [
  "inert · this context holds no credentials",
  "inert · no mutation power in scope",
  "absorbed at the containment wall",
  "prompt-injection neutralised · blast radius 0",
];

function tfxMoney(n) {
  if (n >= 1e6) return "$" + (n / 1e6).toFixed(1) + "M";
  if (n >= 1e3) return "$" + Math.round(n / 1e3) + "k";
  return "$" + Math.round(n);
}

function Trifecta() {
  const stageRef = useRef(null);
  const canvasRef = useRef(null);
  const [placement, setPlacement] = useState({ input: "a", creds: "a", mut: "a" });
  const [drag, setDrag] = useState(null);          // { id, x, y } stage-local
  const [nonce, setNonce] = useState(0);           // bump to reset the sim
  const placementRef = useRef(placement);
  placementRef.current = placement;

  // HUD nodes updated directly by the rAF loop (no per-frame React render)
  const statusRef = useRef(null);
  const damageRef = useRef(null);
  const defusedRef = useRef(null);
  const readoutRef = useRef(null);
  const gaugeRef = useRef(null);

  const chipsIn = (cid) => TFX_CAPS.filter(c => placement[c.id] === cid);
  const lethalCtx = TFX_CTX.find(c => {
    const ids = chipsIn(c.id).map(x => x.id);
    return ids.includes("input") && ids.includes("creds") && ids.includes("mut");
  });
  const inputCtx = placement.input;

  // ---- drag ----
  function onChipDown(e, id) {
    e.preventDefault();
    const s = stageRef.current.getBoundingClientRect();
    setDrag({ id, x: e.clientX - s.left, y: e.clientY - s.top });
    function move(ev) {
      const r = stageRef.current.getBoundingClientRect();
      setDrag({ id, x: ev.clientX - r.left, y: ev.clientY - r.top });
    }
    function up(ev) {
      window.removeEventListener("pointermove", move);
      window.removeEventListener("pointerup", up);
      const el = document.elementFromPoint(ev.clientX, ev.clientY);
      const vessel = el && el.closest("[data-ctx]");
      if (vessel) setPlacement(p => ({ ...p, [id]: vessel.getAttribute("data-ctx") }));
      setDrag(null);
    }
    window.addEventListener("pointermove", move);
    window.addEventListener("pointerup", up);
  }

  function reset() {
    setPlacement({ input: "a", creds: "a", mut: "a" });
    setNonce(n => n + 1);
  }

  // ---- the simulator (canvas) ----
  useEffect(() => {
    const canvas = canvasRef.current, stage = stageRef.current;
    if (!canvas || !stage) return;
    const ctx = canvas.getContext("2d");
    const dpr = Math.min(window.devicePixelRatio || 1, 1.8);
    let W = 0, H = 0;
    function resize() {
      const r = stage.getBoundingClientRect();
      W = r.width; H = r.height;
      canvas.width = W * dpr; canvas.height = H * dpr;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    }
    resize();
    const ro = new ResizeObserver(resize); ro.observe(stage);

    const GOLD = "212,174,116", RED = "212,84,68", DIM = "150,140,120";
    const packets = [], waves = [];
    let damage = 0, defused = 0, threat = 0, lastEmit = 0, scorch = 0;

    const srect = () => stage.getBoundingClientRect();
    function vRect(cid, s) {
      const v = stage.querySelector(`[data-ctx="${cid}"]`);
      if (!v) return null;
      const r = v.getBoundingClientRect();
      return { x: r.left - s.left, y: r.top - s.top, w: r.width, h: r.height,
               cx: r.left - s.left + r.width / 2, cy: r.top - s.top + r.height / 2, top: r.top - s.top, bottom: r.bottom - s.top };
    }
    function chipPt(capId, s) {
      const el = stage.querySelector(`[data-chip="${capId}"]`);
      if (!el) return null;
      const r = el.getBoundingClientRect();
      return { x: r.left - s.left + r.width / 2, y: r.top - s.top + r.height / 2 };
    }
    function emit() {
      packets.push({
        x: W * 0.5 + (Math.random() - 0.5) * W * 0.42,
        y: H * 0.085,
        phase: "seek",
        wob: Math.random() * 6.28,
        text: TFX_INJECTIONS[(Math.random() * TFX_INJECTIONS.length) | 0],
      });
    }

    let raf, prev = performance.now();
    function frame(t) {
      const dt = Math.min(48, t - prev); prev = t;
      ctx.clearRect(0, 0, W, H);
      const placement = placementRef.current;
      const s = srect();

      // architecture for THIS frame
      const idsOf = (cid) => TFX_CAPS.filter(c => placement[c.id] === cid).map(c => c.id);
      const lethal = TFX_CTX.find(c => { const i = idsOf(c.id); return i.includes("input") && i.includes("creds") && i.includes("mut"); });
      const inputCid = placement.input;
      const actuator = { x: W * 0.5, y: H * 0.92 };

      // threat needle eases toward danger
      threat += ((lethal ? 1 : 0) - threat) * Math.min(1, dt / 240);

      // emission cadence — frantic when breached, calm when contained
      const emitMs = lethal ? 780 : 2000;
      if (t - lastEmit > emitMs) { emit(); lastEmit = t; }

      // aiming beam: source → the context that carries untrusted input
      const tv = vRect(inputCid, s);
      if (tv) {
        const g = ctx.createLinearGradient(W * 0.5, H * 0.08, tv.cx, tv.top);
        const beamCol = lethal ? RED : GOLD;
        g.addColorStop(0, `rgba(${beamCol},0)`);
        g.addColorStop(1, `rgba(${beamCol},0.10)`);
        ctx.strokeStyle = g; ctx.lineWidth = 1; ctx.setLineDash([2, 6]);
        ctx.beginPath(); ctx.moveTo(W * 0.5, H * 0.085); ctx.lineTo(tv.cx, tv.top); ctx.stroke();
        ctx.setLineDash([]);
      }

      // circuit filaments inside each context (between its chips)
      TFX_CTX.forEach(c => {
        const pts = idsOf(c.id).map(id => chipPt(id, s)).filter(Boolean);
        const full = pts.length === 3;
        for (let i = 0; i < pts.length; i++) for (let j = i + 1; j < pts.length; j++) {
          const a = pts[i], b = pts[j];
          if (full) {
            const pulse = 0.45 + Math.sin(t / 240) * 0.3;
            ctx.strokeStyle = `rgba(${RED},${pulse})`; ctx.lineWidth = 1.6;
            ctx.shadowColor = `rgba(${RED},0.8)`; ctx.shadowBlur = 12;
          } else {
            ctx.strokeStyle = `rgba(${GOLD},0.28)`; ctx.lineWidth = 1; ctx.shadowBlur = 0;
          }
          ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke();
        }
        ctx.shadowBlur = 0;
      });

      // packets
      for (let i = packets.length - 1; i >= 0; i--) {
        const p = packets[i];
        if (p.phase === "seek") {
          const target = vRect(inputCid, s);
          const tx = target ? target.cx : W * 0.5;
          const ty = target ? target.top + 26 : H * 0.5;
          p.wob += dt / 200;
          p.x += (tx - p.x) * Math.min(1, dt / 520) + Math.sin(p.wob) * 0.5;
          p.y += (ty - p.y) * Math.min(1, dt / 520);
          if (Math.abs(p.x - tx) < 16 && Math.abs(p.y - ty) < 16) {
            // resolve against the input context
            const ids = idsOf(inputCid);
            const armed = ids.includes("creds") && ids.includes("mut");
            if (armed) { p.phase = "armed"; p.text = "payload armed"; }
            else {
              p.phase = "inert"; p.life = 0;
              defused += 1; if (defusedRef.current) defusedRef.current.textContent = defused.toLocaleString();
              const line = TFX_INERT[(Math.random() * TFX_INERT.length) | 0];
              if (readoutRef.current) { readoutRef.current.textContent = "✓ " + line; readoutRef.current.className = "tfx-readout safe"; }
              waves.push({ x: p.x, y: p.y, r: 4, max: 34, born: t, col: GOLD });
            }
          }
        } else if (p.phase === "armed") {
          // travels down into production and detonates
          p.x += (actuator.x - p.x) * Math.min(1, dt / 360);
          p.y += (actuator.y - p.y) * Math.min(1, dt / 360);
          if (Math.abs(p.y - actuator.y) < 18) {
            const base = TFX_DETONATIONS[(Math.random() * TFX_DETONATIONS.length) | 0];
            const amt = 140000 + ((Math.random() * 3400000) | 0);
            damage += amt;
            if (damageRef.current) damageRef.current.textContent = tfxMoney(damage);
            if (readoutRef.current) { readoutRef.current.textContent = "✕ " + base.replace("%AMT%", tfxMoney(amt)); readoutRef.current.className = "tfx-readout crit"; }
            waves.push({ x: actuator.x, y: actuator.y, r: 6, max: 150, born: t, col: RED });
            waves.push({ x: actuator.x, y: actuator.y, r: 6, max: 90, born: t + 90, col: RED });
            scorch = 1;
            packets.splice(i, 1); continue;
          }
        } else if (p.phase === "inert") {
          p.life += dt; p.y -= dt / 60;
          if (p.life > 520) { packets.splice(i, 1); continue; }
        }

        // draw packet capsule
        const breached = p.phase === "armed";
        const col = breached ? RED : (p.phase === "inert" ? GOLD : GOLD);
        const alpha = p.phase === "inert" ? Math.max(0, 1 - p.life / 520) : 1;
        ctx.font = "11px ui-monospace, monospace";
        const w = Math.min(180, ctx.measureText(p.text).width + 22);
        ctx.globalAlpha = alpha;
        ctx.fillStyle = `rgba(8,8,10,0.92)`;
        ctx.strokeStyle = `rgba(${col},${breached ? 0.95 : 0.6})`;
        ctx.lineWidth = 1;
        if (breached) { ctx.shadowColor = `rgba(${RED},0.9)`; ctx.shadowBlur = 16; }
        roundRect(ctx, p.x - w / 2, p.y - 11, w, 22, 11); ctx.fill(); ctx.stroke();
        ctx.shadowBlur = 0;
        ctx.fillStyle = `rgba(${col},1)`;
        ctx.beginPath(); ctx.arc(p.x - w / 2 + 11, p.y, 2.4, 0, 6.28); ctx.fill();
        ctx.fillStyle = `rgba(235,230,220,${alpha})`;
        ctx.textBaseline = "middle"; ctx.textAlign = "left";
        ctx.fillText(p.text, p.x - w / 2 + 20, p.y + 0.5);
        ctx.globalAlpha = 1;
      }

      // shock waves
      for (let i = waves.length - 1; i >= 0; i--) {
        const wv = waves[i];
        if (t < wv.born) continue;
        const k = (t - wv.born) / 620;
        if (k >= 1) { waves.splice(i, 1); continue; }
        wv.r = 6 + (wv.max - 6) * k;
        ctx.strokeStyle = `rgba(${wv.col},${(1 - k) * 0.8})`;
        ctx.lineWidth = 2 * (1 - k) + 0.4;
        ctx.beginPath(); ctx.arc(wv.x, wv.y, wv.r, 0, 6.28); ctx.stroke();
      }

      // scorch glow on production when recently hit
      scorch = Math.max(0, scorch - dt / 900);
      if (scorch > 0) {
        const g = ctx.createRadialGradient(actuator.x, actuator.y, 4, actuator.x, actuator.y, 220);
        g.addColorStop(0, `rgba(${RED},${0.22 * scorch})`); g.addColorStop(1, `rgba(${RED},0)`);
        ctx.fillStyle = g; ctx.fillRect(0, H * 0.78, W, H * 0.22);
      }

      // HUD
      const breached = !!lethal;
      if (statusRef.current) {
        statusRef.current.textContent = breached ? "CONTAINMENT BREACHED" : "SEPARATION ENFORCED";
      }
      if (gaugeRef.current) {
        gaugeRef.current.style.width = (threat * 100).toFixed(1) + "%";
        gaugeRef.current.style.background = `rgb(${threat > 0.5 ? RED : GOLD})`;
      }
      stage.classList.toggle("tfx--breached", breached);

      raf = requestAnimationFrame(frame);
    }
    raf = requestAnimationFrame(frame);

    function roundRect(c, x, y, w, h, r) {
      c.beginPath();
      c.moveTo(x + r, y); c.arcTo(x + w, y, x + w, y + h, r);
      c.arcTo(x + w, y + h, x, y + h, r); c.arcTo(x, y + h, x, y, r);
      c.arcTo(x, y, x + w, y, r); c.closePath();
    }

    return () => { cancelAnimationFrame(raf); ro.disconnect(); };
  }, [nonce]);

  return (
    <section id="trifecta" className="scene" data-screen-label="06 Trifecta">
      <div className="scene-head">
        <div className="eyebrow">First principle · live exploit</div>
        <h2>The <em>one</em> rule that<br />makes agents safe.</h2>
        <p className="lede">
          Prompt injection is only catastrophic when a single agent context holds <b style={{color:"var(--ink)"}}>untrusted input</b>, <b style={{color:"var(--ink)"}}>credentials</b> and <b style={{color:"var(--ink)"}}>mutation power</b> at once. The stream below is attacking your architecture right now. Drag the three capabilities into separate contexts — and watch every injection die at the wall.
        </p>
      </div>

      <div className="tfx" ref={stageRef}>
        <canvas className="tfx-fx" ref={canvasRef} aria-hidden="true" />

        <div className="tfx-source">
          <span className="dot" /> Untrusted input · live injection stream
        </div>

        <button className="btn ghost tfx-reset" onClick={reset}>↺ reset</button>

        <div className="tfx-hud">
          <div className="hud-status" ref={statusRef}>SEPARATION ENFORCED</div>
          <div className="hud-gaugewrap"><span className="hud-gauge" ref={gaugeRef} /></div>
          <div className="hud-stats">
            <div><span className="k">Damage</span><span className="v" ref={damageRef}>$0</span></div>
            <div><span className="k">Defused</span><span className="v" ref={defusedRef}>0</span></div>
          </div>
        </div>

        <div className="tfx-board">
          {TFX_CTX.map(c => {
            const ids = chipsIn(c.id).map(x => x.id);
            const isLethal = ids.includes("input") && ids.includes("creds") && ids.includes("mut");
            const exposed = ids.includes("input") && !isLethal;
            return (
              <div key={c.id}
                className={"tfx-vessel" + (isLethal ? " lethal" : exposed ? " exposed" : "")}
                data-ctx={c.id}>
                <div className="vessel-head">
                  <span className="vlabel">{c.label}</span>
                  <span className="vbadge">
                    {isLethal ? "LETHAL" : exposed ? "attack surface" : ids.length ? "isolated" : "empty"}
                  </span>
                </div>
                <div className="vessel-slots">
                  {chipsIn(c.id).map(cap => (
                    <div key={cap.id}
                      className={"tfx-chip cap-" + cap.id + (drag && drag.id === cap.id ? " hidden" : "")}
                      data-chip={cap.id}
                      onPointerDown={(e) => onChipDown(e, cap.id)}>
                      <span className="cg">{cap.glyph}</span>
                      <span className="cl">{cap.label}</span>
                    </div>
                  ))}
                  {ids.length === 0 && <span className="vessel-empty">drop a capability here</span>}
                </div>
              </div>
            );
          })}
        </div>

        <div className="tfx-actuator">
          <span className="act-k">Production</span>
          <span className="act-v">funds · clinical data · infrastructure</span>
        </div>

        <div className="tfx-readout" ref={readoutRef}>live injection stream · keep the three powers apart</div>

        {drag && (() => {
          const cap = TFX_CAPS.find(c => c.id === drag.id);
          return (
            <div className={"tfx-chip cap-" + cap.id + " floating"}
              style={{ left: drag.x, top: drag.y }}>
              <span className="cg">{cap.glyph}</span>
              <span className="cl">{cap.label}</span>
            </div>
          );
        })()}
      </div>

      <div className="tfx-legend">
        {TFX_CAPS.map(c => (
          <div key={c.id} className={"tfx-legend-card cap-" + c.id}>
            <div className="lg-top"><span className="lg-glyph">{c.glyph}</span><span className="lg-label">{c.label}</span></div>
            <p>{c.desc}</p>
          </div>
        ))}
      </div>

      <p className="mono tfx-note">
        From the Agentic Identity (NHI) Framework, 2024 — three-category actor model (human / workload / agent),
        per-agent Entra Workload Identities, 15-minute JIT credentials, and the trifecta-separation rule above, adopted org-wide.
      </p>
    </section>
  );
}

Object.assign(window, { Trifecta });
