const state = {
  atlas: null,
  health: null,
  view: "atlas",
  activeTopic: "",
  topicFocus: null,
  selectedPaper: null,
  selectedMarkdown: "",
  searchItems: [],
  searchTotal: 0,
  searchJobId: "",
  searchResult: null,
  searchPollTimer: null,
  searchFollowups: [],
  aiBusy: false,
  atlasChart: null,
  paperChatHistory: {},
};

const $ = (selector) => document.querySelector(selector);

const elements = {
  app: $(".atlas-app"),
  healthLine: $("#healthLine"),
  searchInput: $("#searchInput"),
  searchBtn: $("#searchBtn"),
  mapTitle: $("#mapTitle"),
  atlasChart: $("#atlasChart"),
  atlasSvg: $("#atlasSvg"),
  routeList: $("#routeList"),
  timeline: $("#timeline"),
  explainAtlasBtn: $("#explainAtlasBtn"),
  readingPathBtn: $("#readingPathBtn"),
  lensEyebrow: $("#lensEyebrow"),
  lensTitle: $("#lensTitle"),
  lensBody: $("#lensBody"),
  closePaperBtn: $("#closePaperBtn"),
};

function escapeHtml(value) {
  return String(value ?? "")
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#039;");
}

function debounce(fn, delay = 260) {
  let timer = null;
  return (...args) => {
    window.clearTimeout(timer);
    timer = window.setTimeout(() => fn(...args), delay);
  };
}

async function api(path, options = {}) {
  const response = await fetch(path, {
    ...options,
    headers: {
      "content-type": "application/json",
      ...(options.headers || {}),
    },
  });
  const payload = await response.json();
  if (!response.ok) {
    throw new Error(payload.error || `HTTP ${response.status}`);
  }
  return payload;
}

function loadChartEngine() {
  if (window.echarts) {
    if (state.atlas) renderAtlasMap();
    return;
  }
  if (document.querySelector("script[data-chart-engine='echarts']")) return;

  const script = document.createElement("script");
  script.src = "/static/vendor/echarts.min.js?v=6.1.0";
  script.async = true;
  script.dataset.chartEngine = "echarts";
  script.onload = () => {
    if (state.atlas) renderAtlasMap();
  };
  script.onerror = () => {
    console.warn("ECharts atlas enhancement unavailable; SVG fallback remains active.");
  };
  document.head.appendChild(script);
}

function topicLabel(topic) {
  const found = state.atlas?.topics?.find((item) => item.id === topic);
  return found?.label || topic.replaceAll("-", " ");
}

function colorFor(index) {
  const colors = ["#0b7a75", "#c95745", "#b37a22", "#4d7f52", "#5f63b5", "#22699a", "#9a5d2f", "#7b4aa0"];
  return colors[index % colors.length];
}

function hexToRgb(hex) {
  const clean = String(hex || "").replace("#", "");
  const value = Number.parseInt(clean.length === 3 ? clean.replace(/(.)/g, "$1$1") : clean, 16);
  return {
    r: (value >> 16) & 255,
    g: (value >> 8) & 255,
    b: value & 255,
  };
}

function hexToRgba(hex, alpha) {
  const { r, g, b } = hexToRgb(hex);
  return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}

function mixColor(hex, target, amount) {
  const base = hexToRgb(hex);
  const next = hexToRgb(target);
  const mix = (left, right) => Math.round(left + (right - left) * amount);
  return `rgb(${mix(base.r, next.r)}, ${mix(base.g, next.g)}, ${mix(base.b, next.b)})`;
}

function chartGradient(color) {
  if (!window.echarts?.graphic?.RadialGradient) return color;
  return new window.echarts.graphic.RadialGradient(0.34, 0.28, 0.86, [
    { offset: 0, color: "#ffffff" },
    { offset: 0.58, color: mixColor(color, "#ffffff", 0.64) },
    { offset: 1, color },
  ]);
}

function setReaderMode(enabled) {
  const changed = elements.app.classList.toggle("reader-mode", Boolean(enabled));
  if (changed && state.atlas) {
    window.requestAnimationFrame(() => renderAtlasMap());
  }
}

async function loadHealth() {
  try {
    const health = await api("/api/health");
    state.health = health;
    const modelState = health.model_api_ready
      ? "Model API ready"
      : health.model_api_ok
        ? "Model API mismatch"
        : "Model API offline";
    elements.healthLine.textContent = `${health.paper_count} papers · ${modelState}`;
  } catch (error) {
    elements.healthLine.textContent = error.message;
  }
}

async function loadAtlas() {
  state.atlas = await api("/api/atlas");
  elements.mapTitle.textContent = `${state.atlas.paper_count} 篇论文构成的 Agent 版图`;
  renderAtlasMap();
  renderRoutes();
  renderTimeline();
  renderOverview();
}

function atlasTopics() {
  return state.atlas.topics.filter((topic) => topic.count >= 30).slice(0, 13);
}

function setAtlasRenderer(useChart) {
  if (elements.atlasChart) elements.atlasChart.hidden = !useChart;
  if (elements.atlasSvg) elements.atlasSvg.hidden = useChart;
}

function renderAtlasMap() {
  if (window.echarts && elements.atlasChart) {
    try {
      renderEchartsAtlasMap();
      return;
    } catch (error) {
      console.warn("ECharts atlas failed, falling back to SVG", error);
    }
  }
  renderSvgAtlasMap();
}

