// Dashboard — 总览数据看板（接 inbox + jobs 真实数据）

const _PLATFORM_NAME = { douyin: "抖音", xhs: "小红书", ks: "快手", sph: "视频号" };

// localStorage 工具：记住用户上次选过的模板/规则，下次自动回显
const _lsGet = (k) => { try { return localStorage.getItem(k) || ""; } catch { return ""; } };
const _lsSet = (k, v) => { try { localStorage.setItem(k, v == null ? "" : String(v)); } catch {} };

const _formatWan = (n) => {
  if (typeof n !== "number" || isNaN(n)) return "0";
  if (n >= 100000000) return (n / 100000000).toFixed(2) + " 亿";
  if (n >= 10000) return (n / 10000).toFixed(1) + " w";
  return n.toLocaleString();
};

// 顶部"链接快速拆解"卡片：粘链接 + 选模板 + 选规则 → 调 /api/analyze/link → 跳转到拆解工作台
const QuickLinkLaunch = ({ onLaunched }) => {
  const [url, setUrl] = React.useState("");
  const [busy, setBusy] = React.useState(false);
  const [hint, setHint] = React.useState(null);
  // 模板/规则下拉数据 + 当前选中（空字符串 = 用户没选，走"未指定"语义）
  const [templates, setTemplates] = React.useState([]);
  const [rulePacks, setRulePacks] = React.useState([]);
  // localStorage 记住上次选过的，下次自动回显
  const [templateId, setTemplateId] = React.useState(_lsGet("quickLink.tplId"));
  const [rulePackId, setRulePackId] = React.useState(_lsGet("quickLink.ruleId"));

  React.useEffect(() => {
    Promise.all([
      API.templates.list({ category: "teardown" }).catch(() => ({ items: [] })),
      API.templates.list({ category: "image_teardown" }).catch(() => ({ items: [] })),
    ]).then(([a, b]) => {
      const merged = [...(a.items || []), ...(b.items || [])];
      setTemplates(merged);
      // 默认偏好：用户没干预过 + 没存 localStorage → 优先用「图文爆款拆解 Skills 提示词模板」
      if (!_lsGet("quickLink.tplId")) {
        const preferred = merged.find(t => t.name === "图文爆款拆解 Skills 提示词模板");
        if (preferred) setTemplateId(String(preferred.id));
      }
    });
    API.rulePacks.list().catch(() => ({ items: [] }))
      .then(d => setRulePacks((d.items || []).filter(r => r.enabled !== false)));
  }, []);

  const onTplChange = (v) => { setTemplateId(v); _lsSet("quickLink.tplId", v); };
  const onRuleChange = (v) => { setRulePackId(v); _lsSet("quickLink.ruleId", v); };

  const submit = async () => {
    const raw = url.trim();
    if (!raw) {
      setHint({ kind: "err", msg: "请先粘贴视频链接" });
      return;
    }
    setBusy(true);
    setHint(null);
    try {
      const body = { url: raw, title: "" };
      if (templateId) body.template_ids = [Number(templateId)];
      if (rulePackId) body.rule_pack_id = rulePackId;
      const res = await API.analyze.link(body);
      const jobId = res.job_id || (res.job_ids && res.job_ids[0]);
      const usedTpl = templates.find(t => String(t.id) === String(templateId));
      const usedRule = rulePacks.find(r => r.pack_id === rulePackId);
      const tplLabel = usedTpl ? `（模板：${usedTpl.name}` : "（默认模板";
      const ruleLabel = usedRule ? ` · 规则：${usedRule.display_name}）` : "）";
      setHint({ kind: "ok", msg: `已发起拆解${tplLabel}${ruleLabel} · 任务 ${jobId}`, jobId });
      setUrl("");
      onLaunched && onLaunched(jobId);
    } catch (e) {
      setHint({ kind: "err", msg: String(e.message || e) });
    } finally {
      setBusy(false);
    }
  };

  const onKeyDown = (e) => {
    if (e.key === "Enter" && !busy) submit();
  };

  return (
    <div className="card" style={{ padding: 14 }}>
      <div className="row between" style={{ alignItems: "center", marginBottom: 10 }}>
        <div className="col gap-2">
          <div className="row gap-8" style={{ alignItems: "center" }}>
            <Icon name="ext" size={12}/>
            <span style={{ fontSize: 13, fontWeight: 600, color: "var(--fg-0)" }}>链接快速拆解</span>
          </div>
          <span className="card-sub">支持抖音 / 小红书 / 快手 · 直接粘贴"复制链接"原文即可</span>
        </div>
        <a className="btn sm ghost" href="/workspace"><Icon name="cut" size={10}/>前往工作台</a>
      </div>
      <div className="row gap-8" style={{ alignItems: "center" }}>
        <input
          className="input grow"
          placeholder="粘贴视频链接，例如 https://v.douyin.com/xxxx/"
          value={url}
          onChange={e => setUrl(e.target.value)}
          onKeyDown={onKeyDown}
          disabled={busy}
          style={{ fontSize: 12 }}
        />
        <select
          className="input"
          value={templateId}
          onChange={e => onTplChange(e.target.value)}
          disabled={busy}
          title="选拆解模板；不选 = 默认模板。图文笔记会自动改走 image_teardown 模板"
          style={{ fontSize: 11.5, padding: "5px 8px", maxWidth: 220 }}
        >
          <option value="">默认模板</option>
          {templates.map(t => (
            <option key={t.id} value={t.id}>
              {t.name}{t.is_default ? "（默认）" : ""}
              {t.category === "image_teardown" ? " · 图文" : t.category === "teardown" ? " · 视频" : ""}
            </option>
          ))}
        </select>
        <select
          className="input"
          value={rulePackId}
          onChange={e => onRuleChange(e.target.value)}
          disabled={busy}
          title="选规则模板：拆解 LLM 会在该规则约束下产出。不选 = 无规则限制"
          style={{ fontSize: 11.5, padding: "5px 8px", maxWidth: 200 }}
        >
          <option value="">无规则限制</option>
          {rulePacks.map(r => (
            <option key={r.pack_id} value={r.pack_id}>{r.display_name}</option>
          ))}
        </select>
        <button
          className={`btn ${busy ? "" : "primary"}`}
          onClick={submit}
          disabled={busy || !url.trim()}
          style={{ minWidth: 96 }}
        >
          {busy ? "发起中…" : "解析并拆解"}
        </button>
      </div>
      {hint && (
        <div
          className="row between"
          style={{
            marginTop: 8,
            padding: "6px 10px",
            borderRadius: 4,
            fontSize: 11,
            background: hint.kind === "ok" ? "var(--cyan-soft)" : "rgba(255,90,90,0.08)",
            color: hint.kind === "ok" ? "var(--cyan)" : "var(--pink)",
          }}
        >
          <span>{hint.msg}</span>
          {hint.kind === "ok" && hint.jobId && (
            <a className="btn sm ghost" href={`/workspace?job=${encodeURIComponent(hint.jobId)}`}>
              <Icon name="ext" size={10}/>查看进度
            </a>
          )}
        </div>
      )}
    </div>
  );
};

