/* ============================================================
   Airport Cyber Resilience Simulator — Kiosk Frontend
   Renders the data-driven infrastructure map, drives GSAP
   animation, and speaks the WebSocket protocol to the engine.
   ============================================================ */

const SVGNS = "http://www.w3.org/2000/svg";
const MAP_OFFSET_X = 460;      // sidebar width; node coords are full-canvas
const IDLE_MS = 90_000;        // 90s -> attract loop

const state = {
  infra: null, playbooks: null, compliance: null,
  nodeEls: {}, linkEls: {}, nodePos: {},
  nodeStatus: {},              // id -> normal|compromised|isolated|reduced|patched
  nodeVector: {},              // id -> last threat vector
  activePlaybook: null,
  simStart: 0, clockTimer: null, idleTimer: null,
  ws: null, ambientTimers: [],
  patchTimers: {},
};

/* -------------------- stage scaling -------------------- */
function fitStage() {
  const stage = document.getElementById("stage");
  const s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
  stage.style.transform = `scale(${s})`;
}
window.addEventListener("resize", fitStage);

/* -------------------- helpers -------------------- */
const el = (tag, attrs = {}) => {
  const e = document.createElementNS(SVGNS, tag);
  for (const k in attrs) e.setAttribute(k, attrs[k]);
  return e;
};
const mapX = (x) => x - MAP_OFFSET_X;

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

/* -------------------- bootstrap -------------------- */
async function boot() {
  fitStage();
  const data = await (await fetch("/api/bootstrap")).json();
  state.infra = data.infrastructure;
  state.playbooks = data.playbooks;
  state.compliance = data.compliance;

  buildGrid();
  buildLinks();
  buildNodes();
  buildPlaybookList();
  wireUI();
  connectWS();
  resetIdle();
  showAttract(true);
}

/* -------------------- grid backdrop -------------------- */
function buildGrid() {
  const g = document.getElementById("grid-layer");
  for (let x = 0; x <= 1460; x += 60)
    g.appendChild(el("line", { x1: x, y1: 0, x2: x, y2: 1080, stroke: "#0c2033", "stroke-width": 1, opacity: .5 }));
  for (let y = 0; y <= 1080; y += 60)
    g.appendChild(el("line", { x1: 0, y1: y, x2: 1460, y2: y, stroke: "#0c2033", "stroke-width": 1, opacity: .5 }));
}

/* -------------------- links -------------------- */
function buildLinks() {
  const nodes = Object.fromEntries(state.infra.nodes.map(n => [n.id, n]));
  const layer = document.getElementById("link-layer");
  state.infra.links.forEach(lk => {
    const a = nodes[lk.source], b = nodes[lk.target];
    if (!a || !b) return;
    const line = el("line", {
      x1: mapX(a.x), y1: a.y, x2: mapX(b.x), y2: b.y, class: "link"
    });
    layer.appendChild(line);
    state.linkEls[`${lk.source}|${lk.target}`] = line;
    state.linkEls[`${lk.target}|${lk.source}`] = line;
    startAmbientParticles(a, b);
  });
}

/* gentle green data particles gliding on every link */
function startAmbientParticles(a, b) {
  const layer = document.getElementById("particle-layer");
  const p = el("circle", { class: "particle g", cx: mapX(a.x), cy: a.y });
  layer.appendChild(p);
  const tl = gsap.to(p, {
    duration: 3 + Math.random() * 2, repeat: -1, ease: "none",
    delay: Math.random() * 3,
    onRepeat() { p.setAttribute("opacity", 0.9); },
    keyframes: {
      "0%":   { attr: { cx: mapX(a.x), cy: a.y }, opacity: 0 },
      "12%":  { opacity: 0.9 },
      "88%":  { opacity: 0.9 },
      "100%": { attr: { cx: mapX(b.x), cy: b.y }, opacity: 0 },
    }
  });
  state.ambientTimers.push(tl);
}

