// 分镜素材库 (/shots-library)
//   左：clip 卡片网格（封面 = keyframe[0]，hover 缩略 + 标签）
//   右：选中 clip → 完整 kh-spec sidecar 视图（summaries / chunks / relations /
//                                              previews / 用量 / 文件信息）+ 视频 + 3 张 keyframe

const _gainBar = (pct, color) => (
  <div className="bar" style={{ height: 4, marginTop: 2 }}>
    <span style={{ width: `${Math.min(100, Math.max(0, pct))}%`, background: color, transition: "width .3s" }}/>
  </div>
);


// 解析 URL 参数：bind_to / current / return
const _parseBindParams = () => {
  const u = new URL(window.location.href);
  const bindTo = u.searchParams.get("bind_to") || "";
  const [jobId, chunkId] = bindTo.includes(":") ? bindTo.split(":") : ["", ""];
  return {
    bindMode: !!(jobId && chunkId),
    jobId,
    chunkId,
    currentClipId: u.searchParams.get("current") || "",
    returnUrl: u.searchParams.get("return") || "/remix",
  };
};


// 单 clip 卡片（左侧网格）
const ClipCard = ({ clip, usage, active, bindable, isCurrent, deleting, scrubbing, onClick, onBind, onDelete, onScrub }) => {
  const lifePct = Math.round((usage?.life_pct || 0) * 100);
  const lifeColor = lifePct >= 86 ? "var(--pink)" : lifePct >= 57 ? "var(--orange)" : "var(--cyan)";
  return (
    <div onClick={onClick} style={{
      border: `2px solid ${active ? "var(--cyan)" : "var(--line-1)"}`,
      borderRadius: 5, background: active ? "color-mix(in oklch, var(--cyan) 8%, var(--bg-1))" : "var(--bg-1)",
      cursor: "pointer", overflow: "hidden", display: "flex", flexDirection: "column",
    }}>
      <div style={{ position: "relative", paddingTop: "133%", background: "#000" }}>
        <img src={API.seedLibrary.keyframeUrl(clip.clip_id, 1)} alt=""
             onError={(e) => { e.currentTarget.style.display = "none"; }}
             style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover" }}/>
        {clip.has_kh && (
          <span className="tag" style={{
            position: "absolute", top: 4, left: 4, fontSize: 8.5,
            background: "var(--cyan-soft)", color: "var(--cyan)", padding: "0 4px",
          }}>kh ✓</span>
        )}
        <button
          title="清洗 / 扰乱元数据（剥离 EXIF + 像素扰动 + 重写真实元数据）"
          onClick={(e) => { e.stopPropagation(); onScrub && onScrub(clip); }}
          disabled={scrubbing}
          style={{
            position: "absolute", bottom: 4, left: 4,
            width: 22, height: 22, padding: 0, borderRadius: 4,
            border: "1px solid rgba(255,255,255,.25)",
            background: scrubbing ? "rgba(0,0,0,.55)" : "rgba(124, 58, 237, .85)",
            color: "#fff", fontSize: 11, lineHeight: 1, fontFamily: "inherit",
            cursor: scrubbing ? "wait" : "pointer", display: "flex",
            alignItems: "center", justifyContent: "center", fontWeight: 700,
          }}>
          {scrubbing ? "…" : "✱"}
        </button>
        {usage && (
          <span className="mono" style={{
            position: "absolute", top: 4, right: 4, fontSize: 9,
            background: "rgba(0,0,0,.65)", color: lifeColor, padding: "0 4px", borderRadius: 2,
          }}>{usage.total_uses}/{7}</span>
        )}
        <button
          title="删除该分镜（物理删除文件 + sidecar）"
          onClick={(e) => { e.stopPropagation(); onDelete && onDelete(clip); }}
          disabled={deleting}
          style={{
            position: "absolute", bottom: 4, right: 4,
            width: 22, height: 22, padding: 0, borderRadius: 4,
            border: "1px solid rgba(255,255,255,.25)",
            background: deleting ? "rgba(0,0,0,.55)" : "rgba(220, 50, 70, .85)",
            color: "#fff", fontSize: 13, lineHeight: 1, fontFamily: "inherit",
            cursor: deleting ? "wait" : "pointer", display: "flex",
            alignItems: "center", justifyContent: "center", fontWeight: 700,
          }}>
          {deleting ? "…" : "✕"}
        </button>
      </div>
      <div className="col gap-2" style={{ padding: 6 }}>
        <div style={{ fontSize: 10, color: "var(--fg-1)", lineHeight: 1.3,
                       overflow: "hidden", display: "-webkit-box",
                       WebkitLineClamp: 2, WebkitBoxOrient: "vertical" }}
             title={clip.name}>
          {clip.caption || clip.name}
        </div>
        {usage && _gainBar(lifePct, lifeColor)}
        {bindable && (
          <button
            onClick={(e) => { e.stopPropagation(); onBind && onBind(clip.clip_id); }}
            disabled={isCurrent}
            style={{
              fontSize: 10, padding: "3px 6px", borderRadius: 3,
              border: "1px solid " + (isCurrent ? "var(--line-1)" : "var(--purple)"),
              background: isCurrent ? "var(--bg-2)" : "var(--purple)",
              color: isCurrent ? "var(--fg-3)" : "var(--bg-0)",
              cursor: isCurrent ? "not-allowed" : "pointer", fontFamily: "inherit",
              fontWeight: 600,
            }}>
            {isCurrent ? "当前已绑定" : "✓ 选这条 →"}
          </button>
        )}
      </div>
    </div>
  );
};