// AIGC 混剪：粘文案 → /api/aigc-mix → 轮询进度 → 渲染成片预览
const AigcMixPanel = () => {
  const [text, setText] = React.useState(
    "当你打开自己家门的那一刻，什么都不用解释了。那套房，就是答案。现在就添加AI选房师。住进去那天，你笑了。",
  );
  const [voice, setVoice] = React.useState("zh-CN-XiaoxiaoNeural");
  const [seed, setSeed] = React.useState("42");
  const [job, setJob] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState("");
  const [health, setHealth] = React.useState(null);
  const esRef = React.useRef(null);          // EventSource
  const pollerRef = React.useRef(null);      // 兜底 5s 轮询（仅 SSE 断流时启）

  React.useEffect(() => {
    API.aigcMix.health().then(setHealth).catch(() => setHealth(null));
    return () => {
      if (esRef.current) { try { esRef.current.close(); } catch (_) {} esRef.current = null; }
      if (pollerRef.current) clearInterval(pollerRef.current);
    };
  }, []);

  // 订阅一个 mix job 的 SSE 进度（替代 1.2s 轮询）
  const subscribe = (jobId) => {
    if (esRef.current) { try { esRef.current.close(); } catch (_) {} esRef.current = null; }
    if (pollerRef.current) { clearInterval(pollerRef.current); pollerRef.current = null; }
    let es;
    try {
      es = new EventSource(`/api/aigc-mix/jobs/${encodeURIComponent(jobId)}/stream`);
    } catch (_) {
      pollerRef.current = setInterval(async () => {
        try { setJob(await API.aigcMix.get(jobId)); } catch (_) {}
      }, 5000);
      return;
    }
    esRef.current = es;
    es.addEventListener("status", (ev) => {
      try {
        const d = JSON.parse(ev.data);
        setJob(prev => prev ? { ...prev, status: d.status || prev.status } : prev);
      } catch (_) {}
    });
    es.addEventListener("progress", (ev) => {
      try {
        const d = JSON.parse(ev.data);
        setJob(prev => prev ? {
          ...prev,
          progress: typeof d.pct === "number" ? d.pct : prev.progress,
          message: d.message || prev.message,
          step: d.step || prev.step,
        } : prev);
      } catch (_) {}
    });
    es.addEventListener("done", (ev) => {
      try {
        const d = JSON.parse(ev.data);
        setJob(prev => prev ? {
          ...prev,
          status: d.status || prev.status,
          error: d.error || prev.error,
          output_url: d.output_url || prev.output_url,
          unmatched_count: typeof d.unmatched_count === "number" ? d.unmatched_count : prev.unmatched_count,
        } : prev);
      } catch (_) {}
      setBusy(false);
      try { es.close(); } catch (_) {}
      esRef.current = null;
    });
    es.onerror = () => {
      // SSE 断流 → 启 5s 兜底轮询，直到 status 终结
      try { es.close(); } catch (_) {}
      esRef.current = null;
      if (!pollerRef.current) {
        pollerRef.current = setInterval(async () => {
          try {
            const next = await API.aigcMix.get(jobId);
            setJob(next);
            if (next.status === "completed" || next.status === "failed" || next.status === "cancelled") {
              clearInterval(pollerRef.current);
              pollerRef.current = null;
              setBusy(false);
            }
          } catch (_) {}
        }, 5000);
      }
    };
  };

  const startMix = async () => {
    if (!text.trim()) { setErr("请填写文案"); return; }
    setBusy(true); setErr("");
    try {
      const j = await API.aigcMix.start({
        text: text.trim(), voice,
        seed: seed === "" ? null : Number(seed),
      });
      setJob(j);
      subscribe(j.job_id);
    } catch (e) {
      setErr(String(e.message || e));
      setBusy(false);
    }
  };

  const reset = () => {
    if (esRef.current) { try { esRef.current.close(); } catch (_) {} esRef.current = null; }
    if (pollerRef.current) { clearInterval(pollerRef.current); pollerRef.current = null; }
    setJob(null); setBusy(false); setErr("");
  };

  const pct = job?.progress || 0;
  const isDone = job?.status === "completed";
  const isFail = job?.status === "failed";

  return (
    <div className="card" style={{ padding: 14 }}>
      <div className="row between" style={{ alignItems: "center", marginBottom: 10 }}>
        <div className="col gap-2">
          <div className="row gap-8" style={{ alignItems: "center" }}>
            <Icon name="cut" size={12}/>
            <span style={{ fontSize: 13, fontWeight: 600, color: "var(--fg-0)" }}>AIGC 混剪合成</span>
            {health && (
              <span className="tag" style={{ fontSize: 9, padding: "0 5px",
                color: health.ok ? "var(--cyan)" : "var(--pink)" }}>
                {health.ok ? `就绪 · ${health.clips_count} 分镜` : "脚本未就绪"}
              </span>
            )}
          </div>
          <span className="card-sub">
            文案 → edge-tts 口播 → ffmpeg 拼接分镜 + 烧字幕 + 三轨混音 → 成片
          </span>
        </div>
        {job && (
          <button className="btn sm ghost" onClick={reset}>
            <Icon name="close" size={10}/>新一条
          </button>
        )}
      </div>

      {!job && (
        <div className="col gap-8">
          <textarea
            className="textarea"
            value={text}
            onChange={e => setText(e.target.value)}
            placeholder="粘贴或输入文案；多句用句号 / 问号 / 感叹号断开。"
            style={{ minHeight: 90, fontSize: 12, lineHeight: 1.6 }}
            disabled={busy}
          />
          <div className="row gap-8" style={{ alignItems: "center" }}>
            <span className="kpi-l">音色</span>
            <select className="input" value={voice} onChange={e => setVoice(e.target.value)}
                    disabled={busy} style={{ fontSize: 11.5, padding: "4px 8px", maxWidth: 220 }}>
              <option value="zh-CN-XiaoxiaoNeural">晓晓（女·标准）</option>
              <option value="zh-CN-YunxiNeural">云希（男·年轻）</option>
              <option value="zh-CN-YunyangNeural">云扬（男·新闻）</option>
              <option value="zh-CN-XiaoyiNeural">晓伊（女·温柔）</option>
              <option value="zh-CN-YunjianNeural">云健（男·解说）</option>
            </select>
            <span className="kpi-l" style={{ marginLeft: 8 }}>seed</span>
            <input className="input" value={seed} onChange={e => setSeed(e.target.value)}
                   placeholder="留空 = 随机" disabled={busy}
                   style={{ fontSize: 11.5, padding: "4px 8px", width: 80 }}/>
            <div className="grow"/>
            <button className={`btn ${busy ? "" : "primary"}`} onClick={startMix}
                    disabled={busy || !text.trim() || (health && !health.ok)}>
              {busy ? "提交中…" : "开始合成"}
            </button>
          </div>
          {err && <span style={{ fontSize: 11, color: "var(--pink)" }}>{err}</span>}
          {health && !health.ok && (
            <span style={{ fontSize: 10.5, color: "var(--fg-3)" }}>
              ⚠ AIGC混剪 venv 或脚本路径不在 {"{python: " + health.python + "}"}，请先按 DEPLOY.md 在
              ~/Downloads/AIGC混剪 跑一次 python3.11 -m venv .venv && .venv/bin/pip install -r requirements.txt
            </span>
          )}
        </div>
      )}

      {job && (
        <div className="col gap-10">
          <div className="row between" style={{ alignItems: "center" }}>
            <span className="mono" style={{ fontSize: 10.5, color: "var(--fg-3)" }}>
              任务 {job.job_id} · {job.status}
            </span>
            <span className="mono" style={{ fontSize: 11, color: isFail ? "var(--pink)" : "var(--cyan)" }}>
              {pct}%
            </span>
          </div>
          <div className="bar"><span style={{
            width: `${pct}%`,
            background: isFail ? "var(--pink)" : "var(--cyan)",
          }}/></div>
          <div style={{ fontSize: 11, color: "var(--fg-1)" }}>
            {job.message || (isFail ? (job.error || "失败") : "运行中…")}
          </div>
          {(job.log_tail || []).length > 0 && (
            <pre style={{
              fontSize: 10.5, lineHeight: 1.5, color: "var(--fg-3)",
              background: "var(--bg-1)", padding: 8, borderRadius: 4,
              maxHeight: 110, overflow: "auto", margin: 0, whiteSpace: "pre-wrap",
            }}>{(job.log_tail || []).slice(-8).join("\n")}</pre>
          )}
          {isDone && (
            <div className="col gap-6">
              <video
                src={API.aigcMix.videoUrl(job.job_id)}
                controls
                style={{ width: "100%", maxHeight: 480, background: "#000", borderRadius: 4 }}
              />
              <div className="row gap-8">
                <a className="btn sm" href={API.aigcMix.videoUrl(job.job_id)} download>
                  <Icon name="ext" size={10}/>下载 mp4
                </a>
                <a className="btn sm ghost" href={`/api/aigc-mix/jobs/${job.job_id}/manifest`} target="_blank" rel="noreferrer">
                  <Icon name="ext" size={10}/>查看 manifest
                </a>
              </div>
            </div>
          )}
        </div>
      )}
    </div>
  );
};

