// ── Symi UI — Message Renderer ────────────────────────────────────────
// Handles all content types: text (markdown), thinking, tool_use,
// tool_result, image. Mirrors the Standard UI feature set.

// ── Configure marked ──────────────────────────────────────────────────
if (typeof marked !== "undefined") {
  marked.setOptions({
    breaks: true,
    gfm: true,
    pedantic: false,
  });

  // GFM task list extension (- [ ] and - [x])
  const _taskListExt = {
    name: "taskList",
    level: "inline",
    start(src) {
      return src.indexOf("[ ]") !== -1 || src.indexOf("[x]") !== -1 ? 0 : -1;
    },
  };
  marked.use({
    hooks: {
      postprocess(html) {
        return html
          .replace(
            /<li>\s*\[ \]/g,
            '<li class="task-item"><input type="checkbox" disabled class="task-cb"> ',
          )
          .replace(
            /<li>\s*\[x\]/gi,
            '<li class="task-item task-done"><input type="checkbox" disabled checked class="task-cb"> ',
          );
      },
    },
  });

  // Custom renderer — open links in new tab, add copy buttons to code blocks
  const renderer = new marked.Renderer();

  // marked v15+: renderer methods receive a token object
  renderer.link = (token) => {
    const href = typeof token === "string" ? token : (token.href ?? "");
    const title = typeof token === "object" ? (token.title ?? "") : "";
    const text = typeof token === "object" ? (token.text ?? "") : "";
    const t = title ? ` title="${escAttr(title)}"` : "";
    return `<a href="${escAttr(href)}"${t} target="_blank" rel="noopener">${text}</a>`;
  };

  renderer.code = (token) => {
    const raw = typeof token === "string" ? token : (token.text ?? "");
    const meta = typeof token === "object" ? (token.lang ?? "") : "";

    // Split "python filename.py" -> lang + optional filename
    const parts = meta.trim().split(/\s+/);
    const lang = parts[0] || "";
    const filename = parts.slice(1).join(" ");
    const language = lang && hljs && hljs.getLanguage(lang) ? lang : "plaintext";

    let highlighted = escHtml(raw);
    try {
      if (typeof hljs !== "undefined") {
        highlighted = hljs.highlight(raw, { language }).value;
      }
    } catch {
      /* ignore */
    }

    // Build line-numbered HTML
    const lines = highlighted.split("\n");
    if (lines[lines.length - 1] === "") {
      lines.pop();
    }
    const numbered = lines
      .map((line, i) => {
        return (
          '<span class="code-line"><span class="line-num">' +
          (i + 1) +
          '</span><span class="line-content">' +
          (line || "\u200b") +
          "</span></span>"
        );
      })
      .join("\n");

    const langIcon = codeIcon(lang);
    const langColor = codeLangColor(lang);
    const id = "code-" + Math.random().toString(36).slice(2);
    const collapsible = lines.length > 30;
    const linesLabel = lines.length + " line" + (lines.length !== 1 ? "s" : "");

    let html =
      '<div class="code-block code-block-full' +
      (collapsible ? " code-collapsible" : "") +
      '" id="' +
      id +
      '">';
    html += '<div class="code-header">';
    html += '<div class="code-header-left">';
    html +=
      '<span class="code-lang-badge" style="--lang-color:' +
      langColor +
      '">' +
      langIcon +
      escHtml(lang || "code") +
      "</span>";
    if (filename) {
      html += '<span class="code-filename">' + escHtml(filename) + "</span>";
    }
    html += '<span class="code-lines-count">' + linesLabel + "</span>";
    html += "</div>";
    html += '<div class="code-header-right">';
    if (collapsible) {
      html +=
        '<button class="code-expand-btn" onclick="toggleCodeExpand(\'' +
        id +
        '\')" title="Expand">⤢</button>';
    }
    html += '<button class="code-copy" onclick="copyCode(this)" title="Copy code">';
    html +=
      '<svg width="13" height="13" viewBox="0 0 24 24" fill="none"><rect x="9" y="9" width="13" height="13" rx="2" stroke="currentColor" stroke-width="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="2"/></svg> Copy';
    html += "</button></div></div>";
    html += '<div class="code-body' + (collapsible ? " code-body-collapsed" : "") + '">';
    html +=
      '<pre class="code-pre"><code class="hljs language-' +
      escHtml(language) +
      ' code-numbered">' +
      numbered +
      "</code></pre>";
    html += "</div>";
    if (collapsible) {
      html +=
        '<button class="code-show-more" onclick="toggleCodeExpand(\'' +
        id +
        "')\">Show all " +
        lines.length +
        " lines ▾</button>";
    }
    html += "</div>";
    return html;
  };

  renderer.codespan = (token) => {
    const text = typeof token === "string" ? token : (token.text ?? "");
    return `<code class="inline-code">${text}</code>`;
  };

  marked.use({ renderer });
}

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