// 详情面板：完整 kh sidecar
const SidecarDetail = ({ clip, sidecar, usage, onIngest, ingestBusy,
                         bindable, isCurrent, onBind, bindBusy }) => {
  if (!clip) return null;
  if (!sidecar) {
    return <div style={{ padding: 30, textAlign: "center", color: "var(--fg-3)" }}>加载 sidecar…</div>;
  }
  const noKh = sidecar._status === "no_sidecar";
  const previewKeyframes = (sidecar.previews || []).filter(p => p.kind === "keyframe");
  const seg = (sidecar.previews || []).find(p => p.kind === "segment");

  return (
    <div className="col" style={{ gap: 12 }}>
      {/* 绑定模式：突出的「绑这条」按钮 */}
      {bindable && (
        <div className="row between" style={{
          padding: "10px 12px", borderRadius: 5, alignItems: "center",
          background: "color-mix(in oklch, var(--purple) 12%, var(--bg-1))",
          border: "1px solid var(--purple)",
        }}>
          <div className="col gap-2">
            <span style={{ fontSize: 12, fontWeight: 600, color: "var(--purple)" }}>
              绑定这条到当前 chunk？
            </span>
            <span style={{ fontSize: 10, color: "var(--fg-2)" }}>
              {isCurrent ? "这就是当前已绑定的 clip。" : "确认后会替换掉当前绑定，并将原 clip 加入拒绝列表。"}
            </span>
          </div>
          <button onClick={() => onBind && onBind(clip.clip_id)}
                  disabled={isCurrent || bindBusy}
                  className="btn primary"
                  style={{
                    background: isCurrent ? "var(--bg-2)" : "var(--purple)",
                    color: isCurrent ? "var(--fg-3)" : "var(--bg-0)",
                    cursor: isCurrent ? "not-allowed" : "pointer",
                  }}>
            <Icon name="check" size={11}/>
            {bindBusy ? "绑定中…" : isCurrent ? "已绑定" : "绑定 → 返回"}
          </button>
        </div>
      )}

      {/* 视频预览 */}
      <video src={API.seedLibrary.videoUrl(clip.clip_id)} controls
             style={{ width: "100%", maxHeight: 480, background: "#000", borderRadius: 4 }}/>

      {/* 关键帧条 */}
      {previewKeyframes.length > 0 && (
        <div className="col gap-4">
          <span className="kpi-l">关键帧（{previewKeyframes.length} 张 · 10/50/90% 时间点）</span>
          <div className="row gap-6">
            {previewKeyframes.map((p, i) => (
              <a key={i} href={p.url} target="_blank" rel="noreferrer">
                <img src={window.withApiToken(p.url)} style={{ width: 96, height: 128, objectFit: "cover",
                                           borderRadius: 3, border: "1px solid var(--line-1)" }}/>
                <div className="mono" style={{ fontSize: 9, color: "var(--fg-3)", textAlign: "center", marginTop: 2 }}>
                  {(p.timestamp || 0).toFixed(1)}s
                </div>
              </a>
            ))}
          </div>
        </div>
      )}

      {noKh && (
        <div style={{ padding: 12, borderRadius: 4,
                      background: "color-mix(in oklch, var(--orange) 14%, var(--bg-1))",
                      color: "var(--orange)" }}>
          <div style={{ fontSize: 12, fontWeight: 600, marginBottom: 6 }}>尚未 ingest</div>
          <button className="btn primary" onClick={() => onIngest(clip.clip_id)} disabled={ingestBusy}
                  style={{ background: "var(--purple)", color: "var(--bg-0)" }}>
            <Icon name="play" size={11}/>{ingestBusy ? "ingest…" : "立即抽取 kh sidecar"}
          </button>
        </div>
      )}

      {/* summaries — 仅展示 summaries 列表（按用户要求：分镜素材文件只展示 summaries） */}
      <div className="card">
        <div className="card-hd">
          <span className="card-title">summaries</span>
          <span className="card-sub">{(sidecar.summaries || []).length} 条画面摘要</span>
        </div>
        <div className="col" style={{ padding: 12, gap: 8 }}>
          {(sidecar.summaries || []).length > 0 ? (
            <div className="col gap-3">
              {(sidecar.summaries || []).map((s, i) => (
                <div key={i} className="row gap-6" style={{ alignItems: "flex-start" }}>
                  <span className="mono" style={{ fontSize: 9.5, color: "var(--fg-3)", marginTop: 2 }}>#{i + 1}</span>
                  <span style={{ fontSize: 12, color: "var(--fg-0)", lineHeight: 1.5 }}>{s}</span>
                </div>
              ))}
            </div>
          ) : <span style={{ fontSize: 11, color: "var(--fg-3)" }}>—（尚未抽取）</span>}
        </div>
      </div>

      {/* 用量统计 */}
      {usage && (
        <div className="card">
          <div className="card-hd">
            <span className="card-title">用量 / 寿命 / 冷却</span>
            <span className="card-sub">规则：3 天冷却 · 7 次寿命 · 用满自动删</span>
          </div>
          <div className="col" style={{ padding: 12, gap: 6, fontSize: 11 }}>
            <div className="row between">
              <span className="kpi-l">已用次数</span>
              <span className="mono" style={{ color: usage.in_cooldown ? "var(--orange)" : "var(--fg-0)" }}>
                {usage.total_uses} / 7（剩 {usage.life_remaining}）
              </span>
            </div>
            {usage.last_used_at && (
              <div className="row between">
                <span className="kpi-l">上次使用</span>
                <span className="mono" style={{ color: "var(--fg-2)" }}>{usage.last_used_at.slice(0, 19).replace("T", " ")}</span>
              </div>
            )}
            {usage.in_cooldown && (
              <div className="row between">
                <span className="kpi-l">冷却至</span>
                <span className="mono" style={{ color: "var(--orange)" }}>{(usage.cooldown_until || "").slice(0, 19).replace("T", " ")}</span>
              </div>
            )}
            <div className="row between">
              <span className="kpi-l">状态</span>
              <span style={{
                color: usage.exhausted ? "var(--pink)" : usage.in_cooldown ? "var(--orange)" : "var(--green)",
                fontWeight: 600,
              }}>
                {usage.exhausted ? "已耗尽 (下次扫库自动删)" : usage.in_cooldown ? "冷却中" : "可用"}
              </span>
            </div>
          </div>
        </div>
      )}

    </div>
  );
};