// 链接 → 文案二创独立链路（送到 /remix?text_rewrite_job=<id>）
const QuickTextRewriteLaunch = () => {
  const [url, setUrl] = React.useState("");
  const [busy, setBusy] = React.useState(false);
  const [hint, setHint] = React.useState(null);
  const [templates, setTemplates] = React.useState([]);
  const [rulePacks, setRulePacks] = React.useState([]);
  const [templateId, setTemplateId] = React.useState(_lsGet("quickRewrite.tplId"));
  const [rulePackId, setRulePackId] = React.useState(_lsGet("quickRewrite.ruleId"));

  React.useEffect(() => {
    // 当前 /text-rewrite/templates 表是空的，这里合并拉 teardown + image_teardown
    // 两个 category 的拆解模板，让用户能一次看到所有可选（视频拆解 + 图文拆解）
    Promise.all([
      API.textRewrite.listTemplates().catch(() => ({ items: [] })),
      API.templates.list({ category: "teardown" }).catch(() => ({ items: [] })),
      API.templates.list({ category: "image_teardown" }).catch(() => ({ items: [] })),
    ]).then(([a, b, c]) => {
      const merged = [...(a.items || []), ...(b.items || []), ...(c.items || [])];
      setTemplates(merged);
      if (!_lsGet("quickRewrite.tplId")) {
        const preferred = merged.find(t => t.name === "[内置·拆解] 房地产 AIDIS 五段拆解");
        if (preferred) setTemplateId(String(preferred.id));
      }
    });
    API.rulePacks.list().catch(() => ({ items: [] }))
      .then(d => setRulePacks((d.items || []).filter(r => r.enabled !== false)));
  }, []);

  const onTplChange = (v) => { setTemplateId(v); _lsSet("quickRewrite.tplId", v); };
  const onRuleChange = (v) => { setRulePackId(v); _lsSet("quickRewrite.ruleId", v); };

  const submit = async () => {
    const raw = url.trim();
    if (!raw) { setHint({ kind: "err", msg: "请先粘贴视频链接" }); return; }
    setBusy(true); setHint(null);
    try {
      const body = { url: raw, title: "" };
      if (templateId) body.rewrite_template_id = Number(templateId);
      if (rulePackId) body.rule_pack_id = rulePackId;
      const res = await API.textRewrite.startLink(body);
      const usedTpl = templates.find(t => String(t.id) === String(templateId));
      const usedRule = rulePacks.find(r => r.pack_id === rulePackId);
      const tplLabel = usedTpl ? `（模板：${usedTpl.name}` : "（默认模板";
      const ruleLabel = usedRule ? ` · 规则：${usedRule.display_name}）` : "）";
      setHint({ kind: "ok",
                msg: `已发起文案二创${tplLabel}${ruleLabel} · 任务 ${res.job_id}`,
                jobId: res.job_id });
      setUrl("");
      window.history.pushState({}, "",
        `/remix?text_rewrite_job=${encodeURIComponent(res.job_id)}`);
      window.dispatchEvent(new PopStateEvent("popstate"));
    } catch (e) {
      setHint({ kind: "err", msg: String(e.message || e) });
    } finally { setBusy(false); }
  };

  return (
    <div className="card" style={{ padding: 14 }}>
      <div className="row between" style={{ alignItems: "center", marginBottom: 10 }}>
        <div className="col gap-2">
          <div className="row gap-8" style={{ alignItems: "center" }}>
            <Icon name="edit" size={12}/>
            <span style={{ fontSize: 13, fontWeight: 600, color: "var(--fg-0)" }}>文案二创独立链路</span>
            <span className="tag" style={{ fontSize: 9, padding: "0 5px",
              background: "color-mix(in oklch, var(--purple) 14%, var(--bg-1))",
              color: "var(--purple)" }}>纯文案 · 7 stage</span>
          </div>
          <span className="card-sub">
            链接 → ASR → 修字 → 句切 → 段切（语义自然段）→ 段级二创 · 不做视觉拆解
          </span>
        </div>
        <a className="btn sm ghost" href="/remix"><Icon name="ext" size={10}/>前往复刻看链路</a>
      </div>
      <div className="row gap-8" style={{ alignItems: "center" }}>
        <input className="input grow" placeholder="粘贴视频链接（抖音 / 小红书 / 快手 / yt-dlp 兼容）"
               value={url} onChange={e => setUrl(e.target.value)}
               onKeyDown={e => e.key === "Enter" && !busy && submit()}
               disabled={busy} style={{ fontSize: 12 }}/>
        <select className="input" value={templateId}
                onChange={e => onTplChange(e.target.value)}
                disabled={busy}
                title="选二创模板；不选 = 「[内置·拆解] 房地产 AIDIS 五段拆解」"
                style={{ fontSize: 11.5, padding: "5px 8px", maxWidth: 220 }}>
          <option value="">默认模板（AIDIS 五段）</option>
          {templates.map(t => (
            <option key={t.id} value={t.id}>
              {t.name}{t.is_default ? "（默认）" : ""}
              {t.category === "image_teardown" ? " · 图文" : t.category === "teardown" ? " · 视频" : ""}
            </option>
          ))}
        </select>
        <select className="input" value={rulePackId}
                onChange={e => onRuleChange(e.target.value)}
                disabled={busy}
                title="选规则模板：二创 LLM 会在该规则约束下产出。不选 = 无规则限制"
                style={{ fontSize: 11.5, padding: "5px 8px", maxWidth: 200 }}>
          <option value="">无规则限制</option>
          {rulePacks.map(r => (
            <option key={r.pack_id} value={r.pack_id}>{r.display_name}</option>
          ))}
        </select>
        <button className={`btn ${busy ? "" : "primary"}`} onClick={submit}
                disabled={busy || !url.trim()}
                style={{ minWidth: 120,
                  background: busy ? undefined : "var(--purple)",
                  color: busy ? undefined : "var(--bg-0)" }}>
          {busy ? "发起中…" : "二创 + 送复刻 →"}
        </button>
      </div>
      {hint && (
        <div className="row between" style={{
          marginTop: 8, padding: "6px 10px", borderRadius: 4, fontSize: 11,
          background: hint.kind === "ok" ? "color-mix(in oklch, var(--purple) 12%, var(--bg-1))"
                                         : "rgba(255,90,90,0.08)",
          color: hint.kind === "ok" ? "var(--purple)" : "var(--pink)",
        }}>
          <span>{hint.msg}</span>
        </div>
      )}
    </div>
  );
};