/* -------------------- nodes -------------------- */
function buildNodes() {
  const layer = document.getElementById("node-layer");
  state.infra.nodes.forEach(n => {
    state.nodePos[n.id] = { x: mapX(n.x), y: n.y };
    state.nodeStatus[n.id] = "normal";

    const g = el("g", { class: "node", "data-id": n.id });
    g.setAttribute("transform", `translate(${mapX(n.x)},${n.y})`);

    const halo = el("circle", { class: "halo", r: 44 });
    const core = el("circle", { class: "core", r: 34 });
    const use = el("use", { class: "ico", href: `#node-${n.icon}`, x: -24, y: -24, width: 48, height: 48 });
    const label = el("text", { class: "label", x: 0, y: 66 });
    label.textContent = n.name;
    const cap = el("text", { class: "cap", x: 0, y: 84 });
    cap.textContent = "100%";

    g.append(halo, core, use, label, cap);
    layer.appendChild(g);

    // idle pulse
    gsap.to(halo, { attr: { r: 48 }, opacity: 0.45, duration: 1.6, repeat: -1, yoyo: true, ease: "sine.inOut", delay: Math.random() });

    g.addEventListener("click", () => { resetIdle(); openNode(n.id); });
    state.nodeEls[n.id] = { g, halo, core, use, label, cap };
  });
}

/* -------------------- playbook list -------------------- */
function buildPlaybookList() {
  const list = document.getElementById("playbook-list");
  list.innerHTML = "";
  state.playbooks.forEach((pb, i) => {
    const card = document.createElement("div");
    card.className = "pb-card";
    card.dataset.id = pb.playbook_id;
    card.innerHTML = `<div class="pb-idx">0${i + 1}</div>
      <div class="pb-meta"><b>${pb.name}</b><span>Ingress · ${nodeName(pb.ingress_point)}</span></div>`;
    card.addEventListener("click", () => { resetIdle(); launchPlaybook(pb.playbook_id); });
    list.appendChild(card);
  });
}
const nodeName = (id) => (state.infra.nodes.find(n => n.id === id) || {}).name || id;

/* -------------------- websocket -------------------- */
function connectWS() {
  const proto = location.protocol === "https:" ? "wss" : "ws";
  const ws = new WebSocket(`${proto}://${location.host}/ws`);
  state.ws = ws;
  const status = document.getElementById("conn-status");
  ws.onopen = () => { status.className = "ok"; status.querySelector("span").textContent = "ENGINE ONLINE"; };
  ws.onclose = () => { status.className = "down"; status.querySelector("span").textContent = "RECONNECTING…"; setTimeout(connectWS, 1500); };
  ws.onmessage = (e) => handleMessage(JSON.parse(e.data));
}
const send = (obj) => state.ws && state.ws.readyState === 1 && state.ws.send(JSON.stringify(obj));

/* -------------------- protocol -------------------- */
function handleMessage(m) {
  switch (m.type) {
    case "metrics":        updateMetrics(m.metrics); break;
    case "sim_start":      onSimStart(m); break;
    case "ingress":        onIngress(m); break;
    case "compromised":    onCompromised(m); break;
    case "spread":         onSpread(m); break;
    case "spread_blocked": logEvent("good", `Spread to ${nodeName(m.target)} blocked.`); break;
    case "mitigation":     onMitigation(m); break;
    case "patch_start":    onPatchStart(m); break;
    case "patch_complete": onPatchComplete(m); break;
    case "sim_complete":   logEvent("mit", "Playbook timeline complete."); stopClock(); break;
    case "reset":          onReset(); break;
  }
}

/* -------------------- sim lifecycle -------------------- */
function launchPlaybook(id) {
  const pb = state.playbooks.find(p => p.playbook_id === id);
  state.activePlaybook = pb;
  document.querySelectorAll(".pb-card").forEach(c => c.classList.toggle("active", c.dataset.id === id));
  showAttract(false);
  send({ action: "reset" });
  clearVisualState();
  send({ action: "start", playbook_id: id });
}

function onSimStart(m) {
  document.getElementById("scenario-title").textContent = m.playbook.name.toUpperCase();
  document.getElementById("scenario-sub").textContent = m.playbook.description;
  logEvent("mit", `Scenario armed: ${m.playbook.name}`);
  startClock();
}