function renderEchartsAtlasMap() {
  const isMobile = window.matchMedia("(max-width: 760px)").matches;
  const readerMode = elements.app.classList.contains("reader-mode");
  let topics = atlasTopics();
  if (readerMode && !isMobile) {
    topics = topics.slice(0, 10);
  }
  const topicMap = new Map(topics.map((topic, index) => [topic.id, { ...topic, index }]));
  const maxCount = Math.max(...topics.map((topic) => topic.count), 1);
  setAtlasRenderer(true);

  if (!state.atlasChart) {
    state.atlasChart = window.echarts.init(elements.atlasChart, null, {
      renderer: "canvas",
      useDirtyRect: true,
    });
  }

  const width = elements.atlasChart.clientWidth || (isMobile ? 360 : 780);
  const height = elements.atlasChart.clientHeight || (isMobile ? 360 : 460);
  const isCompact = isMobile || width < 690 || elements.app.classList.contains("reader-mode");
  const padding = isCompact
    ? { left: 46, right: 86, top: 42, bottom: 54 }
    : { left: 92, right: 92, top: 64, bottom: 74 };
  const layoutX = (topic) => (isCompact ? topic.x - Math.max(0, topic.x - 64) * 0.65 : topic.x);
  const xs = topics.map(layoutX);
  const ys = topics.map((topic) => topic.y);
  const minX = Math.min(...xs);
  const maxX = Math.max(...xs);
  const minY = Math.min(...ys);
  const maxY = Math.max(...ys);
  const xSpan = Math.max(maxX - minX, 1);
  const ySpan = Math.max(maxY - minY, 1);
  const plotWidth = Math.max(width - padding.left - padding.right, 260);
  const plotHeight = Math.max(height - padding.top - padding.bottom, 230);
  const sizeBase = isCompact ? (isMobile ? 24 : 36) : 48;
  const sizeRange = isCompact ? (isMobile ? 30 : 42) : 62;

  const nodes = topics.map((topic, index) => {
    const color = colorFor(index);
    const active = topic.id === state.activeTopic;
    const symbolSize = sizeBase + sizeRange * Math.sqrt(topic.count / maxCount);
    return {
      id: topic.id,
      name: topic.label,
      value: topic.count,
      x: padding.left + ((layoutX(topic) - minX) / xSpan) * plotWidth,
      y: padding.top + ((topic.y - minY) / ySpan) * plotHeight,
      symbolSize,
      count: topic.count,
      recent_count: topic.recent_count,
      heat: topic.heat,
      question: topic.question,
      stance: topic.stance,
      itemStyle: {
        color: chartGradient(color),
        borderColor: active ? "#17201c" : mixColor(color, "#101614", 0.08),
        borderWidth: active ? 3 : 1.4,
        shadowBlur: active ? 30 : 18,
        shadowColor: hexToRgba(color, active ? 0.34 : 0.2),
      },
      label: {
        color: active ? "#0f1714" : "#18211d",
      },
    };
  });

  const links = state.atlas.edges
    .filter((edge) => topicMap.has(edge.source) && topicMap.has(edge.target))
    .map((edge) => ({
      source: edge.source,
      target: edge.target,
      value: edge.count,
      lineStyle: {
        width: Math.max(1, 1.2 + edge.strength * 5.4),
        opacity: Math.min(0.58, 0.13 + edge.strength * 0.5),
        color: hexToRgba(colorFor(topicMap.get(edge.source).index), 0.48),
        curveness: 0.08,
      },
    }));

  state.atlasChart.setOption(
    {
      backgroundColor: "transparent",
      animationDuration: 850,
      animationDurationUpdate: 450,
      animationEasing: "cubicOut",
      tooltip: {
        trigger: "item",
        confine: true,
        appendToBody: true,
        backgroundColor: "rgba(252, 253, 249, 0.97)",
        borderColor: "rgba(31, 37, 33, 0.18)",
        borderWidth: 1,
        padding: [10, 12],
        textStyle: {
          color: "#151b18",
          fontSize: 12,
          lineHeight: 18,
        },
        extraCssText: "box-shadow:0 16px 42px rgba(24,31,28,.16);border-radius:8px;",
        formatter(params) {
          if (params.dataType === "edge") {
            return `
              <strong>${escapeHtml(topicLabel(params.data.source))} ↔ ${escapeHtml(topicLabel(params.data.target))}</strong><br>
              共现证据：${escapeHtml(params.data.value)}
            `;
          }
          const item = params.data;
          return `
            <strong>${escapeHtml(item.name)}</strong><br>
            ${escapeHtml(item.count)} papers · recent +${escapeHtml(item.recent_count)}<br>
            <span style="color:#68706b">${escapeHtml(item.question)}</span>
          `;
        },
      },
      series: [
        {
          type: "graph",
          layout: "none",
          data: nodes,
          links,
          roam: true,
          draggable: true,
          center: isCompact ? (isMobile ? ["80%", "50%"] : ["120%", "50%"]) : ["50%", "50%"],
          zoom: isCompact ? (isMobile ? 0.64 : 0.72) : 1,
          nodeScaleRatio: isCompact ? 0.24 : 0.5,
          cursor: "pointer",
          selectedMode: "single",
          scaleLimit: { min: 0.45, max: 3.2 },
          label: {
            show: true,
            position: "inside",
            overflow: "truncate",
            formatter(params) {
              return `{name|${params.data.name}}\n{meta|${params.data.count} · +${params.data.recent_count}}`;
            },
            rich: {
              name: {
                fontSize: isCompact ? (isMobile ? 9 : 10) : 12,
                fontWeight: 850,
                lineHeight: isCompact ? (isMobile ? 12 : 13) : 16,
              },
              meta: {
                color: "#59625d",
                fontSize: isCompact ? (isMobile ? 8 : 9) : 10,
                fontWeight: 700,
                lineHeight: isCompact ? (isMobile ? 10 : 11) : 13,
              },
            },
          },
          lineStyle: {
            color: "source",
            cap: "round",
          },
          emphasis: {
            focus: "adjacency",
            scale: 1.08,
            itemStyle: {
              shadowBlur: 36,
            },
            lineStyle: {
              opacity: 0.82,
              width: 4,
            },
            label: {
              color: "#0b1411",
            },
          },
          blur: {
            itemStyle: {
              opacity: 0.28,
            },
            lineStyle: {
              opacity: 0.06,
            },
            label: {
              opacity: 0.35,
            },
          },
          select: {
            itemStyle: {
              borderColor: "#111714",
              borderWidth: 3,
              shadowBlur: 34,
            },
          },
        },
      ],
    },
    true,
  );

  state.atlasChart.off("click");
  state.atlasChart.on("click", async (params) => {
    if (params.dataType !== "node" || !params.data?.id) return;
    await selectTopic(params.data.id);
    scrollLensIntoView();
  });
}