const ShotsLibraryPage = () => {
  const [clips, setClips] = React.useState([]);
  const [usageMap, setUsageMap] = React.useState({});
  const [stats, setStats] = React.useState(null);
  const [filter, setFilter] = React.useState("all");
  const [activeId, setActiveId] = React.useState(null);
  const [sidecar, setSidecar] = React.useState(null);
  const [loading, setLoading] = React.useState(false);
  const [err, setErr] = React.useState("");
  const [ingestAllBusy, setIngestAllBusy] = React.useState(false);
  const [ingestOneBusy, setIngestOneBusy] = React.useState(false);
  const [search, setSearch] = React.useState("");
  const [bindBusy, setBindBusy] = React.useState(false);
  const [bindParams, setBindParams] = React.useState(_parseBindParams());
  const [deletingId, setDeletingId] = React.useState("");
  const [scrubbingId, setScrubbingId] = React.useState("");
  const [scrubAllBusy, setScrubAllBusy] = React.useState(false);
  const [batchBusy, setBatchBusy] = React.useState(false);
  const [batchMsg, setBatchMsg] = React.useState("");
  const batchInputRef = React.useRef(null);
  // "需重跑" 面板
  const [staleOpen, setStaleOpen] = React.useState(false);
  const [staleItems, setStaleItems] = React.useState([]);
  const [staleLoading, setStaleLoading] = React.useState(false);
  const [reingestId, setReingestId] = React.useState("");
  const [reingestAllBusy, setReingestAllBusy] = React.useState(false);

  const loadStale = async () => {
    setStaleLoading(true);
    try {
      const r = await API.seedLibrary.listStale();
      setStaleItems(r.items || []);
    } catch (e) {
      setBatchMsg(`✗ 加载 stale 列表失败：${e.message || e}`);
    } finally {
      setStaleLoading(false);
    }
  };

  const reingestOne = async (clipId) => {
    if (reingestId) return;
    setReingestId(clipId);
    try {
      await API.seedLibrary.ingestOne(clipId, true);
      await loadStale();
      await load();
      setBatchMsg(`✓ ${clipId} 已重跑`);
    } catch (e) {
      setBatchMsg(`✗ 重跑 ${clipId} 失败：${e.message || e}`);
    } finally {
      setReingestId("");
      setTimeout(() => setBatchMsg(""), 4000);
    }
  };

  const reingestAllStale = async () => {
    if (reingestAllBusy || !staleItems.length) return;
    if (!confirm(`确认对 ${staleItems.length} 条 stale clip 全部 force ingest？\n会逐条调 kh 服务，耗时按张数线性。`)) return;
    setReingestAllBusy(true);
    let ok = 0, fail = 0;
    for (const it of staleItems) {
      try {
        await API.seedLibrary.ingestOne(it.clip_id, true);
        ok++;
        setBatchMsg(`重跑中：${ok + fail} / ${staleItems.length}（✓ ${ok} · ✗ ${fail}）`);
      } catch (_) { fail++; }
    }
    setReingestAllBusy(false);
    setBatchMsg(`✓ 批量重跑完成：成功 ${ok} · 失败 ${fail}`);
    await loadStale();
    await load();
    setTimeout(() => setBatchMsg(""), 6000);
  };

  const onBatchPick = async (e) => {
    const files = Array.from(e.target.files || []);
    e.target.value = ""; // 允许重复选同一个文件
    if (!files.length) return;
    setBatchBusy(true); setBatchMsg(`上传中：0 / ${files.length}`);
    try {
      const fd = new FormData();
      for (const f of files) fd.append("files", f);
      const r = await API.seedLibrary.uploadClips(fd);
      const ok = r.ok_count || (r.uploaded || []).length;
      const fail = r.fail_count || (r.skipped || []).length;
      setBatchMsg(`✓ 上传 ${ok} · 跳过 ${fail}${fail ? " · " + (r.skipped[0]?.reason || "") : ""}`);
      await load();
    } catch (err) {
      setBatchMsg(`✗ 上传失败：${err.message || err}`);
    } finally {
      setBatchBusy(false);
      setTimeout(() => setBatchMsg(""), 6000);
    }
  };

  // URL 跳转回 / 前进时同步 bindParams + 默认选中当前 clip
  React.useEffect(() => {
    const onPop = () => setBindParams(_parseBindParams());
    window.addEventListener("popstate", onPop);
    return () => window.removeEventListener("popstate", onPop);
  }, []);
  React.useEffect(() => {
    if (bindParams.bindMode && bindParams.currentClipId && !activeId) {
      setActiveId(bindParams.currentClipId);
    }
  }, [bindParams]);

  const doBind = async (clipId) => {
    if (!bindParams.bindMode || bindBusy) return;
    setBindBusy(true);
    setErr("");
    try {
      await API.shotQc.manualBind(bindParams.jobId, bindParams.chunkId, clipId);
      // 跳回 /remix（带成功标记便于上游识别）
      const ret = bindParams.returnUrl || "/remix";
      const sep = ret.includes("?") ? "&" : "?";
      const url = `${ret}${sep}bound=${encodeURIComponent(clipId)}`;
      window.history.pushState({}, "", url);
      window.dispatchEvent(new PopStateEvent("popstate"));
    } catch (e) {
      setErr(String(e.message || e));
      setBindBusy(false);
    }
  };

  const cancelBind = () => {
    const ret = bindParams.returnUrl || "/remix";
    window.history.pushState({}, "", ret);
    window.dispatchEvent(new PopStateEvent("popstate"));
  };

  const load = async () => {
    setLoading(true); setErr("");
    try {
      const [u, h] = await Promise.all([
        API.seedLibrary.usage().catch(() => ({ items: [] })),
        API.seedLibrary.health().catch(() => null),
      ]);
      setStats(h);
      setClips(u.items || []);
      const m = {};
      for (const it of u.items || []) {
        m[it.clip_id] = it;
      }
      setUsageMap(m);
      if (!activeId && (u.items || []).length) setActiveId(u.items[0].clip_id);
    } catch (e) { setErr(String(e.message || e)); }
    finally { setLoading(false); }
  };

  const loadSidecar = async (clipId) => {
    if (!clipId) { setSidecar(null); return; }
    try {
      const s = await API.seedLibrary.sidecar(clipId);
      setSidecar(s);
    } catch (e) {
      setSidecar({ _status: "no_sidecar", _message: String(e.message || e) });
    }
  };

  React.useEffect(() => { load(); }, []);
  React.useEffect(() => { loadSidecar(activeId); }, [activeId]);

  const ingestAll = async () => {
    if (ingestAllBusy) return;
    setIngestAllBusy(true);
    try {
      await API.seedLibrary.ingestAll(false);
      await load();
      if (activeId) await loadSidecar(activeId);
    } catch (e) { setErr(String(e.message || e)); }
    finally { setIngestAllBusy(false); }
  };

  const scrubClip = async (clip) => {
    if (!clip || scrubbingId) return;
    if (!window.confirm(`清洗并扰乱「${clip.name || clip.clip_id}」的元数据 + 像素？\n会就地修改文件，不可恢复。`)) return;
    setScrubbingId(clip.clip_id);
    setErr("");
    try {
      await API.postprocess.scrubClip(clip.clip_id);
      await load();
      if (activeId === clip.clip_id) await loadSidecar(clip.clip_id);
    } catch (e) {
      setErr(`脱敏失败：${String(e.message || e)}`);
    } finally {
      setScrubbingId("");
    }
  };

  const scrubLibraryHits = async () => {
    if (scrubAllBusy) return;
    if (!window.confirm("扫全库 → 命中 AIGC 元数据的 clip 一次性脱敏？")) return;
    setScrubAllBusy(true);
    setErr("");
    try {
      const r = await API.postprocess.scrubLibrary();
      alert(`脱敏完成：成功 ${r.cleaned} · 失败 ${r.failed} · 跳过 ${r.skipped}`);
      await load();
    } catch (e) {
      setErr(`批量脱敏失败：${String(e.message || e)}`);
    } finally {
      setScrubAllBusy(false);
    }
  };

  const deleteClip = async (clip) => {
    if (!clip || deletingId) return;
    const name = clip.name || clip.clip_id;
    if (!window.confirm(`确认物理删除「${name}」？\n\n会移除视频文件 + sidecar，并清空该 clip 的用量记录，不可恢复。`)) {
      return;
    }
    setDeletingId(clip.clip_id);
    setErr("");
    try {
      await API.seedLibrary.deleteClip(clip.clip_id);
      if (activeId === clip.clip_id) {
        setActiveId(null);
        setSidecar(null);
      }
      await load();
    } catch (e) {
      setErr(`删除失败：${String(e.message || e)}`);
    } finally {
      setDeletingId("");
    }
  };

  const ingestOne = async (clipId) => {
    setIngestOneBusy(true);
    try {
      await API.seedLibrary.ingestOne(clipId, true);
      await load();
      await loadSidecar(clipId);
    } catch (e) { setErr(String(e.message || e)); }
    finally { setIngestOneBusy(false); }
  };

  // 过滤 + 搜索
  const visible = React.useMemo(() => {
    let xs = clips;
    if (filter === "with_kh") xs = xs.filter(c => c.in_cooldown !== undefined);  // usage 已包含的都是已索引
    if (filter === "in_cooldown") xs = xs.filter(c => c.in_cooldown);
    if (filter === "exhausted") xs = xs.filter(c => c.exhausted);
    if (filter === "available") xs = xs.filter(c => c.available);
    if (search.trim()) {
      const q = search.trim().toLowerCase();
      xs = xs.filter(c => (c.name || "").toLowerCase().includes(q));
    }
    return xs;
  }, [clips, filter, search]);

  const activeClip = clips.find(c => c.clip_id === activeId);

  return (
    <>
      <Topbar
        crumbs={["资产", "分镜素材库"]}
        right={<>
          <span className="pill" style={{
            background: "color-mix(in oklch, var(--cyan) 14%, var(--bg-1))",
            color: "var(--cyan)",
          }}>
            <Icon name="folder" size={10}/>
            {stats?.count || 0} clip · {stats?.with_kh || 0} 已 kh
          </span>
          <button className="btn ghost" onClick={load} disabled={loading}>
            <Icon name="refresh" size={11}/>{loading ? "刷新…" : "刷新"}
          </button>
          <button className="btn ghost" onClick={() => batchInputRef.current?.click()}
                  disabled={batchBusy}
                  title="从本地文件夹批量上传分镜视频（支持 mp4/mov/mkv/webm/avi · 一次可选多文件）">
            <Icon name="folder" size={11}/>{batchBusy ? "上传中…" : "批量上传素材"}
          </button>
          <input ref={batchInputRef} type="file" multiple
                 accept=".mp4,.mov,.mkv,.webm,.avi,video/*"
                 style={{ display: "none" }}
                 onChange={onBatchPick}/>
          <button className="btn ghost" onClick={async () => { setStaleOpen(true); await loadStale(); }}
                  disabled={staleLoading || reingestAllBusy}
                  title="列出 summary 不正常的 clip（旧 stub / 配额耗尽 / filename-as-summary）">
            <Icon name="more" size={11}/>{staleLoading ? "扫描中…" : "需重跑（stale）"}
          </button>
          <button className="btn ghost" onClick={scrubLibraryHits} disabled={scrubAllBusy}
                  title="扫描全库 → 对命中 AIGC 元数据的 clip 自动脱敏（剥离 EXIF + 像素扰动）">
            <Icon name="filter" size={11}/>{scrubAllBusy ? "脱敏中…" : "扫描+脱敏 AIGC 元数据"}
          </button>
          <button className="btn primary" onClick={ingestAll} disabled={ingestAllBusy}
                  style={{ background: "var(--purple)", color: "var(--bg-0)" }}>
            <Icon name="play" size={11}/>{ingestAllBusy ? "ingesting…" : "全库 ingest"}
          </button>
        </>}
      />

      {batchMsg && (
        <div style={{
          margin: "10px 16px 0", padding: "8px 12px", borderRadius: 5,
          background: batchMsg.startsWith("✗")
            ? "color-mix(in oklch, var(--pink) 14%, var(--bg-1))"
            : batchMsg.startsWith("✓")
              ? "color-mix(in oklch, var(--green) 14%, var(--bg-1))"
              : "color-mix(in oklch, var(--cyan) 14%, var(--bg-1))",
          color: batchMsg.startsWith("✗") ? "var(--pink)"
            : batchMsg.startsWith("✓") ? "var(--green)" : "var(--cyan)",
          fontSize: 12,
        }}>{batchMsg}</div>
      )}

      {/* 绑定模式 Banner */}
      {bindParams.bindMode && (
        <div style={{
          margin: "10px 16px 0", padding: "10px 14px",
          background: "color-mix(in oklch, var(--purple) 12%, var(--bg-1))",
          border: "1px solid var(--purple)", borderRadius: 5,
          display: "flex", alignItems: "center", gap: 12,
        }}>
          <Icon name="folder" size={14} style={{ color: "var(--purple)" }}/>
          <div className="col gap-2" style={{ flex: 1 }}>
            <span style={{ fontSize: 12, fontWeight: 600, color: "var(--purple)" }}>
              绑定模式 · 正在为 chunk <span className="mono">{bindParams.chunkId}</span> 选片
              （job <span className="mono">{bindParams.jobId.slice(0, 8)}</span>）
            </span>
            <span style={{ fontSize: 10.5, color: "var(--fg-2)" }}>
              点击任何 clip 卡片下的「✓ 选这条」或在右侧详情里「绑定 → 返回」即可完成绑定，自动跳回 {bindParams.returnUrl}
            </span>
          </div>
          <button className="btn sm" onClick={cancelBind}>
            <Icon name="close" size={10}/>取消（返回 /remix）
          </button>
        </div>
      )}

      {err && (
        <div style={{ margin: "10px 16px 0", padding: "8px 12px",
                      background: "color-mix(in oklch, var(--pink) 14%, var(--bg-1))",
                      color: "var(--pink)", borderRadius: 4, fontSize: 11 }}>{err}</div>
      )}

      {/* 顶部 stats + filter + search */}
      <div className="row" style={{
        padding: "10px 16px", borderBottom: "1px solid var(--line-1)",
        background: "var(--bg-1)", gap: 10, alignItems: "center",
      }}>
        <span className="kpi-l">筛选</span>
        {[
          { k: "all", l: `全部 ${clips.length}` },
          { k: "available", l: `可用 ${clips.filter(c => c.available).length}` },
          { k: "in_cooldown", l: `冷却 ${clips.filter(c => c.in_cooldown).length}` },
          { k: "exhausted", l: `已耗尽 ${clips.filter(c => c.exhausted).length}` },
        ].map(t => (
          <button key={t.k} className="btn sm" onClick={() => setFilter(t.k)} style={{
            fontSize: 10.5,
            background: filter === t.k ? "color-mix(in oklch, var(--cyan) 14%, var(--bg-1))" : "transparent",
            color: filter === t.k ? "var(--cyan)" : "var(--fg-2)",
            border: filter === t.k ? "1px solid var(--cyan)" : "1px solid var(--line-1)",
          }}>{t.l}</button>
        ))}
        <input className="input grow" placeholder="按文件名 / caption 搜索…" value={search}
               onChange={e => setSearch(e.target.value)}
               style={{ fontSize: 11.5, padding: "4px 8px", maxWidth: 280 }}/>
        <div className="grow"/>
        <span style={{ fontSize: 10, color: "var(--fg-3)" }}>
          数据持久化：<span className="mono">data/seed_clips/.kh-meta/*.json</span>
        </span>
      </div>

      <div className="main-body">
        {/* 左：clip 网格 */}
        <div className="col scroll" style={{
          width: 480, flexShrink: 0, padding: "12px 12px",
          borderRight: "1px solid var(--line-soft)", background: "var(--bg-0)",
        }}>
          {visible.length === 0 ? (
            <div style={{ padding: 30, textAlign: "center", fontSize: 11, color: "var(--fg-3)" }}>
              {loading ? "加载中…" : "无符合条件的 clip"}
            </div>
          ) : (
            <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 8 }}>
              {visible.map(c => (
                <ClipCard key={c.clip_id} clip={c} usage={c}
                          active={c.clip_id === activeId}
                          bindable={bindParams.bindMode}
                          isCurrent={c.clip_id === bindParams.currentClipId}
                          deleting={deletingId === c.clip_id}
                          scrubbing={scrubbingId === c.clip_id}
                          onClick={() => setActiveId(c.clip_id)}
                          onBind={doBind}
                          onDelete={deleteClip}
                          onScrub={scrubClip}/>
              ))}
            </div>
          )}
        </div>

        {/* 右：sidecar 详情 */}
        <div className="col grow scroll" style={{
          padding: "12px 14px 24px", background: "var(--bg-0)", minWidth: 0,
        }}>
          {!activeClip ? (
            <div className="card col center" style={{ padding: "60px 20px", gap: 10, textAlign: "center" }}>
              <Icon name="folder" size={24} style={{ color: "var(--fg-3)" }}/>
              <div style={{ fontSize: 13, color: "var(--fg-1)" }}>请在左侧选择一条 clip</div>
              <div style={{ fontSize: 11, color: "var(--fg-3)" }}>
                右侧将显示完整 kh-spec sidecar（summaries / chunks / relations / previews）
              </div>
            </div>
          ) : (
            <SidecarDetail clip={activeClip} sidecar={sidecar} usage={usageMap[activeClip.clip_id]}
                           onIngest={ingestOne} ingestBusy={ingestOneBusy}
                           bindable={bindParams.bindMode}
                           isCurrent={activeClip.clip_id === bindParams.currentClipId}
                           onBind={doBind} bindBusy={bindBusy}/>
          )}
        </div>
      </div>

      {staleOpen && (
        <div onClick={() => setStaleOpen(false)}
             style={{
               position: "fixed", inset: 0, zIndex: 9999,
               background: "rgba(0,0,0,0.5)",
               display: "flex", alignItems: "center", justifyContent: "center",
               padding: 24,
             }}>
          <div onClick={e => e.stopPropagation()}
               style={{
                 background: "var(--bg-0)", color: "var(--fg-0)",
                 width: "min(900px, 96vw)", maxHeight: "88vh",
                 borderRadius: 8, display: "flex", flexDirection: "column",
                 border: "1px solid var(--line-1)",
                 boxShadow: "0 8px 32px rgba(0,0,0,0.18)",
               }}>
            {/* 顶栏 */}
            <div className="row" style={{
              padding: "12px 16px", borderBottom: "1px solid var(--line-1)",
              alignItems: "center", gap: 10,
            }}>
              <Icon name="more" size={14} style={{ color: "var(--orange)" }}/>
              <span style={{ fontWeight: 600, fontSize: 14 }}>需重跑（stale）</span>
              <span style={{ fontSize: 11, color: "var(--fg-3)" }}>
                {staleItems.length} 条
              </span>
              <div className="grow" style={{ flex: 1 }}/>
              <button className="btn sm" onClick={loadStale} disabled={staleLoading}>
                <Icon name="refresh" size={10}/>{staleLoading ? "扫描中…" : "重扫"}
              </button>
              <button className="btn sm primary" onClick={reingestAllStale}
                      disabled={reingestAllBusy || !staleItems.length}
                      title="对当前列表所有 clip 都跑一次 force ingest（顺序执行）">
                <Icon name="play" size={10}/>
                {reingestAllBusy ? "重跑中…" : `全部重跑（${staleItems.length}）`}
              </button>
              <button className="btn sm ghost" onClick={() => setStaleOpen(false)}>
                <Icon name="close" size={10}/>关闭
              </button>
            </div>

            {/* 列表 */}
            <div style={{ flex: 1, overflow: "auto", padding: "8px 16px" }}>
              {staleItems.length === 0 ? (
                <div className="empty" style={{ padding: 32, textAlign: "center",
                                                color: "var(--fg-3)" }}>
                  {staleLoading ? "扫描中…" : "✓ 全库 summary 都正常"}
                </div>
              ) : (
                <table className="table" style={{ width: "100%", fontSize: 12 }}>
                  <thead>
                    <tr>
                      <th style={{ width: "30%" }}>文件名</th>
                      <th style={{ width: "30%" }}>当前 summaries</th>
                      <th>原因</th>
                      <th style={{ width: 90 }}></th>
                    </tr>
                  </thead>
                  <tbody>
                    {staleItems.map(it => (
                      <tr key={it.clip_id}>
                        <td>
                          <div style={{ fontWeight: 500 }}>{it.name}</div>
                          <div className="mono" style={{ fontSize: 10, color: "var(--fg-3)" }}>
                            {it.clip_id}
                          </div>
                        </td>
                        <td>
                          {(it.summaries || []).length === 0 ? (
                            <span style={{ fontSize: 10.5, color: "var(--fg-3)" }}>（空）</span>
                          ) : (
                            <div className="mono" style={{ fontSize: 10.5, color: "var(--fg-2)",
                                                          maxHeight: 40, overflow: "hidden" }}>
                              {(it.summaries || []).slice(0, 2).join(" · ")}
                              {(it.summaries || []).length > 2 && ` +${it.summaries.length - 2}`}
                            </div>
                          )}
                        </td>
                        <td>
                          <div className="col" style={{ gap: 2 }}>
                            {(it.reasons || []).map((r, i) => (
                              <span key={i} className="tag orange" style={{ fontSize: 10 }}>
                                {r.length > 40 ? r.slice(0, 40) + "…" : r}
                              </span>
                            ))}
                          </div>
                        </td>
                        <td>
                          <button className="btn sm primary"
                                  onClick={() => reingestOne(it.clip_id)}
                                  disabled={!!reingestId || reingestAllBusy}>
                            {reingestId === it.clip_id ? "…" : "重跑"}
                          </button>
                        </td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              )}
            </div>

            {/* 底部 footer */}
            <div style={{
              padding: "8px 16px", borderTop: "1px solid var(--line-soft)",
              fontSize: 11, color: "var(--fg-3)",
            }}>
              ⚠ 重跑前确认 kh01 本地服务已启动且 Gemini 配额未耗尽，否则会被回落到本地启发式（filename-only 文件名场景下会写 summaries=[]）。
            </div>
          </div>
        </div>
      )}
    </>
  );
};

Object.assign(window, { ShotsLibraryPage });