function onIngress(m) {
  logEvent("ingress", `⚠ INGRESS at ${nodeName(m.node)} — ${m.method}`);
  spawnIngressMarker(m.node, m.icon, m.method);
}

function onCompromised(m) {
  setNodeStatus(m.node, "compromised");
  state.nodeVector[m.node] = m.vector;
  logEvent("compromise", `${m.initial ? "☣ INITIAL COMPROMISE" : "✖ COMPROMISED"}: ${nodeName(m.node)}${m.initial ? "" : " via " + m.vector}`);
  pulseNode(m.node);
  if (m.initial) removeIngressMarker();
}

function onSpread(m) {
  logEvent("spread", `↷ LATERAL MOVE: ${nodeName(m.source)} → ${nodeName(m.target)} (${m.vector})`);
  const link = state.linkEls[`${m.source}|${m.target}`];
  if (link) link.classList.add("hot");
  fireSpreadParticle(m.source, m.target);
}

function onMitigation(m) {
  const map = { ISOLATE: "isolated", REDUCE: "reduced", STOP: "compromised" };
  if (m.option !== "STOP") setNodeStatus(m.node, map[m.option]);
  logEvent("mit", `🛡 ${m.message}`);
  if (state.overlayNode === m.node) refreshOverlayStatus(m.node);
}

function onPatchStart(m) {
  logEvent("mit", `⏳ ${m.message}`);
  runPatchCountdown(m.node, m.seconds);
}

function onPatchComplete(m) {
  clearPatchCountdown(m.node);
  if (m.success) { setNodeStatus(m.node, "patched"); logEvent("good", `✔ Patch complete on ${nodeName(m.node)} — spread contained.`); }
  else { logEvent("compromise", `✖ Patch on ${nodeName(m.node)} finished too late — attack already spread.`); }
  if (state.overlayNode === m.node) refreshOverlayStatus(m.node);
}

function onReset() { clearVisualState(); }

/* -------------------- node visuals -------------------- */
function setNodeStatus(id, status) {
  state.nodeStatus[id] = status;
  const n = state.nodeEls[id];
  if (!n) return;
  n.g.classList.remove("compromised", "isolated", "reduced", "patched");
  if (status !== "normal") n.g.classList.add(status);
  const capMap = { normal: "100%", compromised: "70%", reduced: "40%", isolated: "0%", patched: "85%" };
  n.cap.textContent = capMap[status] || "—";
}

function pulseNode(id) {
  const n = state.nodeEls[id];
  if (!n) return;
  gsap.fromTo(n.g, { scale: 1 }, { scale: 1.18, duration: 0.22, yoyo: true, repeat: 3, transformOrigin: "center", ease: "power2.out",
    onComplete() { gsap.set(n.g, { scale: 1 }); } });
}

function fireSpreadParticle(srcId, tgtId) {
  const a = state.nodePos[srcId], b = state.nodePos[tgtId];
  if (!a || !b) return;
  const layer = document.getElementById("particle-layer");
  for (let i = 0; i < 5; i++) {
    const p = el("circle", { class: "particle r", cx: a.x, cy: a.y, r: 4.5 });
    layer.appendChild(p);
    gsap.to(p, {
      duration: 1.1, delay: i * 0.14, ease: "power1.in",
      attr: { cx: b.x, cy: b.y },
      onComplete() { p.remove(); }
    });
  }
}

/* ingress method indicator floating over the point of ingress */
function spawnIngressMarker(nodeId, icon, method) {
  removeIngressMarker();
  const pos = state.nodePos[nodeId];
  if (!pos) return;
  const layer = document.getElementById("ingress-layer");
  const g = el("g", { class: "ingress-marker", transform: `translate(${pos.x},${pos.y - 92})` });
  const halo = el("circle", { class: "halo", r: 30 });
  const use = el("use", { class: "ico", href: `#ing-${icon}`, x: -20, y: -20, width: 40, height: 40 });
  const lbl = el("text", { class: "ingress-label", y: 50 });
  lbl.textContent = "METHOD OF INGRESS";
  g.append(halo, use, lbl);
  layer.appendChild(g);
  state.ingressMarker = g;
  gsap.fromTo(g, { opacity: 0, y: pos.y - 60 }, { opacity: 1, duration: 0.5 });
  gsap.to(halo, { attr: { r: 36 }, opacity: 0.4, duration: 0.9, repeat: -1, yoyo: true, ease: "sine.inOut" });
  gsap.to(g, { y: "-=10", duration: 1.3, repeat: -1, yoyo: true, ease: "sine.inOut" });
}
function removeIngressMarker() {
  const marker = state.ingressMarker;
  if (marker) {
    state.ingressMarker = null;
    gsap.killTweensOf(marker);
    gsap.to(marker, { opacity: 0, duration: 0.3, onComplete: () => marker.remove() });
  }
}

