// ── Symframe Substrate ─────────────────────────────────────────────────
// Stage B of the Dynamic Glass Panel arc — see ~/Documents/dynamic-glass-panel.md.
//
// The substrate is the ambient reasoning layer that lives BEHIND the
// foreground cards in the symframe. It's the single move that converts
// the panel from "card list with a tagline" into a felt "window into
// the agent's mind" — there is always something alive in the negative
// space, even when no specific card is being pushed.
//
// What this module owns:
//   - The substrate container (#symframe-substrate)
//   - The live trace (the in-flight thinking entry, pinned at top with a
//     pulsing dot — full opacity, no blur)
//   - Recent reasoning entries (every block routed through the old
//     reasoning-feed window globals — `appendToReasoningPanel`,
//     `injectReasoningRunSeparator`, `openLiveThinking`,
//     `updateLiveThinking`, `closeLiveThinking`, `clearReasoningPanel`)
//   - Per-entry decay tick: opacity / blur / scale / height collapse as
//     a function of (now - entry.createdAt). See DECAY_TIERS below.
//   - Quiet-mind empty state when the substrate has no entries
//
// Backward-compatible surface:
//   This module DEFINES the same six window globals that render.js used
//   to expose for the reasoning panel. Script load order in index.html
//   loads render.js first then this file, so our definitions override.
//   app.js (~12 call sites) is unchanged.
//
// Layout:
//   The substrate is one of two children of .symframe-stack (the other
//   is .symframe-cards). Both share a single CSS grid cell via
//   grid-area: overlay, with the substrate at z-index 0 and cards at
//   z-index 1. Cards have backdrop-filter so the substrate is visible
//   THROUGH them — the agent's mind breathes behind whatever artifacts
//   are foregrounded.

"use strict";