function renderSvgAtlasMap() {
  const topics = state.atlas.topics.filter((topic) => topic.count >= 30).slice(0, 13);
  const topicMap = new Map(topics.map((topic, index) => [topic.id, { ...topic, index }]));
  const maxCount = Math.max(...topics.map((topic) => topic.count), 1);
  const isMobile = window.matchMedia("(max-width: 760px)").matches;
  setAtlasRenderer(false);
  elements.atlasSvg.setAttribute("viewBox", isMobile ? "16 -4 78 86" : "0 0 170 100");
  const point = (topic) => ({
    x: isMobile ? topic.x : 14 + topic.x * 1.58,
    y: -18 + topic.y * 0.93,
  });

  const edges = state.atlas.edges
    .filter((edge) => topicMap.has(edge.source) && topicMap.has(edge.target))
    .map((edge) => {
      const source = topicMap.get(edge.source);
      const target = topicMap.get(edge.target);
      const sourcePoint = point(source);
      const targetPoint = point(target);
      return `
        <line class="atlas-edge"
          x1="${sourcePoint.x}" y1="${sourcePoint.y}"
          x2="${targetPoint.x}" y2="${targetPoint.y}"
          stroke-width="${Math.max(0.25, 1.8 * edge.strength).toFixed(2)}">
          <title>${escapeHtml(topicLabel(edge.source))} ↔ ${escapeHtml(topicLabel(edge.target))}: ${edge.count}</title>
        </line>
      `;
    })
    .join("");

  const nodes = topics
    .map((topic, index) => {
      const active = topic.id === state.activeTopic ? " active" : "";
      const r = 3.6 + 5.6 * (topic.count / maxCount);
      const ring = r + 1.6 + topic.heat * 1.7;
      const color = colorFor(index);
      const p = point(topic);
      return `
        <g class="atlas-node${active}" data-topic="${escapeHtml(topic.id)}" transform="translate(${p.x.toFixed(2)} ${p.y.toFixed(2)})">
          <circle class="heat-ring" r="${ring.toFixed(2)}" style="stroke:${color}; opacity:${0.2 + topic.heat * 0.65}"></circle>
          <circle r="${r.toFixed(2)}" style="stroke:${color};"></circle>
          <text y="-0.4">${escapeHtml(topic.label)}</text>
          <text class="node-count" y="3.0">${topic.count} · +${topic.recent_count}</text>
        </g>
      `;
    })
    .join("");

  elements.atlasSvg.innerHTML = `${edges}${nodes}`;
}

function renderRoutes() {
  elements.routeList.innerHTML = state.atlas.routes
    .map(
      (route) => `
        <button class="route-item" type="button" data-route="${escapeHtml(route.id)}">
          <strong>${escapeHtml(route.title)}</strong>
          <span>${escapeHtml(route.summary)}</span>
        </button>
      `,
    )
    .join("");
}

function renderTimeline() {
  const rows = state.atlas.timeline.slice(-13);
  const maxTotal = Math.max(...rows.map((row) => row.total), 1);
  elements.timeline.innerHTML = rows
    .map((row) => {
      const height = 8 + 70 * (row.total / maxTotal);
      return `
        <div class="month-cell" title="${escapeHtml(row.month)} · ${row.total}">
          <div class="month-bar" style="height:${height.toFixed(1)}px"></div>
          <div class="month-label">${escapeHtml(row.month.slice(5))}</div>
        </div>
      `;
    })
    .join("");
}

function statGrid(items) {
  return `
    <div class="stat-grid">
      ${items
        .map(
          (item) => `
            <div class="stat">
              <strong>${escapeHtml(item.value)}</strong>
              <span>${escapeHtml(item.label)}</span>
            </div>
          `,
        )
        .join("")}
    </div>
  `;
}

function paperList(items, limit = items.length) {
  if (!items?.length) {
    return `<div class="empty-state">暂无证据</div>`;
  }
  return `
    <div class="paper-list">
      ${items
        .slice(0, limit)
        .map(
          (paper) => `
            <button class="paper-item" type="button" data-paper="${escapeHtml(paper.id)}">
              <div class="paper-title">${escapeHtml(paper.title)}</div>
              <div class="paper-meta">
                <span>${escapeHtml(paper.published_at || paper.year || "unknown")}</span>
                <span>score ${escapeHtml(paper.collection_score)}</span>
                ${(paper.topics || [])
                  .slice(0, 4)
                  .map((topic) => `<span class="mini-badge">${escapeHtml(topicLabel(topic))}</span>`)
                  .join("")}
              </div>
            </button>
          `,
        )
        .join("")}
    </div>
  `;
}