function clearVisualState() {
  Object.keys(state.nodeStatus).forEach(id => setNodeStatus(id, "normal"));
  Object.values(state.linkEls).forEach(l => l.classList.remove("hot"));
  document.querySelectorAll(".patch-ring").forEach(e => e.remove());
  removeIngressMarker();
  document.getElementById("scenario-title").textContent = "SELECT A PLAYBOOK TO BEGIN";
  document.getElementById("scenario-sub").textContent = "Interactive airport infrastructure — hover or tap a node";
  document.querySelectorAll(".pb-card").forEach(c => c.classList.remove("active"));
  stopClock();
  document.getElementById("sim-clock").textContent = "T+00.0s";
}

/* -------------------- patch countdown ring -------------------- */
function runPatchCountdown(id, seconds) {
  const n = state.nodeEls[id];
  if (!n) return;
  const ring = el("circle", { class: "patch-ring", r: 52, cx: 0, cy: 0, fill: "none",
    stroke: "var(--cyan)", "stroke-width": 4, "stroke-dasharray": 327, "stroke-dashoffset": 0,
    transform: "rotate(-90)", filter: "url(#glow-green)" });
  n.g.appendChild(ring);
  state.patchTimers[id] = gsap.to(ring, { attr: { "stroke-dashoffset": 327 }, duration: seconds, ease: "none" });
}
function clearPatchCountdown(id) {
  if (state.patchTimers[id]) { state.patchTimers[id].kill(); delete state.patchTimers[id]; }
  const n = state.nodeEls[id];
  if (n) n.g.querySelectorAll(".patch-ring").forEach(e => e.remove());
}

/* -------------------- metrics -------------------- */
function updateMetrics(m) {
  document.getElementById("m-security").textContent = m.security + "%";
  document.getElementById("m-capacity").textContent = m.capacity + "%";
  document.getElementById("m-financial").textContent = fmtMoney(m.financial);
  document.getElementById("bar-security").style.width = m.security + "%";
  document.getElementById("bar-capacity").style.width = m.capacity + "%";
  document.getElementById("bar-financial").style.width = Math.min(100, m.financial / 5e6 * 100) + "%";
}

/* -------------------- event log -------------------- */
function logEvent(kind, text) {
  const log = document.getElementById("event-log");
  const item = document.createElement("div");
  item.className = "log-item " + kind;
  const t = state.simStart ? ((performance.now() - state.simStart) / 1000).toFixed(1) : "0.0";
  item.innerHTML = `<span class="t">T+${t}</span>${text}`;
  log.prepend(item);
  while (log.children.length > 40) log.lastChild.remove();
}

/* -------------------- clock -------------------- */
function startClock() {
  state.simStart = performance.now();
  stopClock();
  state.clockTimer = setInterval(() => {
    const s = ((performance.now() - state.simStart) / 1000).toFixed(1);
    document.getElementById("sim-clock").textContent = `T+${s.padStart(4, "0")}s`;
  }, 100);
}
function stopClock() { if (state.clockTimer) { clearInterval(state.clockTimer); state.clockTimer = null; } }