// ── Language icon + color ─────────────────────────────────────────────
function codeIcon(lang = "") {
  const l = lang.toLowerCase();
  if (["js", "javascript", "ts", "typescript", "jsx", "tsx"].includes(l)) {
    return "⬡ ";
  }
  if (["py", "python"].includes(l)) {
    return "🐍 ";
  }
  if (["bash", "sh", "zsh", "shell"].includes(l)) {
    return "$ ";
  }
  if (["json"].includes(l)) {
    return "{ ";
  }
  if (["css", "scss", "sass"].includes(l)) {
    return "🎨 ";
  }
  if (["html", "xml", "svg"].includes(l)) {
    return "⟨⟩ ";
  }
  if (["sql"].includes(l)) {
    return "⛁ ";
  }
  if (["go"].includes(l)) {
    return "◉ ";
  }
  if (["rust"].includes(l)) {
    return "⚙ ";
  }
  if (["java", "kotlin"].includes(l)) {
    return "☕ ";
  }
  if (["c", "cpp", "c++"].includes(l)) {
    return "⚡ ";
  }
  if (["yaml", "yml", "toml"].includes(l)) {
    return "⚙ ";
  }
  if (["dockerfile", "docker"].includes(l)) {
    return "🐳 ";
  }
  return "◇ ";
}

function codeLangColor(lang = "") {
  const l = lang.toLowerCase();
  if (["js", "javascript", "jsx"].includes(l)) {
    return "#f7df1e";
  }
  if (["ts", "typescript", "tsx"].includes(l)) {
    return "#3178c6";
  }
  if (["py", "python"].includes(l)) {
    return "#3572a5";
  }
  if (["bash", "sh", "zsh", "shell"].includes(l)) {
    return "#89e051";
  }
  if (["json"].includes(l)) {
    return "#cbcb41";
  }
  if (["css", "scss"].includes(l)) {
    return "#563d7c";
  }
  if (["html", "xml"].includes(l)) {
    return "#e34c26";
  }
  if (["sql"].includes(l)) {
    return "#e38c00";
  }
  if (["go"].includes(l)) {
    return "#00add8";
  }
  if (["rust"].includes(l)) {
    return "#dea584";
  }
  if (["java"].includes(l)) {
    return "#b07219";
  }
  if (["c", "cpp"].includes(l)) {
    return "#555555";
  }
  if (["yaml", "yml"].includes(l)) {
    return "#cb171e";
  }
  if (["dockerfile", "docker"].includes(l)) {
    return "#0db7ed";
  }
  return "#aaaaaa";
}

// ── Toggle code block expand ──────────────────────────────────────────
window.toggleCodeExpand = (id) => {
  const block = document.getElementById(id);
  if (!block) {
    return;
  }
  const body = block.querySelector(".code-body");
  const showBtn = block.querySelector(".code-show-more");
  const expBtn = block.querySelector(".code-expand-btn");
  const isOpen = block.classList.toggle("code-expanded");
  if (body) {
    body.classList.toggle("code-body-collapsed", !isOpen);
  }
  if (showBtn) {
    showBtn.textContent = isOpen
      ? "Collapse ▴"
      : `Show all ${body?.querySelectorAll(".code-line").length || "?"} lines ▾`;
  }
  if (expBtn) {
    expBtn.textContent = isOpen ? "⤡" : "⤢";
  }
};