function compactPaperList(items, limit = items.length) {
  return paperList(items, limit);
}

function routeButtons(routes) {
  return `
    <div class="chip-row">
      ${routes
        .map(
          (route) => `
            <button class="pill-button" type="button" data-route="${escapeHtml(route.id)}">${escapeHtml(route.title)}</button>
          `,
        )
        .join("")}
    </div>
  `;
}

function searchActivity(steps = []) {
  const current =
    steps.find((step) => step.status === "running") ||
    [...steps].reverse().find((step) => step.status === "done") ||
    steps[0] ||
    {};
  const doneCount = steps.filter((step) => step.status === "done").length;
  const currentIndex = Math.min(doneCount + (current.status === "running" ? 1 : 0), steps.length || 1);
  const statusLabel = current.status === "running" ? "working" : current.status || "pending";
  return `
    <div class="agent-activity ${escapeHtml(current.status || "pending")}">
      <span class="activity-dot"></span>
      <div class="activity-copy">
        <div class="activity-title">
          <span>Search Agent</span>
          <strong>${escapeHtml(current.label || "准备搜索")}</strong>
        </div>
        <div class="activity-detail">${escapeHtml(current.detail || "正在准备检索上下文")}</div>
      </div>
      <div class="activity-meta">${escapeHtml(statusLabel)} · ${currentIndex}/${steps.length || 1}</div>
    </div>
  `;
}

function searchAggregationView(aggregation) {
  if (!aggregation) return "";
  const topTopics = aggregation.topics || [];
  const recentMonths = aggregation.recent_months || [];
  return `
    <section class="section">
      ${statGrid([
        { value: aggregation.total || 0, label: "matches" },
        { value: topTopics.length, label: "topics" },
        { value: aggregation.date_range?.end || "unknown", label: "latest" },
      ])}
      <div class="mini-chart-row">
        ${recentMonths
          .map((row) => `<span class="month-chip">${escapeHtml(row.month)} · ${escapeHtml(row.count)}</span>`)
          .join("")}
      </div>
      <div class="chip-row search-topic-row">
        ${topTopics
          .slice(0, 8)
          .map(
            (topic) => `
              <button class="pill-button" type="button" data-topic="${escapeHtml(topic.topic)}">
                ${escapeHtml(topic.label)} · ${topic.count}
              </button>
            `,
          )
          .join("")}
      </div>
    </section>
  `;
}

function searchPlanView(plan) {
  if (!plan) return "";
  const keywords = [...(plan.keywords_en || []), ...(plan.keywords_zh || [])].slice(0, 14);
  return `
    <section class="section compact-section">
      <div class="eyebrow">Search Plan</div>
      <p class="statement">${escapeHtml(plan.interpreted_query || "")}</p>
      <p class="microcopy">${escapeHtml(plan.search_rationale || "")}</p>
      <div class="chip-row">
        ${keywords.map((keyword) => `<span class="mini-badge">${escapeHtml(keyword)}</span>`).join("")}
      </div>
    </section>
  `;
}

function renderSearchRunning(job) {
  setReaderMode(false);
  state.view = "search";
  setModeButtons("search");
  elements.lensEyebrow.textContent = "Search Agent";
  elements.lensTitle.textContent = job?.query || elements.searchInput.value.trim() || "搜索";
  elements.lensBody.innerHTML = `
    <section class="section">
      ${searchActivity(job?.steps || [])}
    </section>
  `;
}

function renderSearchHome() {
  setReaderMode(false);
  state.view = "search";
  setModeButtons("search");
  elements.lensEyebrow.textContent = "Search Agent";
  elements.lensTitle.textContent = "智能搜索";
  elements.lensBody.innerHTML = `
    <section class="section">
      <p class="statement">输入主题、论文线索或研究问题。Search Agent 会理解意图、扩展中英文关键词、检索本地论文库，并生成一份简短结论。</p>
    </section>
    <section class="section">
      <h3>可以这样搜</h3>
      <div class="chip-row">
        <button class="pill-button" type="button" data-search-example="强化学习">强化学习</button>
        <button class="pill-button" type="button" data-search-example="最近的 agent evaluation benchmark">最近的 agent evaluation benchmark</button>
        <button class="pill-button" type="button" data-search-example="长期记忆和 RAG 有什么关系">长期记忆和 RAG 有什么关系</button>
      </div>
    </section>
  `;
}

function renderSearchError(jobOrError) {
  const message = typeof jobOrError === "string" ? jobOrError : jobOrError?.error || "search failed";
  elements.lensBody.innerHTML = `
    <section class="section">
      <div class="empty-state">${escapeHtml(message)}</div>
    </section>
  `;
}

function followupList() {
  const rows = state.searchFollowups || [];
  if (!rows.length) return "";
  return `
    <div class="dialogue-list">
      ${rows
        .map(
          (row) => `
            <div class="dialogue-row ${escapeHtml(row.role)}">
              <strong>${row.role === "user" ? "你" : "Search Agent"}</strong>
              <div>${escapeHtml(row.content)}</div>
            </div>
          `,
        )
        .join("")}
    </div>
  `;
}