/* -------------------- node overlay -------------------- */
function openNode(id) {
  const n = state.infra.nodes.find(x => x.id === id);
  if (!n) return;
  state.overlayNode = id;
  document.getElementById("ov-icon").setAttribute("href", `#node-${n.icon}`);
  document.getElementById("ov-name").textContent = n.name;
  document.getElementById("ov-service").textContent = n.type.toUpperCase();
  document.getElementById("ov-desc").textContent = n.description;
  refreshOverlayStatus(id);

  // regulatory tab
  const c = (state.compliance.mappings || {})[id];
  const objs = state.compliance.frameworks.caf.objectives;
  if (c) {
    document.getElementById("reg-obj").textContent = `CAF Objective ${c.caf_objective} — ${objs[c.caf_objective]}`;
    document.getElementById("reg-principle").textContent = c.caf_principle;
    document.getElementById("reg-duty").textContent = c.csr_duty;
    document.getElementById("reg-insight").textContent = c.insight;
  } else {
    document.getElementById("reg-obj").textContent = "No mapping";
    document.getElementById("reg-principle").textContent = "—";
    document.getElementById("reg-duty").textContent = "—";
    document.getElementById("reg-insight").textContent = "—";
  }
  switchTab("details");
  document.getElementById("node-overlay").classList.remove("hidden");
}

function refreshOverlayStatus(id) {
  const status = state.nodeStatus[id];
  const capMap = { normal: "100%", compromised: "70%", reduced: "40%", isolated: "0%", patched: "85%" };
  const badge = document.getElementById("ov-status");
  const labelMap = { normal: "NORMAL", compromised: "COMPROMISED", isolated: "ISOLATED", reduced: "TRAFFIC REDUCED", patched: "PATCHED" };
  badge.textContent = labelMap[status];
  badge.className = "overlay-status " + (status === "compromised" ? "bad" : status === "normal" || status === "patched" ? "" : "warn");
  document.getElementById("ov-capacity").textContent = capMap[status];
  document.getElementById("ov-vector").textContent = state.nodeVector[id] || (status === "compromised" ? "Under attack" : "None");

  // mitigation only meaningful on an active threat
  const actionable = status === "compromised";
  document.querySelectorAll(".mit-btn").forEach(b => b.disabled = !actionable);
}

function switchTab(name) {
  document.querySelectorAll(".tab").forEach(t => t.classList.toggle("active", t.dataset.tab === name));
  document.querySelectorAll(".tab-body").forEach(b => b.classList.toggle("hidden", b.dataset.body !== name));
}

/* -------------------- UI wiring -------------------- */
function wireUI() {
  document.getElementById("reset-btn").addEventListener("click", () => {
    resetIdle(); send({ action: "reset" }); clearVisualState();
    document.getElementById("event-log").innerHTML = "";
    updateMetrics({ security: 100, capacity: 100, financial: 0 });
  });

  document.querySelectorAll("[data-close]").forEach(b => b.addEventListener("click", closeOverlay));
  document.getElementById("node-overlay").addEventListener("click", (e) => { if (e.target.id === "node-overlay") closeOverlay(); });
  document.querySelectorAll(".tab").forEach(t => t.addEventListener("click", () => switchTab(t.dataset.tab)));

  document.querySelectorAll(".mit-btn").forEach(b => b.addEventListener("click", () => {
    if (b.disabled) return;
    send({ action: "mitigate", node: state.overlayNode, option: b.dataset.mit });
    if (b.dataset.mit !== "STOP") closeOverlay();
    resetIdle();
  }));

  document.getElementById("attract-start").addEventListener("click", () => { showAttract(false); resetIdle(); });

  ["pointerdown", "keydown", "mousemove"].forEach(ev =>
    window.addEventListener(ev, resetIdle, { passive: true }));
}
function closeOverlay() { document.getElementById("node-overlay").classList.add("hidden"); state.overlayNode = null; }

/* -------------------- attract loop / idle -------------------- */
function showAttract(show) { document.getElementById("attract").classList.toggle("hidden", !show); }
function resetIdle() {
  if (state.idleTimer) clearTimeout(state.idleTimer);
  state.idleTimer = setTimeout(() => {
    send({ action: "reset" });
    clearVisualState();
    document.getElementById("event-log").innerHTML = "";
    updateMetrics({ security: 100, capacity: 100, financial: 0 });
    closeOverlay();
    showAttract(true);
  }, IDLE_MS);
}

boot();