window.copyCode = async (btn) => {
  const code = btn.closest(".code-block")?.querySelector("code")?.innerText ?? "";
  try {
    await navigator.clipboard.writeText(code);
    btn.classList.add("copied");
    btn.textContent = "Copied!";
    setTimeout(() => {
      btn.classList.remove("copied");
      btn.innerHTML = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none"><rect x="9" y="9" width="13" height="13" rx="2" stroke="currentColor" stroke-width="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="2"/></svg> Copy`;
    }, 2000);
  } catch {}
};

// ── Model token stripping (profile-driven) ───────────────────────────
// Strips whatever patterns the active profile declares in
// profile.filters.stripPatterns.  There are no hardcoded Gemma patterns
// here — relying on the profile means Qwen/Nemotron/Claude (which declare
// empty stripPatterns) pass through untouched, and the Ollama/Gemma
// profiles apply their own patterns.  Server-side stripping in
// output-normalizer.ts remains the primary defense; this is best-effort
// for history messages that predate the active profile.
function stripModelTokensClient(text) {
  const profile = window.activeModelProfile;
  const patterns = profile?.filters?.stripPatterns;
  if (!patterns?.length) {
    return text;
  }
  let cleaned = text;
  for (const pat of patterns) {
    try {
      cleaned = cleaned.replace(new RegExp(pat, "gi"), "");
    } catch {
      /* skip invalid patterns */
    }
  }
  return cleaned;
}

// NOTE: Monologue suppression has moved to the OutputNormalizer (server-side).
// The model stream is cleaned before events reach the Glass UI, so client-side
// monologue detection is no longer needed.

// ── Markdown renderer ─────────────────────────────────────────────────
function renderMarkdown(text) {
  if (!text) {
    return "";
  }
  try {
    return typeof marked !== "undefined"
      ? marked.parse(text)
      : escHtml(text).replace(/\n/g, "<br>");
  } catch {
    return escHtml(text).replace(/\n/g, "<br>");
  }
}

// ── Extract plain text from content blocks ────────────────────────────
window.extractText = function (content) {
  if (!content) {
    return "";
  }
  if (typeof content === "string") {
    return content;
  }
  if (Array.isArray(content)) {
    return content
      .filter((b) => b?.type === "text")
      .map((b) => b.text ?? "")
      .join("");
  }
  if (content?.content) {
    return window.extractText(content.content);
  }
  return "";
};

// ── Tool name formatter ───────────────────────────────────────────────
function formatToolName(name) {
  if (!name) {
    return "Tool";
  }
  return name.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
}

function toolIcon(name = "") {
  if (name.includes("read") || name.includes("file")) {
    return "📄";
  }
  if (name.includes("write") || name.includes("edit")) {
    return "✏️";
  }
  if (name.includes("exec") || name.includes("run")) {
    return "⚡";
  }
  if (name.includes("search") || name.includes("web")) {
    return "🔍";
  }
  if (name.includes("browser")) {
    return "🌐";
  }
  if (name.includes("memory")) {
    return "🧠";
  }
  if (name.includes("spawn") || name.includes("agent")) {
    return "🤖";
  }
  return "🔧";
}

// ── Render a single content block ─────────────────────────────────────
function renderBlock(block) {
  const el = document.createElement("div");

  switch (block.type) {
    // ── Plain text with markdown ──────────────────────────────────────
    case "text": {
      el.className = "msg-text";
      const rawText = block.text ?? "";
      const stripped = stripModelTokensClient(rawText);
      // If stripping ate everything but we started with real text, render
      // the raw text rather than silently dropping the message.  Prevents
      // the "response appeared then disappeared" regression.
      const safeText = stripped.trim() ? stripped : rawText;
      if (!safeText) {
        break;
      }
      el.innerHTML = renderMarkdown(safeText);
      break;
    }

    // ── Thinking block (collapsible) ──────────────────────────────────
    case "thinking": {
      el.className = "msg-thinking";
      const id = "think-" + Math.random().toString(36).slice(2);
      el.innerHTML = `
        <button class="thinking-toggle" onclick="toggleThinking('${id}')">
          <span class="thinking-icon">◈</span>
          <span class="thinking-label">Reasoning</span>
          <span class="thinking-chevron">▾</span>
        </button>
        <div class="thinking-body" id="${id}">
          <div class="thinking-text">${escHtml(block.thinking ?? "")}</div>
        </div>
      `;
      break;
    }

    // ── Tool use (what was called) ────────────────────────────────────
    case "tool_use": {
      el.className = "msg-tool-card";
      const name = block.name ?? "tool";
      const input = block.input ? JSON.stringify(block.input, null, 2) : "";
      const id = "tool-" + Math.random().toString(36).slice(2);
      el.innerHTML = `
        <button class="tool-header" onclick="toggleTool('${id}')">
          <span class="tool-icon">${toolIcon(name)}</span>
          <span class="tool-name">${escHtml(formatToolName(name))}</span>
          <span class="tool-chevron">▾</span>
        </button>
        <div class="tool-body" id="${id}">
          <pre class="tool-input">${escHtml(input)}</pre>
        </div>
      `;
      break;
    }

    // ── Tool result ───────────────────────────────────────────────────
    case "tool_result": {
      el.className = "msg-tool-result";
      const content = Array.isArray(block.content)
        ? block.content.map((b) => b.text ?? "").join("\n")
        : String(block.content ?? "");
      const _preview = content.slice(0, 120);
      const more = content.length > 120;
      const id = "result-" + Math.random().toString(36).slice(2);
      el.innerHTML = `
        <div class="result-header">
          <span class="result-icon">↩</span>
          <span class="result-label">Result</span>
          ${more ? `<button class="result-expand" onclick="toggleTool('${id}')">Expand</button>` : ""}
        </div>
        <div class="result-preview" id="${id}">
          <pre class="result-text">${escHtml(content)}</pre>
        </div>
      `;
      if (!more) {
        el.querySelector(".result-preview")?.classList.add("result-open");
      }
      break;
    }

    // ── Image ─────────────────────────────────────────────────────────
    case "image": {
      el.className = "msg-image";
      const src =
        block.source?.url ??
        (block.source?.type === "base64"
          ? `data:${block.source.media_type};base64,${block.source.data}`
          : "");
      if (src) {
        el.innerHTML = `<img src="${escAttr(src)}" alt="image" />`;
      }
      break;
    }

    default:
      break;
  }

  return el;
}

// ── Toggle helpers ────────────────────────────────────────────────────
window.toggleThinking = (id) => {
  const body = document.getElementById(id);
  const btn = body?.previousElementSibling;
  if (!body) {
    return;
  }
  const open = body.classList.toggle("thinking-open");
  if (btn) {
    btn.querySelector(".thinking-chevron").textContent = open ? "▴" : "▾";
  }
};

window.toggleTool = (id) => {
  const body = document.getElementById(id);
  if (!body) {
    return;
  }
  body.classList.toggle("tool-open");
  const btn = body?.previousElementSibling?.querySelector?.(".tool-chevron");
  if (btn) {
    btn.textContent = body.classList.contains("tool-open") ? "▴" : "▾";
  }
};

// ── Copy message text ─────────────────────────────────────────────────
window.copyMessage = async (btn) => {
  const bubble = btn.closest(".stream-bubble, .bubble");
  const text = bubble?.querySelector(".msg-text")?.innerText ?? bubble?.innerText ?? "";
  try {
    await navigator.clipboard.writeText(text.trim());
    btn.classList.add("copied");
    setTimeout(() => btn.classList.remove("copied"), 2000);
  } catch {}
};

// ── Timestamp formatter ───────────────────────────────────────────────
function formatTime(ts) {
  if (!ts) {
    return "";
  }
  const d = new Date(typeof ts === "number" && ts < 1e12 ? ts * 1000 : ts);
  return d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" });
}

// Expose renderBlock for use in app.js streaming finalization
window.renderBlock = renderBlock;

// ── Reasoning helpers moved to glass-ui/js/symframe-substrate.js ──────
// Stage B of the Dynamic Glass Panel arc (3.5.20): the six window
// globals previously defined here — appendToReasoningPanel,
// injectReasoningRunSeparator, clearReasoningPanel, openLiveThinking,
// updateLiveThinking, closeLiveThinking, extractThinkingText — now
// live in symframe-substrate.js, which routes them to the ambient
// substrate behind the foreground cards instead of the now-removed
// reasoning panel card. app.js call sites are unchanged; they bind to
// the same names. See ~/Documents/dynamic-glass-panel.md §5 Stage B.
//
// Original implementations preserved in git history at b80e4fa4e^.

// ── Main render function — builds a full message element ──────────────
window.renderMessage = function (message) {
  const { role, content, timestamp } = message;
  const isUser = role === "user";

  // ── Filter noisy/internal messages ────────────────────────────────
  const _msgText = Array.isArray(content)
    ? content
        .filter((b) => b.type === "text")
        .map((b) => b.text ?? "")
        .join("")
        .trim()
    : extractText(content).trim();

  // NOTE: Monologue suppression moved server-side to OutputNormalizer.

  // Plugin context messages (e.g. "[Outlook 365] Not connected...")
  if (role === "user" && /^\[.+\]\s+(Not connected|Connected as)\b/i.test(_msgText)) {
    if (typeof window.appendToReasoningPanel === "function") {
      window.appendToReasoningPanel({ type: "text", text: "[Plugin context]" });
    }
    return null;
  }

  // Post-generation verification user messages
  if (
    role === "user" &&
    /^Review your response against the original user request/i.test(_msgText)
  ) {
    if (typeof window.appendToReasoningPanel === "function") {
      window.appendToReasoningPanel({ type: "text", text: "[Post-generation verification]" });
    }
    return null;
  }

  const wrapper = document.createElement("div");
  wrapper.className = `message ${isUser ? "user-msg" : "symi-msg"}`;

  const bubble = document.createElement("div");
  bubble.className = isUser ? "bubble" : "bubble stream-bubble done";

  // Add copy button for assistant messages
  if (!isUser) {
    const copyBtn = document.createElement("button");
    copyBtn.className = "msg-copy-btn";
    copyBtn.title = "Copy";
    copyBtn.addEventListener("click", function () {
      window.copyMessage(this);
    });
    copyBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none">
      <rect x="9" y="9" width="13" height="13" rx="2" stroke="currentColor" stroke-width="2"/>
      <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="2"/>
    </svg>`;
    bubble.appendChild(copyBtn);
  }

  // Render each content block — route reasoning/tool blocks to the side panel
  const REASONING_TYPES = new Set(["thinking", "tool_use", "tool_result", "toolCall"]);
  const isNarration = !isUser && message.stopReason === "toolUse";
  const blocks = Array.isArray(content) ? content : [{ type: "text", text: extractText(content) }];
  for (const block of blocks) {
    if (!isUser && REASONING_TYPES.has(block.type)) {
      // Route to Reasoning Panel — keep response bubble clean
      window.appendToReasoningPanel(block);
      continue;
    }
    // Text blocks in tool-use messages are narration — route to Reasoning Panel
    if (isNarration && block.type === "text") {
      window.appendToReasoningPanel(block);
      continue;
    }
    const el = renderBlock(block);
    if (el.childNodes.length || el.innerHTML) {
      bubble.appendChild(el);
    }
  }

  // If the bubble has no visible content (only copy button), skip it entirely
  const visibleChildren = bubble.querySelectorAll(
    ".msg-text, .msg-image, .msg-thinking, .msg-tool-card, .msg-tool-result",
  );
  if (!isUser && visibleChildren.length === 0) {
    return null;
  }

  // Timestamp
  if (timestamp) {
    const ts = document.createElement("div");
    ts.className = "msg-timestamp";
    ts.textContent = formatTime(timestamp);
    bubble.appendChild(ts);
  }

  wrapper.appendChild(bubble);
  wrapper.appendChild(
    Object.assign(document.createElement("div"), { className: "message-clearfix" }),
  );
  return wrapper;
};

// ── Re-render for streaming (plain text, no markdown until final) ──────
window.renderStreamText = function (el, text) {
  if (!el) {
    return;
  }
  el.textContent = text;
};

// ── Apply highlight.js to any unhighlighted code blocks ──────────────
window.applyHighlighting = function (container) {
  if (typeof hljs === "undefined") {
    return;
  }
  container.querySelectorAll("pre code:not(.hljs)").forEach((block) => {
    hljs.highlightElement(block);
  });
};