function renderSearchResult(result) {
  setReaderMode(false);
  state.view = "search";
  setModeButtons("search");
  elements.lensEyebrow.textContent = "Search Agent";
  elements.lensTitle.textContent = result.query;
  const summary = result.summary || {};
  elements.lensBody.innerHTML = `
    <section class="section">
      <div class="ai-box search-brief">
        <h3>${escapeHtml(summary.model || "Search Agent")}</h3>
        <div class="ai-output">${escapeHtml(summary.text || "")}</div>
      </div>
    </section>
    ${searchAggregationView(result.aggregation)}
    ${searchPlanView(result.plan)}
    <section class="section">
      <h3>继续追问</h3>
      ${followupList()}
      <div class="followup-row">
        <input id="searchFollowupInput" type="text" placeholder="基于当前结果继续问，比如：只看最近的 / 给阅读顺序" />
        <button class="tool-button primary" type="button" data-search-followup>Ask</button>
      </div>
    </section>
    <section class="section">
      <h3>相关论文</h3>
      ${compactPaperList(result.items || [], 60)}
    </section>
  `;
}

function renderOverview(aiHtml = "") {
  setReaderMode(false);
  state.view = "atlas";
  state.selectedPaper = null;
  elements.lensEyebrow.textContent = "Atlas";
  elements.lensTitle.textContent = "整体脉络";
  setModeButtons("atlas");

  const topTopics = state.atlas.topics.slice(0, 7);
  elements.lensBody.innerHTML = `
    <section class="section">
      ${statGrid([
        { value: state.atlas.paper_count, label: "papers" },
        { value: state.atlas.topic_count, label: "topics" },
        { value: state.atlas.recent_months.join(" / "), label: "recent window" },
      ])}
    </section>

    <section class="section">
      <p class="statement">这版 Atlas 把论文当作证据，把主题、路线和趋势当作入口。当前最强的主轴是 evaluation、tool use、RAG、reasoning、planning、safety、memory。</p>
      <p class="question">核心问题：Agent 如何从“能回答”走向“能长期、可观察、可治理地行动”？</p>
    </section>

    <section class="section">
      <h3>主题入口</h3>
      <div class="chip-row">
        ${topTopics
          .map(
            (topic) => `
              <button class="pill-button" type="button" data-topic="${escapeHtml(topic.id)}">
                ${escapeHtml(topic.label)} · ${topic.count}
              </button>
            `,
          )
          .join("")}
      </div>
    </section>

    <section class="section">
      <h3>路线</h3>
      ${routeButtons(state.atlas.routes)}
    </section>

    <section class="section">
      <h3>关键论文候选</h3>
      ${paperList(state.atlas.key_papers, 8)}
    </section>

    <section class="section">
      <h3>AI 研究编辑</h3>
      <div class="tool-row">
        <button class="tool-button primary" type="button" data-atlas-ai="atlas">解释地图</button>
        <button class="tool-button" type="button" data-atlas-ai="path">阅读路径</button>
      </div>
      <div id="aiResult" class="ai-box" style="${aiHtml ? "" : "display:none"}">${aiHtml}</div>
    </section>
  `;
}

async function selectTopic(topic) {
  state.activeTopic = topic;
  state.selectedPaper = null;
  renderAtlasMap();
  const focus = await api(`/api/topic?topic=${encodeURIComponent(topic)}`);
  state.topicFocus = focus;
  renderTopic(focus);
}

function renderTopic(focus, aiHtml = "") {
  setReaderMode(false);
  state.view = "atlas";
  elements.lensEyebrow.textContent = "Topic";
  elements.lensTitle.textContent = focus.label;
  setModeButtons("atlas");

  const recentTotal = Object.values(focus.months).slice(-3).reduce((sum, value) => sum + Number(value || 0), 0);
  elements.lensBody.innerHTML = `
    <section class="section">
      ${statGrid([
        { value: focus.count, label: "papers" },
        { value: recentTotal, label: "recent" },
        { value: focus.neighbors.length, label: "neighbors" },
      ])}
    </section>

    <section class="section">
      <p class="statement">${escapeHtml(focus.stance)}</p>
      <p class="question">${escapeHtml(focus.question)}</p>
    </section>

    <section class="section">
      <h3>相邻主题</h3>
      <div class="chip-row">
        ${focus.neighbors
          .slice(0, 8)
          .map(
            (item) => `
              <button class="pill-button" type="button" data-topic="${escapeHtml(item.topic)}">
                ${escapeHtml(item.label)} · ${item.count}
              </button>
            `,
          )
          .join("")}
      </div>
    </section>

    <section class="section">
      <h3>所在路线</h3>
      ${routeButtons(focus.routes)}
    </section>

    <section class="section">
      <h3>AI 研究编辑</h3>
      <div class="tool-row">
        <button class="tool-button primary" type="button" data-topic-ai="topic">生成主题导读</button>
        <button class="tool-button" type="button" data-topic-ai="path">生成阅读路径</button>
        ${
          focus.neighbors[0]
            ? `<button class="tool-button" type="button" data-compare-topic="${escapeHtml(focus.neighbors[0].topic)}">对比 ${escapeHtml(focus.neighbors[0].label)}</button>`
            : ""
        }
      </div>
      <div id="aiResult" class="ai-box" style="${aiHtml ? "" : "display:none"}">${aiHtml}</div>
    </section>

    <section class="section">
      <h3>关键论文</h3>
      ${paperList(focus.key_papers, 10)}
    </section>

    <section class="section">
      <h3>最近新增</h3>
      ${paperList(focus.recent_papers, 8)}
    </section>
  `;
}

