import { useState, useEffect, useRef } from "react";
import { createSession, resolveApiUrl, stopSessionRun, streamChat, uploadSessionFiles } from "./api";

const DEFAULT_LLM_PROVIDER =
  (import.meta.env.VITE_DEFAULT_LLM_PROVIDER || "deepseek").trim().toLowerCase();

export default function ChatBox() {
  const [sessionId, setSessionId] = useState(null);
  const [llmProvider, setLlmProvider] = useState(DEFAULT_LLM_PROVIDER);
  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState("");
  const [isStreaming, setIsStreaming] = useState(false);
  const [isUploading, setIsUploading] = useState(false);
  const [selectedFiles, setSelectedFiles] = useState([]);
  const [uploadedFiles, setUploadedFiles] = useState([]);
  const [activeAssistantIndex, setActiveAssistantIndex] = useState(null);
  const cancelRef = useRef(null);
  const fileInputRef = useRef(null);
  const folderInputRef = useRef(null);
  const conversationBottomRef = useRef(null);
  const reasoningBottomRef = useRef(null);

  // 初始化 session
  useEffect(() => {
    setSessionId(null);
    createSession(llmProvider).then(setSessionId);
    return () => cancelRef.current?.();
  }, [llmProvider]);

  // 自动滚动到底部
  useEffect(() => {
    conversationBottomRef.current?.scrollIntoView({ behavior: "smooth" });
    reasoningBottomRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [messages]);

  const sendMessage = async () => {
    if (!input.trim() || isStreaming || !sessionId || isUploading) return;

    let finalMessage = input;
    if (selectedFiles.length > 0) {
      const uploadedSignatures = new Set(
        uploadedFiles.map((f) => `${f.name}::${f.size}::${f.lastModified}`)
      );
      const pendingFiles = selectedFiles.filter(
        (f) => !uploadedSignatures.has(`${f.name}::${f.size}::${f.lastModified}`)
      );

      if (pendingFiles.length > 0) {
        setIsUploading(true);
        try {
          const uploadRes = await uploadSessionFiles(sessionId, pendingFiles);
          const uploadedNow = pendingFiles.map((f, idx) => ({
            ...(uploadRes?.files?.[idx] || {}),
            name: f.name,
            size: f.size,
            lastModified: f.lastModified,
          }));
          setUploadedFiles((prev) => [...prev, ...uploadedNow]);
        } catch (err) {
          setMessages((prev) => [
            ...prev,
            { role: "assistant", steps: [{ type: "error", content: String(err) }], done: true },
          ]);
          setIsUploading(false);
          return;
        }
        setIsUploading(false);
      }

      finalMessage = `${finalMessage}\n\n[Use uploaded files: ${selectedFiles
        .map((f) => f.name)
        .join(", ")}]`;
    }

    const userMsg = { role: "user", content: input };
    const agentMsg = {
      role: "assistant",
      steps: [],   // 每个 step: { type, content, lang?, tool? }
      done: false,
    };

    setMessages((prev) => {
      const next = [...prev, userMsg, agentMsg];
      setActiveAssistantIndex(next.length - 1);
      return next;
    });
    setIsStreaming(true);
    setInput("");

    const cancel = streamChat(finalMessage, sessionId, (event) => {
      if (event.type === "heartbeat") return;

      if (event.type === "done") {
        setIsStreaming(false);
        setMessages((prev) => {
          const msgs = [...prev];
          msgs[msgs.length - 1] = { ...msgs[msgs.length - 1], done: true };
          return msgs;
        });
        return;
      }

      if (event.type === "error") {
        setIsStreaming(false);
        setMessages((prev) => {
          const msgs = [...prev];
          const last = msgs[msgs.length - 1];
          msgs[msgs.length - 1] = {
            ...last,
            done: true,
            steps: [...last.steps, event],
          };
          return msgs;
        });
        return;
      }

      // 把每个事件追加到最后一条 assistant 消息的 steps 中
      setMessages((prev) => {
        const msgs = [...prev];
        const last = msgs[msgs.length - 1];
        const steps = last.steps || [];

        msgs[msgs.length - 1] = {
          ...last,
          steps: [...steps, event],
        };
        return msgs;
      });
    }, { llmProvider });

    cancelRef.current = cancel;
  };

  const stopCurrentRun = async () => {
    if (sessionId) {
      try {
        await stopSessionRun(sessionId);
      } catch (err) {
        console.warn("Failed to notify backend stop:", err);
      }
    }
    cancelRef.current?.();
    cancelRef.current = null;
    setIsStreaming(false);
    setMessages((prev) => {
      if (prev.length === 0) return prev;
      const msgs = [...prev];
      const last = msgs[msgs.length - 1];
      if (last?.role !== "assistant" || last.done) return prev;
      msgs[msgs.length - 1] = {
        ...last,
        done: true,
        steps: [...last.steps, { type: "error", content: "Run cancelled by user." }],
      };
      return msgs;
    });
  };

  const handleKeyDown = (e) => {
    if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault();
      sendMessage();
    }
  };

  const appendSelectedFiles = (incomingFiles) => {
    setSelectedFiles((prev) => {
      const merged = [...prev];
      const seen = new Set(prev.map((f) => `${f.name}::${f.size}::${f.lastModified}`));
      for (const file of incomingFiles) {
        const key = `${file.name}::${file.size}::${file.lastModified}`;
        if (!seen.has(key)) {
          merged.push(file);
          seen.add(key);
        }
      }
      return merged;
    });
    setUploadedFiles([]);
  };

  const onFileChange = (e) => {
    const files = Array.from(e.target.files || []);
    appendSelectedFiles(files);
    e.target.value = "";
  };

  const onFolderChange = (e) => {
    const files = Array.from(e.target.files || []);
    appendSelectedFiles(files);
    e.target.value = "";
  };

  const assistantIndexes = messages
    .map((msg, idx) => ({ msg, idx }))
    .filter((item) => item.msg.role === "assistant");
  const currentAssistant =
    activeAssistantIndex != null && messages[activeAssistantIndex]?.role === "assistant"
      ? messages[activeAssistantIndex]
      : assistantIndexes.length > 0
        ? assistantIndexes[assistantIndexes.length - 1].msg
        : null;

  return (
    <div className="chat-workspace">
      <div className="workspace-header">
        <div>
          <div className="header-title">Strata Bio OS</div>
          <div className="header-subtitle">Conversation + Structured Reasoning</div>
        </div>
        <span className="session-badge">
          {sessionId ? `Session ${sessionId.slice(0, 8)}…` : "Initializing…"}
        </span>
      </div>

      <div className="workspace-body">
        <section className="conversation-pane">
          <div className="pane-title">Conversation</div>
          <div className="conversation-list">
            {messages.map((msg, i) => (
              <ConversationItem
                key={i}
                message={msg}
                isActive={i === activeAssistantIndex}
                onSelect={() => msg.role === "assistant" && setActiveAssistantIndex(i)}
              />
            ))}
            {isStreaming && (
              <div className="streaming-indicator">
                <span className="dot" /><span className="dot" /><span className="dot" />
              </div>
            )}
            <div ref={conversationBottomRef} />
          </div>
        </section>

        <section className="reasoning-pane">
          <div className="pane-title">Reasoning</div>
          <div className="reasoning-list">
            {currentAssistant ? (
              <ReasoningPanel steps={currentAssistant.steps} done={currentAssistant.done} />
            ) : (
              <div className="empty-hint">
                Run a request to see the agent reasoning breakdown.
              </div>
            )}
            <div ref={reasoningBottomRef} />
          </div>
        </section>
      </div>

      <div className="input-dock">
        <div className="file-row">
          <select
            value={llmProvider}
            onChange={(e) => setLlmProvider(e.target.value)}
            disabled={isStreaming || isUploading}
          >
            <option value="deepseek">DeepSeek</option>
            <option value="gemini">Gemini</option>
            <option value="claude">Claude</option>
            <option value="openai">OpenAI</option>
          </select>
          <input
            ref={fileInputRef}
            className="upload-hidden"
            type="file"
            multiple
            onChange={onFileChange}
            disabled={isStreaming || !sessionId || isUploading}
          />
          <input
            ref={folderInputRef}
            className="upload-hidden"
            type="file"
            multiple
            webkitdirectory="true"
            directory="true"
            onChange={onFolderChange}
            disabled={isStreaming || !sessionId || isUploading}
          />
          <div className="upload-actions">
            <button
              type="button"
              className="upload-btn"
              onClick={() => fileInputRef.current?.click()}
              disabled={isStreaming || !sessionId || isUploading}
            >
              📄 Upload Files
            </button>
            <button
              type="button"
              className="upload-btn"
              onClick={() => folderInputRef.current?.click()}
              disabled={isStreaming || !sessionId || isUploading}
            >
              📁 Upload Folder
            </button>
          </div>
          <span className="file-hint">
            {`Backend: ${
              llmProvider === "openai"
                ? "OpenAI"
                : llmProvider === "deepseek"
                  ? "DeepSeek"
                : llmProvider === "gemini"
                  ? "Gemini"
                  : "Claude"
            } | `}
            {selectedFiles.length > 0
              ? `Selected ${selectedFiles.length} file(s): ${selectedFiles
                  .map((f) => f.name)
                  .join(", ")}${uploadedFiles.length > 0 ? " (uploaded)" : ""}`
              : "Optional: upload one or multiple files for this run"}
          </span>
        </div>
        <div className="composer-row">
          <textarea
            value={input}
            onChange={(e) => setInput(e.target.value)}
            onKeyDown={handleKeyDown}
            placeholder="Enter a biomedical task, e.g.: Predict ADMET for CC(C)CC1=CC=C(C=C1)C(C)C(=O)O"
            disabled={isStreaming || !sessionId || isUploading}
            rows={5}
          />
          <button
            onClick={isStreaming ? stopCurrentRun : sendMessage}
            disabled={!sessionId || isUploading || (!isStreaming && !input.trim())}
          >
            {isStreaming ? "Stop" : (isUploading ? "Uploading..." : "Run")}
          </button>
        </div>
      </div>
    </div>
  );
}

