// api-docs-app.jsx —— PR-S.2 API Docs:第 6 个 console page。
//
// Layout:topbar + 左 TOC 栏(260px,sticky)+ 右滚动 article。
// 数据:GET /v1/docs/agent-api.md → marked(CDN)→ HTML 注入。
//   TOC 我们自己从 markdown 源里 grep `## ` / `### ` 出标题 + 同步 marked
//   生成的锚 id(marked 默认 slugify 跟 GitHub 一致:小写 + 非字母连字符
//   归为 `-`)。点 TOC item 跳锚,scroll-spy 高亮当前可视的 h2。
//
// 不引 highlight.js —— 设计稿 / 仓库整体偏 quiet,代码块 mono + 深 bg 即可,
// 不上着色器减体积。

const { useState: adUseState, useEffect: adUseEffect, useMemo: adUseMemo, useRef: adUseRef } = React;

function createRequestId() {
  if (window.crypto?.randomUUID) return window.crypto.randomUUID();
  return `rid-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
}

// ----------------------------------------------------------------------------
// TOC extraction
// ----------------------------------------------------------------------------

// Slugify a heading the same way marked default renderer does, so our TOC
// anchors line up with the ids it injects. marked's default slugger:
//   lowercase → strip non-[a-z0-9 -] → spaces → "-"
// We add a counter for collisions (marked does too).
function slugify(text, seen) {
  const base = text
    .toLowerCase()
    .replace(/[^\w一-龥\s-]/g, "")
    .trim()
    .replace(/\s+/g, "-");
  let slug = base || "section";
  let n = 1;
  while (seen.has(slug)) {
    n += 1;
    slug = `${base}-${n}`;
  }
  seen.add(slug);
  return slug;
}

// Scan markdown source for `## ` / `### ` headings + slug; skip h1.
function extractToc(md) {
  if (!md) return [];
  const seen = new Set();
  const out = [];
  // Walk line-by-line but ignore lines inside fenced code blocks.
  let inFence = false;
  for (const raw of md.split("\n")) {
    const line = raw.replace(/\r$/, "");
    if (/^```/.test(line)) {
      inFence = !inFence;
      continue;
    }
    if (inFence) continue;
    const m = /^(#{1,3})\s+(.+?)\s*$/.exec(line);
    if (!m) continue;
    const level = m[1].length;
    if (level === 1) continue;             // skip h1, it's the page title
    const text = m[2].trim();
    out.push({ level, text, id: slugify(text, seen) });
  }
  return out;
}

// ----------------------------------------------------------------------------
// Markdown renderer
// ----------------------------------------------------------------------------

// Configure marked once.
//
// BUGFIX 三连(api-docs TOC 点击无效)—— 真正的 root cause 是 marked@12 的
// renderer 签名变了:`renderer.heading` 现在收单个 token 对象,不再是老的
// `(text, level)`。旧自定义 renderer 把 token 当字符串 `String(token)` →
// "[object Object]",`<h${level}>` → `<hundefined>`,id 全错 → TOC slug
// 永远对不上 DOM(连 querySelector("h2[id]") fuzzy 兜底都失效,因为 DOM
// 里压根没有 h2/h3,只有坏掉的 hundefined)。
//
// 修法:**彻底不靠 marked 给 heading 加 id**。renderer 不 override,让
// marked 出干净的 `<h2>…</h2>`;渲染进 DOM 后,用 `assignHeadingIds` 走一遍
// 真实 heading 元素,用跟 TOC 完全相同的 slugify + 相同序列打 id。两边由
// 同一函数同一输入产出 → 字节级对齐,且跟 marked 版本解耦。
let _markedReady = false;
function ensureMarked() {
  if (_markedReady || !window.marked) return _markedReady;
  window.marked.use({ gfm: true, breaks: false });
  _markedReady = true;
  return true;
}

// 渲染后给真实 DOM heading 打 id + § 锚。处理 h2/h3(跟 extractToc 同口径,
// h1 是页标题不进 TOC),fresh seen Set 保证序列跟 TOC 一致。
function assignHeadingIds(root) {
  if (!root) return;
  const seen = new Set();
  for (const el of root.querySelectorAll("h2, h3")) {
    const plain = (el.textContent || "").replace(/§\s*$/, "").trim();
    const id = slugify(plain, seen);
    el.id = id;
    if (!el.querySelector("a.anchor")) {
      const a = document.createElement("a");
      a.className = "anchor";
      a.href = `#${id}`;
      a.setAttribute("aria-label", "anchor");
      a.textContent = "§";
      el.appendChild(document.createTextNode(" "));
      el.appendChild(a);
    }
  }
}

function renderMarkdown(md) {
  if (!md) return "";
  if (!window.marked) return "<p>marked.js failed to load.</p>";
  ensureMarked();
  // id 不在 parse 阶段打 —— 渲染进 DOM 后 assignHeadingIds 统一处理。
  return window.marked.parse(md);
}

// ----------------------------------------------------------------------------
// App
// ----------------------------------------------------------------------------

function ApiDocsApp() {
  const [md, setMd] = adUseState(null);
  const [error, setError] = adUseState(null);
  const [activeId, setActiveId] = adUseState(null);
  const articleRef = adUseRef(null);
  const selectedDoc = new URLSearchParams(location.search).get("doc") === "apps" ? "apps" : "agent";
  const docPath = selectedDoc === "apps" ? "/v1/docs/apps-api.md" : "/v1/docs/agent-api.md";
  // PR-S.17d · topbar 上 Users 入口 admin-gate
  const [isAdmin, setIsAdmin] = adUseState(false);
  adUseEffect(() => {
    window.api.authMe()
      .then((m) => setIsAdmin(!!m.is_admin))
      .catch(() => {});
  }, []);

  // Load the markdown source. We don't go through `window.api.call` because
  // the endpoint returns text/markdown (not JSON) and is public-read inside
  // an authenticated session — call() would still work but its JSON parse
  // would fight us.
  adUseEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const token = localStorage.getItem("baizhi.auth.token");
        if (!token) {
          location.replace("/login");
          return;
        }
        const resp = await fetch(docPath, {
          headers: { "Authorization": `Bearer ${token}`, "X-Request-ID": createRequestId() },
        });
        if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
        const text = await resp.text();
        if (!cancelled) setMd(text);
      } catch (e) {
        if (!cancelled) setError(e?.message || String(e));
      }
    })();
    return () => { cancelled = true; };
  }, [docPath]);

  const toc = adUseMemo(() => extractToc(md), [md]);
  const html = adUseMemo(() => renderMarkdown(md), [md]);

  // 渲染进 DOM 后第一件事:给真实 heading 打 id + § 锚。必须在 deep-link /
  // scroll-spy 之前(它们靠 getElementById / querySelectorAll("h2[id]"))。
  // 同 deps 的 effect 按声明顺序执行,所以放最前面。
  adUseEffect(() => {
    if (!html) return;
    assignHeadingIds(articleRef.current);
  }, [html]);

  // After render: deep-link to whatever hash the URL came in with.
  adUseEffect(() => {
    if (!html) return;
    if (location.hash) {
      const id = decodeURIComponent(location.hash.slice(1));
      const el = document.getElementById(id);
      if (el) requestAnimationFrame(() => el.scrollIntoView({ behavior: "auto", block: "start" }));
    }
  }, [html]);

  // BUGFIX(TOC 点击无效)第二层:不依赖浏览器原生 fragment 导航 ——
  // body overflow:hidden + 内部滚动容器布局下原生 #hash 跳转不可靠,
  // 且 TOC slug(markdown 源)跟 marked slug(渲染后 HTML)对 escape 的
  // 处理可能有出入。点击拦截:先 getElementById,找不到按 heading 文本
  // 模糊匹配兜底,然后手动 scrollIntoView(article 是真正的滚动容器)。
  const scrollToHeading = (it) => {
    const root = articleRef.current;
    if (!root) return;
    let el = document.getElementById(it.id);
    if (!el) {
      const want = it.text.replace(/[`*_]/g, "").trim();
      el = [...root.querySelectorAll("h2[id], h3[id]")].find((h) => {
        const plain = (h.textContent || "").replace(/§\s*$/, "").trim();
        return plain === want || plain.startsWith(want) || want.startsWith(plain);
      }) || null;
    }
    if (!el) return;
    el.scrollIntoView({ behavior: "smooth", block: "start" });
    // 同步 URL hash(可分享 deep-link),不触发原生跳转
    history.replaceState(null, "", `#${el.id}`);
    setActiveId(el.id);
  };

  // Scroll-spy: which h2/h3 is currently topmost in the visible area? Use a
  // single IntersectionObserver instead of polling scrollTop on every frame.
  adUseEffect(() => {
    if (!html || !articleRef.current) return;
    const root = articleRef.current;
    const headings = root.querySelectorAll("h2[id], h3[id]");
    if (!headings.length) return;
    // Pick the heading whose top is closest to ~80px below the viewport top.
    const visible = new Map();
    const obs = new IntersectionObserver(
      (entries) => {
        for (const e of entries) {
          if (e.isIntersecting) visible.set(e.target.id, e.intersectionRatio);
          else visible.delete(e.target.id);
        }
        // Of the currently-intersecting headings, pick the one earliest in DOM.
        let pick = null;
        for (const h of headings) {
          if (visible.has(h.id)) { pick = h.id; break; }
        }
        if (pick) setActiveId(pick);
      },
      { root, rootMargin: "-12% 0px -75% 0px", threshold: 0 },
    );
    headings.forEach((h) => obs.observe(h));
    return () => obs.disconnect();
  }, [html]);

  // ---- topbar nav ----
  const navStyle = {
    padding: "6px 10px", borderRadius: 6,
    color: "var(--text-muted)", textDecoration: "none",
    fontSize: 13, fontWeight: 500,
  };

  return (
    <div className="app api-docs-app">
      <div className="topbar">
        <div className="brand">
          baizhi
          <span style={{ color: "var(--text-faint)" }}>/</span>
          <span style={{ fontWeight: 500, color: "var(--text-muted)" }}>API Docs</span>
        </div>
        <div className="tabs">
          <a href="/playground" style={navStyle}>Playground</a>
          <a href="/manage" style={navStyle}>Manage</a>
          <a href="/evals" style={navStyle}>Evals</a>
          <a href="/traces" style={navStyle}>Traces</a>
          <a href="/marketplace" style={navStyle}>Marketplace</a>
          <a href="/apps" style={navStyle}>Apps</a>
          {isAdmin && <a href="/users" style={navStyle}>Users</a>}
          <div className="api-docs-nav-menu">
            <button className="active" aria-haspopup="menu">
              API Docs <span className="api-docs-nav-caret" aria-hidden="true">⌄</span>
            </button>
            <div className="api-docs-nav-dropdown" role="menu">
              <a href="/api-docs?doc=agent" className={selectedDoc === "agent" ? "selected" : ""} role="menuitem">Agent API</a>
              <a href="/api-docs?doc=apps" className={selectedDoc === "apps" ? "selected" : ""} role="menuitem">Apps API</a>
            </div>
          </div>
        </div>
        <div className="spacer" />
        <div className="actions">
          <a
            href={docPath}
            target="_blank"
            rel="noopener noreferrer"
            className="btn ghost sm"
            style={{ textDecoration: "none" }}
            title="Open the raw markdown source"
          >
            <Icon name="code" size={12} /> View&nbsp;raw
          </a>
        </div>
      </div>

      <div className="api-docs-shell">
        <aside className="api-docs-toc">
          <h3>On this page</h3>
          {toc.length === 0 ? (
            <div style={{ color: "var(--text-faint)", fontSize: 12 }}>
              {error ? "—" : "Loading…"}
            </div>
          ) : (
            <ul>
              {toc.map((it) => (
                <li key={it.id}>
                  <a
                    href={`#${it.id}`}
                    onClick={(e) => { e.preventDefault(); scrollToHeading(it); }}
                    className={
                      (it.level === 3 ? "lvl-3 " : "") +
                      (activeId === it.id ? "active" : "")
                    }
                  >
                    {it.text}
                  </a>
                </li>
              ))}
            </ul>
          )}
        </aside>

        <div className="api-docs-article" ref={articleRef}>
          {error ? (
            <div className="api-docs-status error">
              Failed to load <code>{docPath}</code>:
              <pre>{error}</pre>
            </div>
          ) : !md ? (
            <div className="api-docs-status">Loading API docs…</div>
          ) : (
            <div
              className="api-docs-content"
              dangerouslySetInnerHTML={{ __html: html }}
            />
          )}
        </div>
      </div>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<ApiDocsApp />);