function renderRoute(routeId) {
  const route = state.atlas.routes.find((item) => item.id === routeId);
  if (!route) return;
  setReaderMode(false);
  state.selectedPaper = null;
  elements.lensEyebrow.textContent = "Route";
  elements.lensTitle.textContent = route.title;
  elements.lensBody.innerHTML = `
    <section class="section">
      <p class="statement">${escapeHtml(route.summary)}</p>
    </section>
    <section class="section">
      <h3>主题链路</h3>
      <div class="chip-row">
        ${route.topics
          .map((topic) => `<button class="pill-button" type="button" data-topic="${escapeHtml(topic)}">${escapeHtml(topicLabel(topic))}</button>`)
          .join("")}
      </div>
    </section>
    <section class="section">
      <h3>路线证据</h3>
      ${paperList(
        state.atlas.key_papers.filter((paper) => paper.topics.some((topic) => route.topics.includes(topic))),
        12,
      )}
    </section>
  `;
}

async function runSearch() {
  const q = elements.searchInput.value.trim();
  if (!q) {
    renderSearchHome();
    return;
  }
  const scopeTopic = state.topicFocus && state.view === "atlas" ? state.activeTopic : "";
  setReaderMode(false);
  state.view = "search";
  setModeButtons("search");
  state.searchFollowups = [];
  state.searchResult = null;
  const starter = {
    query: q,
    steps: [
      { label: "理解问题", status: "running", detail: "准备启动 Search Agent" },
      { label: "扩展关键词", status: "pending", detail: "" },
      { label: "检索论文库", status: "pending", detail: "" },
      { label: "统计主题/趋势", status: "pending", detail: "" },
      { label: "生成搜索结论", status: "pending", detail: "" },
    ],
  };
  renderSearchRunning(starter);
  scrollLensIntoView();
  try {
    const payload = await api("/api/search/agent", {
      method: "POST",
      body: JSON.stringify({ query: q, topic: scopeTopic }),
    });
    state.searchJobId = payload.job.id;
    renderSearchRunning(payload.job);
    pollSearchJob(payload.job.id);
  } catch (error) {
    renderSearchError(error.message);
  }
}

async function pollSearchJob(jobId) {
  window.clearTimeout(state.searchPollTimer);
  try {
    const payload = await api(`/api/search/agent?id=${encodeURIComponent(jobId)}`);
    const job = payload.job;
    if (job.status === "done") {
      state.searchResult = job.result;
      state.searchItems = job.result.items || [];
      state.searchTotal = job.result.total || 0;
      renderSearchResult(job.result);
      return;
    }
    if (job.status === "error") {
      renderSearchError(job);
      return;
    }
    renderSearchRunning(job);
    state.searchPollTimer = window.setTimeout(() => pollSearchJob(jobId), 900);
  } catch (error) {
    renderSearchError(error.message);
  }
}

async function runSearchFollowup() {
  const input = $("#searchFollowupInput");
  const question = input?.value.trim();
  if (!question || !state.searchJobId) return;
  state.searchFollowups.push({ role: "user", content: question });
  state.searchFollowups.push({ role: "assistant", content: "thinking..." });
  renderSearchResult(state.searchResult);
  try {
    const payload = await api("/api/search/followup", {
      method: "POST",
      body: JSON.stringify({ job_id: state.searchJobId, question }),
    });
    state.searchFollowups[state.searchFollowups.length - 1] = {
      role: "assistant",
      content: `${payload.answer}\n\n${payload.model} · ${payload.duration_seconds}s`,
    };
  } catch (error) {
    state.searchFollowups[state.searchFollowups.length - 1] = {
      role: "assistant",
      content: error.message,
    };
  }
  renderSearchResult(state.searchResult);
}

function renderReading() {
  setReaderMode(false);
  state.view = "reading";
  setModeButtons("reading");
  elements.lensEyebrow.textContent = "Reading";
  elements.lensTitle.textContent = "阅读流";
  elements.lensBody.innerHTML = `
    <section class="section">
      <p class="statement">先从路线理解问题，再进入主题，最后把论文作为证据读。当前版本把高分论文和最近新增放在这里，后续可以接入个人阅读队列。</p>
    </section>
    <section class="section">
      <h3>高优先级</h3>
      ${paperList(state.atlas.key_papers, 12)}
    </section>
    <section class="section">
      <h3>最近新增</h3>
      ${paperList(state.atlas.recent_papers, 12)}
    </section>
  `;
}