function ConversationItem({ message, isActive, onSelect }) {
  if (message.role === "user") {
    return (
      <div className="conversation-item user-item">
        <div className="item-label">User</div>
        <div className="item-content">{message.content}</div>
      </div>
    );
  }

  const finalStep = getAssistantFinalStep(message);
  return (
    <button
      type="button"
      className={`conversation-item assistant-item ${isActive ? "active-item" : ""}`}
      onClick={onSelect}
    >
      <div className="item-top">
        <span className="item-label">Strata Agent Results</span>
      </div>
      <div className="item-content">
        <RenderedFinalResult
          content={finalStep?.content || ""}
          artifacts={Array.isArray(finalStep?.artifacts) ? finalStep.artifacts : []}
        />
      </div>
    </button>
  );
}

function getAssistantFinalStep(message) {
  const result = [...message.steps].reverse().find((s) => s.type === "result");
  if (result?.content) return result;
  const err = [...message.steps].reverse().find((s) => s.type === "error");
  if (err?.content) return err;
  return {
    content: message.done ? "No final result output." : "Running... (final result pending)",
    artifacts: [],
  };
}

function RenderedFinalResult({ content, artifacts = [] }) {
  const blocks = parseMarkdownLikeBlocks(prepareFinalResultContent(content || ""));
  return (
    <div className="final-render">
      {artifacts.length > 0 && <ArtifactDownloads artifacts={artifacts} />}
      {blocks.map((block, idx) => {
        if (block.type === "table") {
          return <ResultTable key={idx} headers={block.headers} rows={block.rows} />;
        }
        return <ResultTextBlock key={idx} content={block.content} />;
      })}
    </div>
  );
}