const Dashboard = () => {
  const [inbox, setInbox] = React.useState([]);
  const [jobs, setJobs] = React.useState([]);
  const [loading, setLoading] = React.useState(true);

  const load = async () => {
    setLoading(true);
    try {
      const [d1, d2] = await Promise.all([
        API.inbox.list({ status: "all", limit: 5000 }).catch(() => ({ items: [] })),
        API.jobs.list().catch(() => ({ items: [] })),
      ]);
      setInbox(d1.items || []);
      setJobs(d2.items || []);
    } finally {
      setLoading(false);
    }
  };

  React.useEffect(() => {
    load();
    const t = setInterval(load, 30000); // 30s 自动刷新
    return () => clearInterval(t);
  }, []);

  // ─── 派生统计 ───
  const stats = React.useMemo(() => {
    const now = new Date();
    const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
    const dayMs = 86400000;

    const inboxTotal = inbox.length;
    const completedJobs = jobs.filter(j => j.status === "completed");
    const completedTotal = completedJobs.length;
    const failedTotal = jobs.filter(j => j.status === "failed").length;
    const runningTotal = jobs.filter(j => j.status === "running" || j.status === "pending").length;

    // 14 天分桶（0 = 13 天前，13 = 今天）；KPI mini-bar 用全部 14 天，趋势折线用后 7 天
    const days14 = [];
    for (let i = 13; i >= 0; i--) {
      const dStart = today.getTime() - i * dayMs;
      const dEnd = dStart + dayMs;
      const collect = inbox.filter(v => {
        const t = Date.parse(v.received_at);
        return !isNaN(t) && t >= dStart && t < dEnd;
      }).length;
      const hit = completedJobs.filter(j => {
        const t = Date.parse(j.updated_at);
        return !isNaN(t) && t >= dStart && t < dEnd;
      }).length;
      days14.push({ collect, hit });
    }
    const collectBars = days14.map(d => d.collect);
    const hitBars = days14.map(d => d.hit);

    // 近 7 天 vs 上 7 天 增长率（KPI delta）
    const sumLast7 = (arr) => arr.slice(7, 14).reduce((s, x) => s + x, 0);
    const sumPrev7 = (arr) => arr.slice(0, 7).reduce((s, x) => s + x, 0);
    const computeDelta = (arr) => {
      const last = sumLast7(arr), prev = sumPrev7(arr);
      if (prev === 0) return last > 0 ? "新增" : "—";
      const pct = (last - prev) / prev * 100;
      const sign = pct >= 0 ? "+" : "";
      return `${sign}${pct.toFixed(1)}%`;
    };

    // 趋势折线（近 7 天每天命中数；如果命中全 0，回落到采集数让曲线有形）
    const last7Hit = days14.slice(7).map(d => d.hit);
    const last7Collect = days14.slice(7).map(d => d.collect);
    const hasHit = last7Hit.some(v => v > 0);
    const trend7 = hasHit ? last7Hit : last7Collect;

    // 平台分布
    const platformAgg = {};
    for (const v of inbox) {
      const p = v.platform || "douyin";
      platformAgg[p] = (platformAgg[p] || 0) + 1;
    }
    const platformList = Object.entries(platformAgg)
      .sort((a, b) => b[1] - a[1])
      .map(([p, n]) => ({
        p, name: _PLATFORM_NAME[p] || p, v: n,
        pct: inboxTotal ? Math.round(n / inboxTotal * 100) : 0,
      }));

    // 实时爆款热榜 — inbox 按点赞排序 TOP 6
    const hot = [...inbox]
      .filter(v => (v.stats?.diggCount || 0) > 0)
      .sort((a, b) => (b.stats?.diggCount || 0) - (a.stats?.diggCount || 0))
      .slice(0, 6)
      .map((v, i) => {
        const digg = v.stats?.diggCount || 0;
        // 简单 hot 分：log10(digg) 映射到 [1, 9.9]
        const hotScore = digg > 0
          ? Math.min(9.9, Math.max(1, Math.log10(digg) * 1.5 + 1)).toFixed(1)
          : "—";
        return {
          r: i + 1,
          v,
          digg,
          hotScore,
          formula: v.estate_type || (v.industry && v.industry !== "general" ? v.industry : null) || null,
        };
      });

    // 题材分布 TOP 6（用 estate_type / industry / hashtag 兜底聚合）
    const formulaAgg = {};
    for (const v of inbox) {
      let k = v.estate_type;
      if (!k && v.industry && v.industry !== "general") k = v.industry;
      if (!k && v.hashtag) {
        const tags = String(v.hashtag).split(/[#\s,，]+/).filter(Boolean);
        if (tags.length) k = tags[0];
      }
      if (!k) continue;
      formulaAgg[k] = (formulaAgg[k] || 0) + 1;
    }
    const formulaList = Object.entries(formulaAgg)
      .sort((a, b) => b[1] - a[1])
      .slice(0, 6)
      .map(([n, c]) => ({ n, c }));

    return {
      inboxTotal, completedTotal, failedTotal, runningTotal,
      collectBars, hitBars,
      collectDelta: computeDelta(collectBars),
      hitDelta: computeDelta(hitBars),
      trend7, trend7Label: hasHit ? "命中数" : "采集数",
      platformList,
      hot,
      formulaList,
    };
  }, [inbox, jobs]);

  // 折线 X 轴 4 个 label（近 7 天均匀分布）
  const trendLabels = React.useMemo(() => {
    const now = new Date();
    const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
    return [0, 2, 4, 6].map(i => {
      const d = new Date(today.getTime() - (6 - i) * 86400000);
      return `${String(d.getMonth() + 1).padStart(2, "0")}/${String(d.getDate()).padStart(2, "0")}`;
    });
  }, []);

  const renderDelta = (d) => {
    if (d === "—" || d === "未启用") {
      return <span style={{ fontSize: 10, color: "var(--fg-3)" }}>{d}</span>;
    }
    const isUp = d === "新增" || d.startsWith("+");
    return <span className={`delta ${isUp ? "up" : "down"}`}>{isUp ? "▲" : "▼"} {d}</span>;
  };

  return (
    <>
      <Topbar
        crumbs={["总览", "全平台 · 近 7 天"]}
        right={<>
          <button className="btn ghost" onClick={load} disabled={loading}>
            <Icon name="calendar" size={12}/>{loading ? "刷新中…" : "刷新"}
          </button>
        </>}
      />
      <div className="col grow scroll" style={{ padding: "14px 16px 24px", gap: 14 }}>

        {/* 链接快速拆解：粘链接直接发起拆解 */}
        <QuickLinkLaunch onLaunched={() => load()}/>

        {/* 文案二创独立链路：链接 → 二创文案 → 跳 /remix */}
        <QuickTextRewriteLaunch/>

        {/* AIGC 混剪：文案 → 成片 */}
        <AigcMixPanel/>

        {/* KPI 行 */}
        <div style={{ display: "grid", gridTemplateColumns: "repeat(4,1fr)", gap: 12 }}>
          {[
            { l: "采集视频", v: stats.inboxTotal.toLocaleString(), u: "条", d: stats.collectDelta, chart: stats.collectBars, col: "cyan" },
            { l: "爆款命中", v: stats.completedTotal.toLocaleString(), u: "条", d: stats.hitDelta, chart: stats.hitBars, col: "orange" },
            { l: "拆解失败", v: stats.failedTotal.toLocaleString(), u: "条", d: "—", chart: Array(14).fill(0), col: "pink" },
            { l: "进行中", v: stats.runningTotal.toLocaleString(), u: "条", d: "—", chart: Array(14).fill(0), col: "purple" },
          ].map((k, i) => (
            <div key={i} className="card" style={{ padding: 14 }}>
              <div className="kpi-l">{k.l}</div>
              <div className="row between" style={{ marginTop: 6, alignItems: "flex-end" }}>
                <div className="row gap-4" style={{ alignItems: "baseline" }}>
                  <span className="kpi-v">{k.v}</span>
                  <span className="u" style={{ fontSize: 11, color: "var(--fg-2)" }}>{k.u}</span>
                </div>
                {renderDelta(k.d)}
              </div>
              <div style={{ marginTop: 10 }}>
                <MiniBars data={k.chart.length ? k.chart : Array(14).fill(0)} color={`var(--${k.col})`} height={26}/>
              </div>
            </div>
          ))}
        </div>

        {/* 爆款趋势 + 平台分布 */}
        <div style={{ display: "grid", gridTemplateColumns: "1.7fr 1fr", gap: 12 }}>
          <div className="card">
            <div className="card-hd">
              <span className="card-title">爆款命中趋势</span>
              <span className="card-sub">daily · {stats.trend7Label}</span>
              <div className="grow"/>
              <span className="tag cyan"><span className="dot" style={{ background: "var(--cyan)" }}/>{stats.trend7Label}</span>
            </div>
            <div style={{ padding: 14 }}>
              {stats.trend7.some(v => v > 0) ? (
                <>
                  <CurveChart data={stats.trend7} width={640} height={140}/>
                  <div className="row between mono" style={{ fontSize: 9.5, color: "var(--fg-3)", marginTop: 6 }}>
                    {trendLabels.map(l => <span key={l}>{l}</span>)}
                  </div>
                </>
              ) : (
                <div style={{ padding: "40px 0", textAlign: "center", fontSize: 12, color: "var(--fg-3)" }}>
                  近 7 天暂无活跃数据
                </div>
              )}
            </div>
          </div>

          <div className="card">
            <div className="card-hd">
              <span className="card-title">平台分布</span>
              <span className="card-sub">基于 inbox · 全部</span>
            </div>
            <div className="col" style={{ padding: 14, gap: 12 }}>
              {stats.platformList.length ? stats.platformList.map((r, i) => (
                <div key={i} className="col gap-6">
                  <div className="row between">
                    <div className="row gap-8">
                      <PF type={r.p}/>
                      <span style={{ fontSize: 11.5, color: "var(--fg-1)" }}>{r.name}</span>
                    </div>
                    <div className="row gap-8">
                      <span className="mono" style={{ fontSize: 11, color: "var(--fg-0)" }}>{r.v.toLocaleString()}</span>
                      <span className="mono" style={{ fontSize: 10, color: "var(--fg-3)", minWidth: 28, textAlign: "right" }}>{r.pct}%</span>
                    </div>
                  </div>
                  <div className="bar"><span style={{ width: `${r.pct}%`, background: "var(--cyan)" }}/></div>
                </div>
              )) : (
                <div style={{ padding: 20, textAlign: "center", fontSize: 11, color: "var(--fg-3)" }}>暂无平台数据</div>
              )}
            </div>
          </div>
        </div>

        {/* 实时热榜 + 题材分布 */}
        <div style={{ display: "grid", gridTemplateColumns: "1.7fr 1fr", gap: 12 }}>
          <div className="card">
            <div className="card-hd">
              <span className="card-title">实时爆款热榜</span>
              <span className="card-sub">TOP {stats.hot.length} · 按点赞排序</span>
              <div className="grow"/>
              <a className="btn sm ghost" href="/collect"><Icon name="ext" size={10}/>查看全部</a>
            </div>
            <div className="col">
              {stats.hot.length ? stats.hot.map((row) => (
                <div key={row.v.id} className="row" style={{ padding: "9px 14px", gap: 12, borderBottom: "1px solid var(--line-soft)", alignItems: "center" }}>
                  <div className="mono" style={{ width: 22, fontSize: 13, fontWeight: 600, color: row.r <= 3 ? "var(--orange)" : "var(--fg-3)" }}>{String(row.r).padStart(2, "0")}</div>
                  <div className="thumb" style={{ width: 36, height: 48, flexShrink: 0, position: "relative", background: row.v.cover ? `center/cover no-repeat url(${row.v.cover})` : "var(--bg-1)" }}>
                    <div style={{ position: "absolute", top: 2, left: 2, zIndex: 2 }}><PF type={row.v.platform || "douyin"}/></div>
                  </div>
                  <div className="col grow gap-2" style={{ minWidth: 0 }}>
                    <div style={{ fontSize: 12, color: "var(--fg-0)", fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{row.v.title || "(无标题)"}</div>
                    <div className="row gap-10" style={{ fontSize: 10.5, color: "var(--fg-3)" }}>
                      <span>{row.v.author ? `@${row.v.author}` : "@未知"}</span>
                      {row.formula && <span className="tag" style={{ padding: "0 5px" }}>{row.formula}</span>}
                    </div>
                  </div>
                  <div className="col gap-2" style={{ textAlign: "right", minWidth: 70 }}>
                    <span className="mono" style={{ fontSize: 12, color: "var(--fg-0)", fontWeight: 600 }}>{_formatWan(row.digg)}</span>
                    <span className="kpi-l">点赞</span>
                  </div>
                  <div className="col gap-2" style={{ textAlign: "right", minWidth: 40 }}>
                    <span className="mono" style={{ fontSize: 12, color: "var(--orange)", fontWeight: 600 }}>{row.hotScore}</span>
                    <span className="kpi-l">HOT</span>
                  </div>
                  <a className="btn sm ghost" href="/workspace"><Icon name="cut" size={10}/>拆解</a>
                </div>
              )) : (
                <div style={{ padding: 30, textAlign: "center", fontSize: 12, color: "var(--fg-3)" }}>
                  暂无 inbox 数据 · 前往「1·收集爆款」入库
                </div>
              )}
            </div>
          </div>

          <div className="card">
            <div className="card-hd">
              <span className="card-title">题材分布</span>
              <span className="card-sub">已采集 · TOP 6</span>
            </div>
            <div className="col" style={{ padding: 10, gap: 2 }}>
              {stats.formulaList.length ? stats.formulaList.map((f, i) => (
                <div key={i} className="row" style={{ padding: "7px 8px", gap: 10, borderRadius: 4 }}>
                  <div className="mono" style={{ fontSize: 10.5, color: "var(--fg-3)", width: 16 }}>{i + 1}</div>
                  <div className="col grow gap-2">
                    <span style={{ fontSize: 11.5, color: "var(--fg-0)", fontWeight: 500 }}>{f.n}</span>
                    <span className="mono" style={{ fontSize: 9.5, color: "var(--fg-3)" }}>{f.c} 条</span>
                  </div>
                  <div className="col gap-2" style={{ textAlign: "right" }}>
                    <span className="mono" style={{ fontSize: 12, color: "var(--orange)", fontWeight: 600 }}>
                      {stats.inboxTotal ? ((f.c / stats.inboxTotal) * 100).toFixed(1) : "0"}%
                    </span>
                    <span className="kpi-l">占比</span>
                  </div>
                </div>
              )) : (
                <div style={{ padding: 20, textAlign: "center", fontSize: 11, color: "var(--fg-3)" }}>暂无题材数据</div>
              )}
            </div>
          </div>
        </div>

      </div>
    </>
  );
};

Object.assign(window, { Dashboard });