function stripFrontmatter(markdown) {
  const text = String(markdown || "");
  const match = text.match(/^# .+?\n\n---[\s\S]*?\n---\n\n?/);
  return match ? text.slice(match[0].length) : text;
}

function renderMarkdown(markdown) {
  return escapeHtml(stripFrontmatter(markdown))
    .replace(/^### (.+)$/gm, "\n$1\n")
    .replace(/^## (.+)$/gm, "\n$1\n")
    .replace(/^# (.+)$/gm, "$1\n")
    .replace(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, "$1 ($2)");
}

async function selectPaper(id) {
  const payload = await api(`/api/paper?id=${encodeURIComponent(id)}`);
  state.selectedPaper = payload.paper;
  state.selectedMarkdown = payload.markdown;
  renderPaperReader();
}

function renderPaperReader(aiHtml = "", abstractHtml = "") {
  const paper = state.selectedPaper;
  if (!paper) return;
  setReaderMode(true);
  const chatRows = state.paperChatHistory[paper.id] || [];
  elements.lensEyebrow.textContent = "Paper";
  elements.lensTitle.textContent = "研读";
  elements.lensBody.innerHTML = `
    <section class="section">
      <h3 class="reader-title">${escapeHtml(paper.title)}</h3>
      <div class="paper-meta">
        <span>${escapeHtml(paper.published_at || paper.year)}</span>
        <span>${escapeHtml(paper.venue || paper.source)}</span>
        <span>score ${escapeHtml(paper.collection_score)}</span>
      </div>
      <div class="chip-row" style="margin-top:10px">
        ${(paper.topics || []).map((topic) => `<button class="pill-button" type="button" data-topic="${escapeHtml(topic)}">${escapeHtml(topicLabel(topic))}</button>`).join("")}
      </div>
    </section>

    <section class="section">
      <div class="tool-row">
        <a class="paper-link" href="${escapeHtml(paper.url)}" target="_blank" rel="noreferrer">arXiv</a>
        <button class="tool-button" type="button" data-reader-action="map">回到图谱</button>
        <button class="tool-button" type="button" data-paper-action="abstract">摘要</button>
        <button class="tool-button primary" type="button" data-paper-action="summary">中文摘要</button>
        <button class="tool-button" type="button" data-paper-action="translate">翻译</button>
        <button class="tool-button" type="button" data-paper-action="deep">深度研读</button>
      </div>
    </section>

    <section class="section" id="paperAbstract" style="${abstractHtml ? "" : "display:none"}">${abstractHtml}</section>
    <section class="section" id="paperAi" style="${aiHtml ? "" : "display:none"}">${aiHtml}</section>

    <section class="section">
      <h3>继续研读</h3>
      <div class="dialogue-list paper-chat-list">
        ${chatRows
          .map(
            (row) => `
              <div class="dialogue-row ${escapeHtml(row.role)}">
                <strong>${row.role === "user" ? "你" : "Paper Agent"}</strong>
                <div>${escapeHtml(row.content)}</div>
              </div>
            `,
          )
          .join("")}
      </div>
      <div class="followup-row">
        <input id="paperChatInput" type="text" placeholder="继续问这篇论文，比如：实验设计有什么问题？和工程实践有什么关系？" />
        <button class="tool-button primary" type="button" data-paper-chat>Ask</button>
      </div>
    </section>

    <section class="section">
      <h3>本地卡片</h3>
      <div class="reader-box markdown-preview">${renderMarkdown(state.selectedMarkdown)}</div>
    </section>
  `;
}

async function loadAbstract() {
  if (!state.selectedPaper) return;
  setAiBusy(true, "摘要获取中");
  try {
    const payload = await api(`/api/paper/abstract?id=${encodeURIComponent(state.selectedPaper.id)}`);
    const abstract = payload.abstract;
    const html = abstract
      ? `<h3>arXiv Abstract</h3><div class="abstract-text">${escapeHtml(abstract.abstract)}</div>`
      : `<div class="empty-state">${escapeHtml(payload.message || "No abstract")}</div>`;
    renderPaperReader("", html);
  } catch (error) {
    renderPaperReader("", `<div class="empty-state">${escapeHtml(error.message)}</div>`);
  } finally {
    setAiBusy(false);
  }
}

async function runPaperChat() {
  if (!state.selectedPaper) return;
  const input = $("#paperChatInput");
  const question = input?.value.trim();
  if (!question) return;
  const paperId = state.selectedPaper.id;
  const history = state.paperChatHistory[paperId] || [];
  history.push({ role: "user", content: question });
  history.push({ role: "assistant", content: "thinking..." });
  state.paperChatHistory[paperId] = history;
  const aiHtml = $("#paperAi")?.innerHTML || "";
  const abstractHtml = $("#paperAbstract")?.innerHTML || "";
  renderPaperReader(aiHtml, abstractHtml);
  try {
    const payload = await api("/api/paper/chat", {
      method: "POST",
      body: JSON.stringify({
        id: paperId,
        question,
        history: history.slice(0, -1),
      }),
    });
    history[history.length - 1] = {
      role: "assistant",
      content: `${payload.answer}\n\n${payload.model} · ${payload.duration_seconds}s`,
    };
  } catch (error) {
    history[history.length - 1] = { role: "assistant", content: error.message };
  }
  state.paperChatHistory[paperId] = history;
  renderPaperReader($("#paperAi")?.innerHTML || aiHtml, $("#paperAbstract")?.innerHTML || abstractHtml);
}

function setAiBusy(isBusy, label = "") {
  state.aiBusy = isBusy;
  document.querySelectorAll(".tool-button, .quiet-button").forEach((button) => {
    button.disabled = isBusy;
  });
  if (isBusy) {
    const box = $("#aiResult") || $("#paperAi");
    if (box) {
      box.style.display = "";
      box.innerHTML = `<div class="empty-state">${escapeHtml(label)}</div>`;
    }
  }
}

function isLargeModel(model) {
  const configuredLarge = state.health?.default_models?.deep;
  return model === configuredLarge || /:(large|sol)$/i.test(model);
}

async function runPaperAi(mode) {
  if (!state.selectedPaper) return;
  const model = state.health?.default_models?.[mode] || (mode === "translate" ? "ChatGPT-5.6:Luna" : "ChatGPT-5.6:Terra");
  if (isLargeModel(model) && !window.confirm("将调用 large 模型，占用更多 GPU。继续？")) return;
  setAiBusy(true, `${mode} · ${model}`);
  try {
    const payload = await api("/api/ai", {
      method: "POST",
      body: JSON.stringify({ id: state.selectedPaper.id, mode, model }),
    });
    const cache = payload.cached ? "cached" : `${payload.duration_seconds}s`;
    const html = `<h3>${escapeHtml(payload.model)} · ${escapeHtml(cache)}</h3><div class="ai-output">${escapeHtml(payload.response)}</div>`;
    renderPaperReader(html, $("#paperAbstract")?.innerHTML || "");
  } catch (error) {
    renderPaperReader(`<div class="empty-state">${escapeHtml(error.message)}</div>`, $("#paperAbstract")?.innerHTML || "");
  } finally {
    setAiBusy(false);
  }
}

async function runAtlasAi(mode, topic = "", topicB = "") {
  const model = state.health?.default_models?.[mode] || "ChatGPT-5.6:Terra";
  if (isLargeModel(model) && !window.confirm("将调用 large 模型做高维综合，占用更多 GPU。继续？")) return;
  setAiBusy(true, `${mode} · ${model}`);
  try {
    const payload = await api("/api/atlas/ai", {
      method: "POST",
      body: JSON.stringify({ mode, topic, topic_b: topicB, model }),
    });
    const cache = payload.cached ? "cached" : `${payload.duration_seconds}s`;
    const html = `<h3>${escapeHtml(payload.model)} · ${escapeHtml(cache)}</h3><div class="ai-output">${escapeHtml(payload.response)}</div>`;
    if (mode === "topic" || (mode === "path" && state.topicFocus)) {
      renderTopic(state.topicFocus, html);
    } else {
      renderOverview(html);
    }
  } catch (error) {
    const html = `<div class="empty-state">${escapeHtml(error.message)}</div>`;
    if (state.topicFocus) renderTopic(state.topicFocus, html);
    else renderOverview(html);
  } finally {
    setAiBusy(false);
  }
}

function setModeButtons(view) {
  document.querySelectorAll("[data-view]").forEach((button) => {
    button.classList.toggle("active", button.dataset.view === view);
  });
}

function scrollLensIntoView() {
  if (window.matchMedia("(max-width: 760px)").matches) {
    document.querySelector(".lens-panel").scrollIntoView({ behavior: "smooth", block: "start" });
  }
}

function bindEvents() {
  elements.atlasSvg.addEventListener("click", async (event) => {
    const node = event.target.closest("[data-topic]");
    if (!node) return;
    await selectTopic(node.dataset.topic);
    scrollLensIntoView();
  });

  elements.routeList.addEventListener("click", (event) => {
    const button = event.target.closest("[data-route]");
    if (button) {
      renderRoute(button.dataset.route);
      scrollLensIntoView();
    }
  });

  elements.lensBody.addEventListener("click", async (event) => {
    const topicButton = event.target.closest("[data-topic]");
    const routeButton = event.target.closest("[data-route]");
    const paperButton = event.target.closest("[data-paper]");
    const atlasAi = event.target.closest("[data-atlas-ai]");
    const topicAi = event.target.closest("[data-topic-ai]");
    const compareButton = event.target.closest("[data-compare-topic]");
    const paperAction = event.target.closest("[data-paper-action]");
    const readerAction = event.target.closest("[data-reader-action]");
    const searchFollowup = event.target.closest("[data-search-followup]");
    const paperChat = event.target.closest("[data-paper-chat]");
    const searchExample = event.target.closest("[data-search-example]");

    if (topicButton) {
      await selectTopic(topicButton.dataset.topic);
      scrollLensIntoView();
    } else if (routeButton) {
      renderRoute(routeButton.dataset.route);
    } else if (paperButton) {
      await selectPaper(paperButton.dataset.paper);
      scrollLensIntoView();
    } else if (atlasAi) {
      await runAtlasAi(atlasAi.dataset.atlasAi, state.activeTopic);
    } else if (topicAi) {
      await runAtlasAi(topicAi.dataset.topicAi, state.activeTopic);
    } else if (compareButton) {
      await runAtlasAi("compare", state.activeTopic, compareButton.dataset.compareTopic);
    } else if (paperAction) {
      const action = paperAction.dataset.paperAction;
      if (action === "abstract") await loadAbstract();
      else await runPaperAi(action);
    } else if (readerAction) {
      if (state.topicFocus) renderTopic(state.topicFocus);
      else renderOverview();
    } else if (searchFollowup) {
      await runSearchFollowup();
    } else if (paperChat) {
      await runPaperChat();
    } else if (searchExample) {
      elements.searchInput.value = searchExample.dataset.searchExample || "";
      await runSearch();
    }
  });
  elements.lensBody.addEventListener("keydown", async (event) => {
    if (event.key === "Enter" && event.target.closest("#searchFollowupInput")) {
      await runSearchFollowup();
    }
    if (event.key === "Enter" && event.target.closest("#paperChatInput")) {
      await runPaperChat();
    }
  });

  elements.searchBtn.addEventListener("click", runSearch);
  elements.searchInput.addEventListener("keydown", (event) => {
    if (event.key === "Enter") runSearch();
  });

  elements.explainAtlasBtn.addEventListener("click", () => runAtlasAi("atlas", state.activeTopic));
  elements.readingPathBtn.addEventListener("click", () => runAtlasAi("path", state.activeTopic));
  elements.closePaperBtn.addEventListener("click", () => {
    if (state.topicFocus) renderTopic(state.topicFocus);
    else renderOverview();
  });

  document.querySelectorAll("[data-view]").forEach((button) => {
    button.addEventListener("click", async () => {
      const view = button.dataset.view;
      if (view === "atlas") {
        state.activeTopic ? await selectTopic(state.activeTopic) : renderOverview();
      } else if (view === "search") {
        await runSearch();
      } else {
        renderReading();
      }
      scrollLensIntoView();
    });
  });

  window.addEventListener(
    "resize",
    debounce(() => {
      if (state.atlas) renderAtlasMap();
    }, 150),
  );
}

async function boot() {
  bindEvents();
  await loadHealth();
  await loadAtlas();
  loadChartEngine();
}

boot();