function ArtifactDownloads({ artifacts }) {
  return (
    <div className="artifact-panel">
      <div className="artifact-title">Download Files</div>
      <div className="artifact-list">
        {artifacts.map((artifact, idx) => (
          <a
            key={`${artifact.root}:${artifact.relative_path}:${idx}`}
            className="artifact-link"
            href={resolveApiUrl(artifact.download_path)}
            target="_blank"
            rel="noreferrer"
          >
            <span className="artifact-name">{artifact.name || artifact.relative_path}</span>
            <span className="artifact-size">{formatArtifactSize(artifact.size_bytes)}</span>
          </a>
        ))}
      </div>
    </div>
  );
}

function formatArtifactSize(sizeBytes) {
  const size = Number(sizeBytes || 0);
  if (!Number.isFinite(size) || size <= 0) return "0 B";
  if (size < 1024) return `${size} B`;
  if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)} KB`;
  if (size < 1024 * 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(2)} MB`;
  return `${(size / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}

function ResultTextBlock({ content }) {
  const sections = parseRichTextSections(normalizeForDisplay(content || ""));
  return (
    <div className="result-rich">
      {sections.map((section, idx) => {
        if (section.type === "heading") {
          return (
            <div key={idx} className={`result-heading h${section.level}`}>
              {renderInlineMarkdown(section.text)}
            </div>
          );
        }
        if (section.type === "list") {
          return (
            <ul key={idx} className="result-list">
              {section.items.map((item, i) => (
                <li key={i}>{renderInlineMarkdown(item)}</li>
              ))}
            </ul>
          );
        }
        if (section.type === "quote") {
          return <blockquote key={idx} className="result-quote">{renderInlineMarkdown(section.text)}</blockquote>;
        }
        if (section.type === "sequence") {
          return (
            <div key={idx} className="result-seq-wrap">
              <div className="result-code-lang">Sequence</div>
              <pre className="result-seq">{section.text}</pre>
            </div>
          );
        }
        return <div key={idx} className="result-text">{renderInlineMarkdown(section.text)}</div>;
      })}
    </div>
  );
}

function ResultTable({ headers, rows }) {
  return (
    <div className="result-table-wrap">
      <table className="result-table">
        <thead>
          <tr>
            {headers.map((h, i) => (
              <th key={i}>{h}</th>
            ))}
          </tr>
        </thead>
        <tbody>
          {rows.map((row, rIdx) => (
            <tr key={rIdx}>
              {headers.map((_, cIdx) => (
                <td key={cIdx}>{renderInlineMarkdown(row[cIdx] || "")}</td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

function parseMarkdownLikeBlocks(input) {
  const lines = input.split("\n");
  const blocks = [];
  let i = 0;

  while (i < lines.length) {
    const line = lines[i];

    // fenced code block
    if (line.trim().startsWith("```")) {
      const lang = line.trim().slice(3).trim();
      i += 1;
      const codeLines = [];
      while (i < lines.length && !lines[i].trim().startsWith("```")) {
        codeLines.push(lines[i]);
        i += 1;
      }
      if (i < lines.length) i += 1;
      blocks.push({ type: "code", lang, content: codeLines.join("\n") });
      continue;
    }

    // markdown table block
    if (isTableHeaderLine(line) && i + 1 < lines.length && isTableDividerLine(lines[i + 1])) {
      const headerCells = splitTableCells(line);
      i += 2;
      const rows = [];
      while (i < lines.length && isTableRowLine(lines[i])) {
        rows.push(splitTableCells(lines[i]));
        i += 1;
      }
      blocks.push({ type: "table", headers: headerCells, rows });
      continue;
    }

    // plain text block
    const textLines = [line];
    i += 1;
    while (i < lines.length) {
      const atCode = lines[i].trim().startsWith("```");
      const atTable = isTableHeaderLine(lines[i]) && i + 1 < lines.length && isTableDividerLine(lines[i + 1]);
      if (atCode || atTable) break;
      textLines.push(lines[i]);
      i += 1;
    }
    blocks.push({ type: "text", content: textLines.join("\n") });
  }

  return blocks.filter((b) => {
    if (b.type === "text") return b.content.trim().length > 0;
    if (b.type === "code") return b.content.trim().length > 0;
    return true;
  });
}

function isTableHeaderLine(line) {
  const trimmed = line.trim();
  return trimmed.includes("|") && splitTableCells(trimmed).length >= 2;
}

function isTableDividerLine(line) {
  return /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line);
}

function isTableRowLine(line) {
  const trimmed = line.trim();
  return trimmed.length > 0 && trimmed.includes("|");
}

function splitTableCells(line) {
  return line
    .trim()
    .replace(/^\|/, "")
    .replace(/\|$/, "")
    .split("|")
    .map((cell) => cell.trim());
}