(function () {
  const substrate = document.getElementById("symframe-substrate");
  if (!substrate) {
    // Substrate not in DOM — nothing to do. The 6 window globals stay
    // bound to render.js's no-op-on-missing-DOM implementations.
    return;
  }

  // ── Decay tiers ─────────────────────────────────────────────────────
  // Each entry carries dataset.createdAt; the tick reads its age and
  // looks up the matching tier. Tiers are evaluated in order; first
  // match (age < maxAge) wins.
  //
  // Stage C.1 revision: the original aggressive curve faded entries to
  // 8% opacity + 6px blur (effectively invisible) after 10 minutes.
  // Users perceived the substrate as "fading to nothing." The new
  // curve keeps content readable at every age — older entries dim and
  // shrink slightly but stay legible. No texture-mode collapse.
  const DECAY_TIERS = [
    { maxAge: 60_000, opacity: 1.0, blur: 0, scale: 1.0, texture: false }, // 0-1min sharp
    { maxAge: 300_000, opacity: 0.9, blur: 0, scale: 1.0, texture: false }, // 1-5min slight dim
    { maxAge: 1_800_000, opacity: 0.7, blur: 1, scale: 1.0, texture: false }, // 5-30min readable
    { maxAge: Infinity, opacity: 0.5, blur: 2, scale: 0.98, texture: false }, // 30min+ floor
  ];

  const DECAY_TICK_MS = 1000;

  // ── Live trace state ────────────────────────────────────────────────
  // The "live" entry is the in-flight thinking stream. It's pinned at
  // the top of the substrate, exempt from decay, and has a pulsing dot.
  // When closeLiveThinking fires, the live class is dropped and the
  // entry starts decaying like any other.
  let liveEntry = null;
  let liveBody = null;
  let liveBuffer = "";

  // Decay tick handle. Pauses when page is hidden (Page Visibility API)
  // so a backgrounded tab doesn't burn CPU on substrate-only animation.
  let decayHandle = null;

  // ── DOM helpers ─────────────────────────────────────────────────────
  function escHtml(s) {
    return String(s ?? "")
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;")
      .replace(/'/g, "&#39;");
  }

  function ensureEmptyRemoved() {
    const empty = substrate.querySelector(".substrate-empty");
    if (empty) {
      empty.remove();
    }
  }

  function renderQuietMind() {
    if (substrate.querySelector(".substrate-empty")) {
      return;
    }
    const empty = document.createElement("div");
    empty.className = "substrate-empty";
    empty.innerHTML = `
      <span class="substrate-quiet-dot" aria-hidden="true"></span>
      <span class="substrate-quiet-label">quiet mind</span>
    `;
    substrate.appendChild(empty);
  }

  function insertAtTopRespectingLive(entry) {
    // Live entry stays at index 0; everything else prepends after it.
    if (liveEntry && substrate.firstChild === liveEntry) {
      liveEntry.after(entry);
    } else {
      substrate.prepend(entry);
    }
  }

  // ── Decay ────────────────────────────────────────────────────────────
  function applyDecay(el, age) {
    let tier = DECAY_TIERS[DECAY_TIERS.length - 1];
    for (const t of DECAY_TIERS) {
      if (age < t.maxAge) {
        tier = t;
        break;
      }
    }
    el.style.setProperty("--substrate-opacity", String(tier.opacity));
    el.style.setProperty("--substrate-blur", `${tier.blur}px`);
    el.style.setProperty("--substrate-scale", String(tier.scale));
    el.classList.toggle("substrate-texture", tier.texture);
  }

  function tickDecay() {
    const now = Date.now();
    const entries = substrate.querySelectorAll(".substrate-entry:not(.substrate-live)");
    for (const e of entries) {
      const createdAt = Number(e.dataset.createdAt) || now;
      applyDecay(e, now - createdAt);
    }
  }

  function startDecayTick() {
    if (decayHandle) {
      return;
    }
    decayHandle = setInterval(tickDecay, DECAY_TICK_MS);
  }

  function stopDecayTick() {
    if (decayHandle) {
      clearInterval(decayHandle);
      decayHandle = null;
    }
  }

  document.addEventListener("visibilitychange", () => {
    if (document.hidden) {
      stopDecayTick();
    } else if (substrate.querySelector(".substrate-entry:not(.substrate-live)")) {
      startDecayTick();
      tickDecay(); // immediate catch-up after a long pause
    }
  });

  // ── Entry builders ──────────────────────────────────────────────────
  // Returns an HTMLElement or null (null = skip, e.g. empty block).
  // Class names mirror the existing reasoning-* set so the badge/body
  // styles from style.css continue to apply unchanged.
  function buildEntryForBlock(block) {
    const entry = document.createElement("div");
    entry.className = "substrate-entry reasoning-entry";
    entry.dataset.createdAt = String(Date.now());

    switch (block.type) {
      case "text": {
        const text = (block.text ?? "").trim();
        if (!text) {
          return null;
        }
        entry.innerHTML = `
          <div class="reasoning-entry-header">
            <span class="reasoning-entry-badge badge-narration">✦ NOTE</span>
            <span class="reasoning-entry-name">${escHtml(text.split("\n")[0].slice(0, 60))}${text.length > 60 ? "…" : ""}</span>
          </div>
        `;
        return entry;
      }
      case "thinking": {
        const text = (block.thinking ?? "").trim();
        if (!text) {
          return null;
        }
        entry.innerHTML = `
          <div class="reasoning-entry-header">
            <span class="reasoning-entry-badge badge-think">◈ THINK</span>
            <span class="reasoning-entry-name">${escHtml(text.split("\n")[0].slice(0, 60))}${text.length > 60 ? "…" : ""}</span>
          </div>
          <div class="reasoning-think-body">${escHtml(text)}</div>
        `;
        return entry;
      }
      case "tool_use":
      case "toolCall": {
        const name = block.name ?? "tool";
        const inputData = block.input ?? block.arguments ?? {};
        const inputJson = inputData ? JSON.stringify(inputData, null, 2) : "{}";
        let preview = "";
        try {
          const obj = typeof inputData === "string" ? JSON.parse(inputData) : inputData;
          const keys = Object.keys(obj ?? {});
          if (keys.length) {
            const val = String(obj[keys[0]]).slice(0, 50);
            preview = `${keys[0]}: ${val}${val.length >= 50 ? "…" : ""}`;
          }
        } catch {
          /* ignore */
        }
        entry.innerHTML = `
          <div class="reasoning-entry-header">
            <span class="reasoning-entry-badge badge-tool">⚙ TOOL</span>
            <span class="reasoning-entry-name">${escHtml(name)}</span>
          </div>
          ${preview ? `<div class="reasoning-tool-preview">${escHtml(preview)}</div>` : ""}
          <div class="reasoning-tool-detail">${escHtml(inputJson)}</div>
        `;
        entry.style.cursor = "pointer";
        entry.addEventListener("click", () => entry.classList.toggle("expanded"));
        return entry;
      }
      case "tool_result": {
        const content = Array.isArray(block.content)
          ? block.content.map((b) => b.text ?? "").join("\n")
          : String(block.content ?? "");
        const chars = content.length;
        const isError = block.is_error === true;
        entry.innerHTML = `
          <div class="reasoning-entry-header">
            <span class="reasoning-entry-badge badge-result">${isError ? "⚠ ERROR" : "↩ RESULT"}</span>
            <span class="reasoning-entry-name">${chars} chars</span>
          </div>
          <div class="reasoning-result-meta">${escHtml(content.slice(0, 80))}${chars > 80 ? "…" : ""}</div>
        `;
        return entry;
      }
      default:
        return null;
    }
  }

  // ── Window globals (preserve render.js signatures) ──────────────────

  /**
   * Append a finished block to the substrate. Block shapes match what
   * app.js produces from agent events: { type:"text"|"thinking"|
   * "tool_use"|"toolCall"|"tool_result", ... }.
   */
  window.appendToReasoningPanel = function (block) {
    ensureEmptyRemoved();
    const entry = buildEntryForBlock(block);
    if (!entry) {
      return;
    }
    insertAtTopRespectingLive(entry);
    startDecayTick();
  };

  /**
   * Inject a horizontal-rule + timestamp separator marking a new agent
   * run. Called from app.js on lifecycle:start.
   */
  window.injectReasoningRunSeparator = function () {
    ensureEmptyRemoved();
    const sep = document.createElement("div");
    sep.className = "substrate-entry substrate-run-sep reasoning-run-sep";
    sep.dataset.createdAt = String(Date.now());
    const now = new Date();
    const hhmm = now.toLocaleTimeString("en-US", {
      hour: "2-digit",
      minute: "2-digit",
      hour12: false,
    });
    sep.innerHTML = `<span class="reasoning-run-sep-label">RUN · ${hhmm}</span>`;
    insertAtTopRespectingLive(sep);
    startDecayTick();
  };

  /**
   * Clear all substrate entries and restore the quiet-mind empty state.
   * Called from app.js on session reset.
   */
  window.clearReasoningPanel = function () {
    substrate.innerHTML = "";
    liveEntry = null;
    liveBody = null;
    liveBuffer = "";
    stopDecayTick();
    renderQuietMind();
  };

  /**
   * Open the live thinking entry — pinned at top, exempt from decay,
   * with a pulsing dot. Called when streaming starts.
   */
  window.openLiveThinking = function () {
    if (liveEntry) {
      return; // already open
    }
    ensureEmptyRemoved();
    liveBuffer = "";
    const entry = document.createElement("div");
    entry.className = "substrate-entry substrate-live reasoning-entry";
    entry.id = "substrate-live-entry";
    entry.innerHTML = `
      <div class="reasoning-entry-header">
        <span class="substrate-live-dot" aria-hidden="true"></span>
        <span class="reasoning-entry-badge badge-think">◈ THINKING</span>
        <span class="reasoning-entry-name" id="substrate-live-name">…</span>
      </div>
      <div class="reasoning-think-body" id="substrate-live-body"></div>
    `;
    substrate.prepend(entry);
    liveEntry = entry;
    liveBody = entry.querySelector("#substrate-live-body");
  };

  /**
   * Append text to the live thinking entry's body. Idempotent on open —
   * calls openLiveThinking if no live entry exists yet.
   */
  window.updateLiveThinking = function (text) {
    if (!liveEntry) {
      window.openLiveThinking();
    }
    if (typeof text !== "string" || !text) {
      return;
    }
    liveBuffer += text;
    if (liveBody) {
      liveBody.textContent = liveBuffer;
    }
    // Update name preview to the first line
    const nameEl = liveEntry?.querySelector("#substrate-live-name");
    if (nameEl) {
      const firstLine = liveBuffer.split("\n")[0].slice(0, 60);
      nameEl.textContent = firstLine + (liveBuffer.length > 60 ? "…" : "");
    }
  };

  /**
   * Close the live thinking entry: drop the live class so it starts
   * decaying like any other entry, and stamp its createdAt to "now" so
   * the decay timer starts from when it finished, not when it opened.
   */
  window.closeLiveThinking = function () {
    if (!liveEntry) {
      return;
    }
    liveEntry.classList.remove("substrate-live");
    liveEntry.removeAttribute("id");
    liveEntry.dataset.createdAt = String(Date.now());
    const nameEl = liveEntry.querySelector("#substrate-live-name");
    if (nameEl) {
      nameEl.removeAttribute("id");
    }
    const bodyEl = liveEntry.querySelector("#substrate-live-body");
    if (bodyEl) {
      bodyEl.removeAttribute("id");
    }
    liveEntry = null;
    liveBody = null;
    liveBuffer = "";
    startDecayTick();
  };

  // Extract thinking text from a content array — used by streaming code
  // in app.js to extract just the thinking portion from a content
  // array (Anthropic-style multi-block messages). Recursive on
  // content.content to handle wrapper-shape messages.
  window.extractThinkingText = function (content) {
    if (!content) {
      return "";
    }
    if (typeof content === "string") {
      return "";
    }
    if (Array.isArray(content)) {
      return content
        .filter((b) => b?.type === "thinking")
        .map((b) => b.thinking ?? "")
        .join("");
    }
    if (content?.content) {
      return window.extractThinkingText(content.content);
    }
    return "";
  };

  // ── Initial state ───────────────────────────────────────────────────
  renderQuietMind();
})();