function prepareFinalResultContent(raw) {
  let text = String(raw || "");
  const solutionMatch = text.match(/<solution>([\s\S]*?)<\/solution>/i);
  if (solutionMatch?.[1]) {
    text = solutionMatch[1];
  }

  text = text.replace(/<execute>[\s\S]*?<\/execute>/gi, " ");
  text = text.replace(/<observation>[\s\S]*?<\/observation>/gi, " ");
  text = text.replace(/<output>[\s\S]*?<\/output>/gi, " ");
  text = text.replace(/<\/?(solution|observation|output|execute)>/gi, " ");
  text = normalizeForDisplay(text);
  text = text.replace(/`<\/?(solution|observation|output|execute)>`/gi, " ");
  text = text.replace(/^\s*```[\s\S]*?```\s*$/gm, " ");
  text = text.replace(/^\s*with\s+open\(.*$/gim, " ");
  text = text.replace(/^\s*print\(.*$/gim, " ");
  text = text.replace(/^\s*for\s+.+:\s*$/gim, " ");
  text = text.replace(/^\s*\w+\s*=\s*os\.path\..*$/gim, " ");
  text = text.replace(/\n{3,}/g, "\n\n");
  return text.trim();
}

function renderInlineMarkdown(text) {
  const lines = String(text || "").split("\n");
  return lines.map((line, index) => (
    <span key={index}>
      {renderInlineMarkdownSegments(line)}
      {index < lines.length - 1 ? <br /> : null}
    </span>
  ));
}

function renderInlineMarkdownSegments(text) {
  const source = String(text || "");
  const regex = /(\*\*([^*]+)\*\*|`([^`]+)`)/g;
  const parts = [];
  let lastIndex = 0;
  let match;

  while ((match = regex.exec(source)) !== null) {
    if (match.index > lastIndex) {
      parts.push(source.slice(lastIndex, match.index));
    }
    if (match[2] != null) {
      parts.push(<strong key={`b-${match.index}`} className="result-inline-bold">{match[2]}</strong>);
    } else if (match[3] != null) {
      parts.push(<code key={`c-${match.index}`} className="result-inline-code">{match[3]}</code>);
    }
    lastIndex = regex.lastIndex;
  }

  if (lastIndex < source.length) {
    parts.push(source.slice(lastIndex));
  }
  return parts;
}

function ReasoningPanel({ steps, done }) {
  const expandedSteps = normalizeReasoningSteps(steps);
  const reasoningSteps = expandedSteps.filter((step) =>
    ["thinking", "code", "tool_use", "observation", "visualization"].includes(step.type)
  );
  const readableSteps = reasoningSteps
    .map((step) => ({
      ...step,
      readableContent: summarizeReasoningStep(step),
    }))
    .filter((step) => Boolean((step.readableContent || "").trim()));

  const hasNonProcessFinal = expandedSteps.some((step) => step.type === "result" || step.type === "error");
  const stats = getReasoningStats(readableSteps);
  const progress = readableSteps.length
    ? Math.min(100, Math.round((stats.observation + stats.tool + stats.thinking) / readableSteps.length * 100))
    : 0;

  if (!readableSteps.length) {
    return <div className="empty-hint">Agent is preparing reasoning steps…</div>;
  }

  return (
    <div className="reasoning-shell">
      <div className="reasoning-summary">
        <span className="summary-chip">Total {readableSteps.length}</span>
        <span className="summary-chip">Planning {stats.thinking}</span>
        <span className="summary-chip">Actions {stats.tool}</span>
        <span className="summary-chip">Progress {stats.observation}</span>
      </div>
      <div className="reasoning-progress">
        <div className="reasoning-progress-bar" style={{ width: `${progress}%` }} />
      </div>
      <div className="reasoning-timeline">
        {readableSteps.map((step, idx) => (
          <div
            key={idx}
            className={`timeline-item ${!done && idx === readableSteps.length - 1 ? "live-step" : ""}`}
          >
            <div className="timeline-node" />
            <ReasoningCard
              step={step}
              index={idx + 1}
              isLive={!done && idx === readableSteps.length - 1}
            />
          </div>
        ))}
      </div>
      {!done && <div className="running-hint">Reasoning in progress…</div>}
      {done && hasNonProcessFinal && (
        <div className="running-hint">Final result is shown in the left conversation pane.</div>
      )}
    </div>
  );
}

function ReasoningCard({ step, index, isLive }) {
  const meta = getStepMeta(step, index);
  return (
    <div className={`reasoning-card ${getStepToneClass(step.type)}`}>
      <div className="card-header">
        <span className="card-tag">{meta.title}</span>
        <span className={`state-pill ${isLive ? "live-pill" : ""}`}>{isLive ? "LIVE" : "DONE"}</span>
      </div>
      <div className="card-subtitle">{meta.subtitle}</div>
      <div className="card-icon">{meta.icon}</div>
      <ReasoningContent step={step} isLive={isLive} />
    </div>
  );
}

function ReasoningContent({ step, isLive = false }) {
  if (step.type === "visualization") {
    const imageSrc = step.image_data_url || (step.download_path ? resolveApiUrl(step.download_path) : "");
    const caption = String(step.readableContent || step.content || step.title || "Generated figure").trim();
    if (!imageSrc) return null;
    return (
      <div className="visualization-card">
        <div className="visualization-caption">{caption}</div>
        <a
          className="visualization-link"
          href={imageSrc}
          target="_blank"
          rel="noreferrer"
        >
          <img className="visualization-image" src={imageSrc} alt={step.title || "Generated visualization"} />
        </a>
      </div>
    );
  }

  const content = String(step.readableContent || "").trim();
  if (!content) return null;

  if (step.type === "thinking") {
    return (
      <div className="plain-block planning-block">
        <div className="planning-text">{content}</div>
      </div>
    );
  }

  if (step.type === "tool_use") {
    const toolName = formatToolDisplayName(step.tool || "MCP Tool");
    return (
      <div className={`tool-activity-card ${isLive ? "tool-activity-live" : ""}`}>
        <div className="tool-activity-top">
          <span className="tool-activity-label">MCP Invocation</span>
          <span className={`tool-activity-state ${isLive ? "running" : "finished"}`}>
            {isLive ? "Calling" : "Completed"}
          </span>
        </div>
        <div className="tool-activity-name-row">
          <span className="tool-activity-dot" />
          <span className="tool-activity-name">{toolName}</span>
        </div>
        <div className="tool-activity-track" aria-hidden="true">
          <span className="tool-activity-wave wave-1" />
          <span className="tool-activity-wave wave-2" />
          <span className="tool-activity-wave wave-3" />
        </div>
        <div className="tool-activity-desc">{content}</div>
      </div>
    );
  }

  if (step.type === "observation") {
    const blocks = parseObservationDisplayBlocks(content);
    return (
      <div className="reasoning-rich">
        {blocks.map((block, idx) => {
          if (block.type === "heading") {
            return (
              <div key={idx} className={`observation-heading level-${block.level || 1}`}>
                {block.text}
              </div>
            );
          }
          if (block.type === "note") {
            return <div key={idx} className="observation-note">{block.text}</div>;
          }
          if (block.type === "table") {
            return <ResultTable key={idx} headers={block.headers} rows={block.rows} />;
          }
          if (block.type === "list") {
            return (
              <ul key={idx} className="result-list observation-list">
                {block.items.map((item, i) => (
                  <li key={i}>{item}</li>
                ))}
              </ul>
            );
          }
          return <pre key={idx} className="plain-block observation-block">{block.text}</pre>;
        })}
      </div>
    );
  }

  return <pre className="plain-block">{content}</pre>;
}

function formatToolDisplayName(toolName) {
  return String(toolName || "MCP Tool")
    .replace(/_/g, " ")
    .replace(/-/g, " ")
    .replace(/\s+/g, " ")
    .trim();
}

function normalizeForDisplay(raw) {
  const text = String(raw ?? "")
    .replace(/biomni\s*agent\s*results?/gi, "Strata Agent Results")
    .replace(/biommi\s*agent\s*results?/gi, "Strata Agent Results")
    .replace(/biomni\s*a1\s*is\s*analyzing(?:\s+query\s+and\s+input\s+data)?\.*/gi, "Agent is analyzing...")
    .replace(/\bbiomni\b/gi, "Strata");
  // Split packed XML/HTML-like tags into lines for readability.
  const lineBroken = text.replace(/>\s*</g, ">\n<");
  // Render angle-bracket tokens as markdown-style inline code.
  return lineBroken.replace(/(<\/?[A-Za-z][^>\n]{0,120}>)/g, "`$1`");
}

function normalizeReasoningSteps(steps) {
  const merged = [];
  let thinkingBuffer = null;

  const flushThinking = () => {
    if (!thinkingBuffer?.content?.trim()) return;
    const sections = splitThinkingSections(thinkingBuffer.content);
    if (sections.length <= 1) {
      merged.push(thinkingBuffer);
      thinkingBuffer = null;
      return;
    }
    sections.forEach((section, idx) => {
      merged.push({
        ...thinkingBuffer,
        content: section,
        thinking_part: idx + 1,
        thinking_total: sections.length,
      });
    });
    thinkingBuffer = null;
  };

  for (const step of steps) {
    if (step.type !== "thinking") {
      flushThinking();
      merged.push(step);
      continue;
    }

    const text = (step.content || "").trim();
    if (!text) continue;

    if (!thinkingBuffer) {
      thinkingBuffer = { ...step, content: text };
      continue;
    }

    // 连续 thinking 聚合成一个“部分”，避免按行碎裂
    thinkingBuffer = {
      ...thinkingBuffer,
      content: `${thinkingBuffer.content}\n${text}`.trim(),
    };
  }

  flushThinking();
  return merged;
}

function splitThinkingSections(text) {
  const lines = text.split("\n");
  const delimiter = /^=+\s*Ai Message\s*=+$/i;
  const sections = [];
  let current = [];

  const flush = () => {
    const block = current.join("\n").trim();
    if (block) sections.push(block);
    current = [];
  };

  for (const line of lines) {
    if (delimiter.test(line.trim()) && current.length > 0) {
      flush();
      current.push(line);
      continue;
    }
    current.push(line);
  }
  flush();
  return sections.length ? sections : [text.trim()].filter(Boolean);
}

function parseRichTextSections(content) {
  const lines = content.split("\n");
  const sections = [];
  let i = 0;

  const pushText = (text) => {
    const t = text.trim();
    if (!t) return;
    sections.push({ type: detectSequenceBlock(t) ? "sequence" : "text", text: t });
  };

  while (i < lines.length) {
    const line = lines[i];
    const trimmed = line.trim();

    if (!trimmed) {
      i += 1;
      continue;
    }

    const headingMatch = /^(#{1,4})\s+(.*)$/.exec(trimmed);
    if (headingMatch) {
      sections.push({
        type: "heading",
        level: headingMatch[1].length,
        text: headingMatch[2].trim(),
      });
      i += 1;
      continue;
    }

    if (trimmed.startsWith(">")) {
      const quoteLines = [];
      while (i < lines.length && lines[i].trim().startsWith(">")) {
        quoteLines.push(lines[i].trim().replace(/^>\s?/, ""));
        i += 1;
      }
      sections.push({ type: "quote", text: quoteLines.join("\n") });
      continue;
    }

    if (/^[-*]\s+/.test(trimmed) || /^\d+\.\s+/.test(trimmed)) {
      const items = [];
      while (i < lines.length) {
        const t = lines[i].trim();
        if (!(/^[-*]\s+/.test(t) || /^\d+\.\s+/.test(t))) break;
        items.push(t.replace(/^([-*]|\d+\.)\s+/, ""));
        i += 1;
      }
      sections.push({ type: "list", items });
      continue;
    }

    const para = [line];
    i += 1;
    while (i < lines.length) {
      const t = lines[i].trim();
      if (!t) break;
      if (/^(#{1,4})\s+/.test(t)) break;
      if (t.startsWith(">")) break;
      if (/^[-*]\s+/.test(t) || /^\d+\.\s+/.test(t)) break;
      para.push(lines[i]);
      i += 1;
    }
    pushText(para.join("\n"));
  }

  return sections;
}

function detectSequenceBlock(text) {
  const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
  if (lines.length < 2) return false;
  const dnaLike = lines.filter((l) => /^[ACGTUNacgtun\s]+$/.test(l) && l.replace(/\s/g, "").length >= 20);
  return dnaLike.length >= 2;
}

function getReasoningStats(steps) {
  return steps.reduce(
    (acc, step) => {
      if (step.type === "thinking") acc.thinking += 1;
      if (step.type === "tool_use") acc.tool += 1;
      if (step.type === "observation") acc.observation += 1;
      if (step.type === "visualization") acc.observation += 1;
      return acc;
    },
    { thinking: 0, tool: 0, observation: 0 }
  );
}

function getStepToneClass(stepType) {
  if (stepType === "thinking") return "thinking-card";
  if (stepType === "tool_use") return "tool-card";
  if (stepType === "observation") return "observation-card";
  if (stepType === "visualization") return "observation-card";
  if (stepType === "code") return "code-card";
  return "thinking-card";
}

function getStepMeta(step, index) {
  if (step.type === "thinking") {
    return {
      title: `Step ${index} · Reasoning`,
      subtitle: "Planning and decomposition of the task",
      icon: "💭",
    };
  }
  if (step.type === "code") {
    return {
      title: `Step ${index} · Executing Task`,
      subtitle: "Performing an analysis action",
      icon: "🛠️",
    };
  }
  if (step.type === "tool_use") {
    return {
      title: `Step ${index} · Tool Call`,
      subtitle: "Invoking external tool",
      icon: "🧰",
    };
  }
  if (step.type === "visualization") {
    return {
      title: `Step ${index} · Output Progress`,
      subtitle: "Preparing visual outputs",
      icon: "🖼️",
    };
  }
  return {
    title: `Step ${index} · Observation`,
    subtitle: "Execution output and tool response",
    icon: "📊",
  };
}

function summarizeReasoningStep(step) {
  const compact = extractAiNarrative(step.content || "", step.type);
  if (step.type === "thinking") {
    if (!compact) return "";
    return compact;
  }
  if (step.type === "tool_use") {
    return "正在调用分析工具并获取中间结果。";
  }
  if (step.type === "observation") {
    if (!compact) return "";
    return compact;
  }
  if (step.type === "visualization") {
    return step.title || step.content || "Generated figure";
  }
  return "";
}

function extractAiNarrative(raw, stepType = "") {
  let text = String(raw || "");
  text = text.replace(/<execute>[\s\S]*?<\/execute>/gi, " ");
  text = text.replace(/```[\s\S]*?```/g, " ");
  text = stripObservationTags(text);
  text = stripHumanMessageSections(text);
  text = normalizeForDisplay(text);
  text = text.replace(/=+\s*Ai Message\s*=+/gi, " ");

  if (stepType === "observation") {
    text = formatObservationText(text);
  } else {
    text = text.replace(/<\/?observation>/gi, " ");
  }

  const filteredLines = text
    .split("\n")
    .map((line) => line.trim())
    .filter(Boolean)
    .filter((line) => !/(traceback|error|exception|module.*not found|failed|no module named)/i.test(line))
    .filter((line) => !/(seem to be running but|not being created|take a different approach|alternative approach|instead, I will|use .* instead|fallback to|switching to|using .* as a fallback)/i.test(line))
    .filter((line) => !/^={3,}$/.test(line));

  if (stepType === "observation") {
    const compact = filteredLines.join("\n").trim();
    if (!compact) return "";
    return truncateMultilineText(compact, 48, 2000);
  }

  const compact = filteredLines.join(" ").replace(/\s+/g, " ").trim();
  return compact;
}

function stripHumanMessageSections(text) {
  return String(text || "")
    .replace(
      /=+\s*Human Message\s*=+[\s\S]*?(?==+\s*(?:Ai|AI)\s*Message\s*=+|$)/gi,
      " "
    )
    .replace(/^Human Message:.*$/gim, " ");
}

function formatObservationText(text) {
  const jsonSummary = formatObservationJson(text);
  if (jsonSummary) {
    return jsonSummary;
  }

  const sectionMatch = text.match(/===\s*([^=]+?)\s*===/i);
  const sectionTitle = sectionMatch ? sectionMatch[1].trim() : "";
  const lines = text.split("\n").map((line) => line.trim()).filter(Boolean);
  const rowLines = lines.filter((line) => /^\d+\s+/.test(line));
  const parsedRows = [];

  for (const line of rowLines) {
    const parts = line.split(/\s{2,}/).map((x) => x.trim()).filter(Boolean);
    if (parts.length < 6) continue;
    const term = parts[2] || "";
    const overlap = parts[3] || "";
    const adjP = parts[5] || "";
    const genes = parts[parts.length - 1] || "";
    if (!term) continue;
    parsedRows.push({ term, overlap, adjP, genes });
  }

  if (!parsedRows.length) {
    return formatObservationNarrative(text);
  }

  const topRows = parsedRows.slice(0, 5);
  const bulletText = topRows
    .map((row, idx) => `${idx + 1}. ${row.term} | Overlap=${row.overlap} | Adj.P=${row.adjP} | Genes=${row.genes}`)
    .join("\n");
  return `${sectionTitle ? `${sectionTitle}\n` : ""}Top enriched terms:\n${bulletText}`;
}

function stripObservationTags(text) {
  return String(text || "")
    .replace(/<\/?observation>/gi, " ")
    .replace(/<\/?observation[^>]*>/gi, " ");
}

function formatObservationNarrative(text) {
  const lines = String(text || "")
    .split("\n")
    .map((line) => line.trim())
    .filter(Boolean)
    .map((line) => line.replace(/^["'`]+|["'`]+$/g, "").trim())
    .filter(Boolean);

  if (!lines.length) return "";

  const cleaned = [];
  for (const line of lines) {
    if (cleaned.length === 0 || cleaned[cleaned.length - 1] !== line) {
      cleaned.push(line);
    }
  }

  const formatted = cleaned.map((line) => {
    if (/completed successfully!?$/i.test(line)) {
      return `Completed: ${line.replace(/completed successfully!?$/i, "").trim() || "Step finished successfully"}`;
    }
    if (/running /i.test(line) || /loading /i.test(line) || /processing /i.test(line)) {
      return `In progress: ${line}`;
    }
    return line;
  });

  return formatted.join("\n");
}

function formatObservationJson(text) {
  const source = String(text || "");
  const parsed = extractFirstJsonObject(source);
  if (!parsed) return "";

  const lines = source
    .split("\n")
    .map((line) => line.trim())
    .filter(Boolean);
  const fileHint = lines.find((line) => /saved to .*\.json/i.test(line)) || "";

  try {
    const pretty = JSON.stringify(JSON.parse(parsed), null, 2);
    const head = fileHint ? `${fileHint}\n` : "";
    return `${head}JSON summary:\n${truncateMultilineText(pretty, 40, 1700)}`;
  } catch {
    return "";
  }
}

function extractFirstJsonObject(text) {
  const source = String(text || "");
  const start = source.indexOf("{");
  if (start < 0) return "";

  let depth = 0;
  let inString = false;
  let escaped = false;

  for (let i = start; i < source.length; i += 1) {
    const ch = source[i];

    if (inString) {
      if (escaped) {
        escaped = false;
        continue;
      }
      if (ch === "\\") {
        escaped = true;
        continue;
      }
      if (ch === "\"") {
        inString = false;
      }
      continue;
    }

    if (ch === "\"") {
      inString = true;
      continue;
    }
    if (ch === "{") {
      depth += 1;
      continue;
    }
    if (ch === "}") {
      depth -= 1;
      if (depth === 0) {
        return source.slice(start, i + 1);
      }
    }
  }
  return "";
}

function truncateMultilineText(text, maxLines = 40, maxChars = 1800) {
  const lines = String(text || "").split("\n");
  const clippedLines = lines.slice(0, maxLines);
  let clipped = clippedLines.join("\n").trim();

  if (clipped.length > maxChars) {
    clipped = `${clipped.slice(0, maxChars).trimEnd()}\n...`;
  } else if (lines.length > maxLines) {
    clipped = `${clipped}\n...`;
  }
  return clipped;
}

function parseObservationDisplayBlocks(content) {
  const jsonBlocks = parseObservationJsonBlocks(content);
  if (jsonBlocks) return jsonBlocks;
  return parseObservationTextBlocks(content);
}

function parseObservationJsonBlocks(content) {
  const source = String(content || "");
  const jsonText = extractFirstJsonObject(source);
  if (!jsonText) return null;

  try {
    const parsed = JSON.parse(jsonText);
    const lines = source.split("\n").map((line) => line.trim()).filter(Boolean);
    const fileHint = lines.find((line) => /saved to .*\.json/i.test(line)) || "";
    const blocks = [];

    if (fileHint) {
      blocks.push({ type: "note", text: fileHint });
    }
    blocks.push({ type: "heading", text: "JSON Summary", level: 1 });
    blocks.push(...buildJsonDisplayBlocks(parsed));
    return blocks;
  } catch {
    return null;
  }
}

function buildJsonDisplayBlocks(value, title = "", depth = 0) {
  const blocks = [];
  const safeLevel = Math.min(depth + 2, 4);

  if (title) {
    blocks.push({ type: "heading", text: humanizeKey(title), level: safeLevel });
  }

  if (Array.isArray(value)) {
    if (!value.length) {
      blocks.push({ type: "text", text: "No items." });
      return blocks;
    }

    if (value.every(isPrimitiveValue)) {
      blocks.push({
        type: "list",
        items: value.slice(0, 12).map((item) => formatDisplayValue(item)),
      });
      if (value.length > 12) {
        blocks.push({ type: "note", text: `Showing first 12 of ${value.length} items.` });
      }
      return blocks;
    }

    if (value.every((item) => isPlainObject(item) && Object.values(item).every(isPrimitiveValue))) {
      const headers = collectObjectKeys(value);
      const rows = value.slice(0, 10).map((item) => headers.map((key) => formatDisplayValue(item[key])));
      blocks.push({
        type: "table",
        headers: headers.map((key) => humanizeKey(key)),
        rows,
      });
      if (value.length > 10) {
        blocks.push({ type: "note", text: `Showing first 10 of ${value.length} rows.` });
      }
      return blocks;
    }

    value.slice(0, 5).forEach((item, index) => {
      blocks.push(...buildJsonDisplayBlocks(item, `Item ${index + 1}`, depth + 1));
    });
    if (value.length > 5) {
      blocks.push({ type: "note", text: `Showing first 5 of ${value.length} items.` });
    }
    return blocks;
  }

  if (isPlainObject(value)) {
    const entries = Object.entries(value);
    const primitiveEntries = entries.filter(([, v]) => isPrimitiveValue(v));
    const complexEntries = entries.filter(([, v]) => !isPrimitiveValue(v));

    if (primitiveEntries.length) {
      blocks.push({
        type: "table",
        headers: ["Field", "Value"],
        rows: primitiveEntries.map(([key, val]) => [humanizeKey(key), formatDisplayValue(val)]),
      });
    }

    if (
      !primitiveEntries.length &&
      entries.length > 0 &&
      entries.every(([, v]) => isPlainObject(v) && Object.values(v).every(isPrimitiveValue))
    ) {
      const rowHeaders = collectObjectKeys(entries.map(([, v]) => v));
      blocks.push({
        type: "table",
        headers: ["Name", ...rowHeaders.map((key) => humanizeKey(key))],
        rows: entries.map(([name, row]) => [
          humanizeKey(name),
          ...rowHeaders.map((key) => formatDisplayValue(row[key])),
        ]),
      });
      return blocks;
    }

    complexEntries.forEach(([key, val]) => {
      blocks.push(...buildJsonDisplayBlocks(val, key, depth + 1));
    });
    return blocks;
  }

  blocks.push({ type: "text", text: formatDisplayValue(value) });
  return blocks;
}

function parseObservationTextBlocks(content) {
  const lines = String(content || "")
    .split("\n")
    .map((line) => line.replace(/\s+$/g, ""));
  const blocks = [];
  let buffer = [];
  let i = 0;

  const flushBuffer = () => {
    const text = buffer.map((line) => line.trim()).filter(Boolean).join("\n").trim();
    if (text) {
      blocks.push({ type: "text", text });
    }
    buffer = [];
  };

  while (i < lines.length) {
    const raw = lines[i];
    const trimmed = raw.trim();

    if (!trimmed) {
      flushBuffer();
      i += 1;
      continue;
    }

    const table = parseWhitespaceTable(lines, i);
    if (table) {
      flushBuffer();
      blocks.push({ type: "table", headers: table.headers, rows: table.rows });
      i = table.nextIndex;
      continue;
    }

    if (isObservationHeading(trimmed)) {
      flushBuffer();
      blocks.push({ type: "heading", text: trimmed, level: 2 });
      i += 1;
      continue;
    }

    if (/saved to .*\.json/i.test(trimmed)) {
      flushBuffer();
      blocks.push({ type: "note", text: trimmed });
      i += 1;
      continue;
    }

    buffer.push(trimmed);
    i += 1;
  }

  flushBuffer();
  return blocks.length ? blocks : [{ type: "text", text: content }];
}

function parseWhitespaceTable(lines, start) {
  const headerCells = splitObservationColumns(lines[start]);
  if (headerCells.length < 3) return null;

  const rows = [];
  let nextIndex = start + 1;
  let maxCols = headerCells.length;

  while (nextIndex < lines.length) {
    const cells = splitObservationColumns(lines[nextIndex]);
    if (cells.length < 3) break;
    rows.push(cells);
    maxCols = Math.max(maxCols, cells.length);
    nextIndex += 1;
  }

  if (rows.length < 2) return null;

  let headers = [...headerCells];
  if (maxCols === headerCells.length + 1 && /^id$/i.test(headerCells[0] || "")) {
    headers = ["id", "symbol", ...headerCells.slice(1)];
  }
  while (headers.length < maxCols) {
    headers.push(`Column ${headers.length + 1}`);
  }

  const normalizedRows = rows.map((cells) => {
    const next = [...cells];
    while (next.length < headers.length) {
      next.push("");
    }
    return next.slice(0, headers.length);
  });

  return {
    headers: headers.map((cell) => humanizeKey(cell)),
    rows: normalizedRows,
    nextIndex,
  };
}

function splitObservationColumns(line) {
  const trimmed = String(line || "").trim();
  if (!trimmed) return [];
  return trimmed.split(/\s{2,}|\t+/).map((cell) => cell.trim()).filter(Boolean);
}

function isObservationHeading(text) {
  const value = String(text || "").trim();
  if (!value) return false;
  if (/^json summary:?$/i.test(value)) return true;
  return /^[A-Z0-9_/'"()\-\s:]{12,}$/.test(value) && !/[a-z]{3,}/.test(value);
}

function isPlainObject(value) {
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}

function isPrimitiveValue(value) {
  return value == null || ["string", "number", "boolean"].includes(typeof value);
}

function collectObjectKeys(items) {
  const seen = [];
  for (const item of items) {
    for (const key of Object.keys(item || {})) {
      if (!seen.includes(key)) {
        seen.push(key);
      }
    }
  }
  return seen;
}

function humanizeKey(value) {
  return String(value || "")
    .replace(/_/g, " ")
    .replace(/\s+/g, " ")
    .trim();
}

function formatDisplayValue(value) {
  if (value == null) return "";
  if (typeof value === "number") {
    if (!Number.isFinite(value)) return String(value);
    if (Math.abs(value) >= 1000) return value.toLocaleString("en-US", { maximumFractionDigits: 4 });
    if (Math.abs(value) > 0 && Math.abs(value) < 0.001) return value.toExponential(3);
    return Number(value.toFixed(6)).toString();
  }
  if (typeof value === "boolean") {
    return value ? "Yes" : "No";
  }
  return String(value);
}
