// Admin: Import / Review / Volunteers / Contributors / Hotspots / Featured / Export / Logs / Settings

// ============ IMPORT ============
function AdminImport({ who }) {
  const s = window.useStore();
  const [drag, setDrag] = React.useState(false);
  const [queue, setQueue] = React.useState([]); // [{id, name, url, status, top3, pickedDeerId}]
  const [csvText, setCsvText] = React.useState("");
  const [mode, setMode] = React.useState("photos"); // photos | csv

  const onFiles = (files) => {
    const items = [...files].map((f, i) => {
      const url = URL.createObjectURL(f);
      return { id: "TMP-" + Date.now() + "-" + i, name: f.name, url, status: "queued", top3: null, picked: null };
    });
    setQueue(q => [...q, ...items]);
  };

  const runAIOne = async (item) => {
    setQueue(q => q.map(x => x.id === item.id ? { ...x, status: "running" } : x));
    try {
      const deerList = s.deer.filter(d => d.verified).map(d => `- ${d.name} (${d.id}) · 物种 ${d.species || "野生动物"} · ${d.region} · ${d.tags.join(", ")}`).join("\n");
      const prompt = `用户批量导入一张大连野生动物照片 (文件名 "${item.name}")。档案库:\n${deerList}\n\n只输出 JSON: {"candidates":[{"id":"DL-XXX","score":<0-100>,"reason":"15字"}, ...3 items]}`;
      const raw = await window.claude.complete(prompt);
      const m = raw.match(/\{[\s\S]*\}/);
      const p = JSON.parse(m ? m[0] : raw);
      const top3 = (p.candidates || []).slice(0, 3);
      setQueue(q => q.map(x => x.id === item.id ? { ...x, status: "done", top3, picked: top3[0]?.id } : x));
    } catch (e) {
      // fallback
      const ids = s.deer.filter(d => d.verified).slice(0, 3);
      const top3 = ids.map((d, i) => ({ id: d.id, score: 70 - i * 15, reason: "离线推测" }));
      setQueue(q => q.map(x => x.id === item.id ? { ...x, status: "done", top3, picked: top3[0]?.id, error: true } : x));
    }
  };

  const runAll = async () => {
    for (const it of queue.filter(x => x.status === "queued")) {
      await runAIOne(it);
    }
  };

  const acceptAll = () => {
    const items = queue.filter(q => q.status === "done" && q.picked).map((q, i) => {
      const deer = s.deer.find(d => d.id === q.picked);
      return {
        type: "photo",
        data: {
          id: "PH-" + (Date.now() + i),
          deerId: q.picked,
          deerName: deer?.name || "",
          species: deer?.species || "",
          speciesKey: deer?.speciesKey || "",
          url: q.url,
          tone: "none",
          role: "gallery",
          taken: new Date().toISOString().slice(0, 10),
          location: deer?.region || "",
          contributor: who,
          tags: [],
          verified: false,
          photoLicense: "本地上传 · 待授权/待审核",
          photoSource: q.name,
          photoSourceNote: "管理员批量导入，需确认上传者授权",
          sourceStatus: "needs-license",
        },
      };
    });
    if (!items.length) return;
    window.dzStore.importBatch(items, who);
    setQueue([]);
    alert(`导入成功：${items.length} 张照片已入库，待后续审核。`);
  };

  const importCSV = () => {
    const lines = csvText.trim().split(/\n/).filter(Boolean);
    if (!lines.length) return;
    const header = lines[0].split(",").map(s => s.trim());
    const items = lines.slice(1).map((ln, i) => {
      const cells = ln.split(",").map(s => s.trim());
      const obj = {}; header.forEach((h, i) => obj[h] = cells[i] || "");
      const n = s.deer.length + i + 1;
      const speciesInput = obj.speciesKey || obj.species || "sika_deer";
      const species = (window.SPECIES_CATALOG || []).find(sp => sp.key === speciesInput || sp.label === speciesInput || sp.en === speciesInput)
        || (window.SPECIES_CATALOG || [])[0]
        || { key: "sika_deer", label: "梅花鹿", en: "Sika deer", group: "mammal" };
      return {
        type: "deer",
        data: {
          id: obj.id || "AN-" + String(n).padStart(3, "0"),
          name: obj.name || "未命名",
          nameEn: obj.nameEn || "Unnamed",
          speciesKey: species.key,
          species: species.label,
          speciesEn: species.en,
          category: species.group || "wildlife",
          firstSeen: obj.firstSeen || new Date().toISOString().slice(0, 10),
          lastSeen: obj.lastSeen || new Date().toISOString().slice(0, 10),
          sightings: Number(obj.sightings) || 0,
          contributors: Number(obj.contributors) || 0,
          sex: obj.sex || "待核",
          estAge: obj.estAge || "待评估",
          region: obj.region || "",
          tags: (obj.tags || "").split("|").filter(Boolean),
          photo: obj.photo || window.DEER_DATA[0].photo,
          photoTone: obj.photoTone || "grayscale(0.3)",
          gallery: [],
          story: obj.story || "",
          palette: ["#28241c", "#78685a", "#b0a294"],
          verified: obj.verified === "true",
          candidate: obj.candidate !== "false",
          photoLicense: obj.photoLicense || "待补充授权",
          photoSource: obj.photoSource || "",
          photoSourceNote: obj.photoSourceNote || "",
          sourceStatus: obj.sourceStatus || (obj.photoLicense ? "licensed-seed" : "needs-license"),
        },
      };
    });
    window.dzStore.importBatch(items, who);
    setCsvText("");
    alert(`成功导入 ${items.length} 条动物档案。`);
  };

  const sampleCSV = `id,name,nameEn,speciesKey,firstSeen,lastSeen,sex,estAge,region,tags,photo,photoLicense,photoSource,story,verified,candidate
AN-009,雾里,Wuli,sika_deer,2026-03-10,2026-04-18,雌 / Female,约 2 岁,老铁山保护区周边,晨雾偏好|左耳缺口,assets/photos/sika-deer-02.jpg,Pexels License,https://www.pexels.com/photo/a-photo-of-a-sika-deer-14582650/,"常在薄雾中出现。",false,true
AN-010,山狐线索,Mountain Fox,fox,2026-04-02,2026-04-21,待核,成年个体 · 待评估,大黑山外围,蓬松尾|夜间活动,assets/photos/fox-02.jpg,Pexels License,https://www.pexels.com/photo/fuchs-wild-animal-predator-animal-world-158340/,"狐狸线索，等待专家定种。",false,true`;

  return (
    <div style={{ display: "grid", gap: 20 }}>
      <div className="admin-toolbar">
        <button className={"btn btn-sm " + (mode === "photos" ? "btn-accent" : "btn-ghost")} onClick={() => setMode("photos")}>照片批量 + AI 识别</button>
        <button className={"btn btn-sm " + (mode === "csv" ? "btn-accent" : "btn-ghost")} onClick={() => setMode("csv")}>CSV / 结构化导入</button>
      </div>

      {mode === "photos" && (
        <>
          <div className={"admin-dropzone" + (drag ? " drag" : "")}
            onDragOver={e => { e.preventDefault(); setDrag(true); }}
            onDragLeave={() => setDrag(false)}
            onDrop={e => { e.preventDefault(); setDrag(false); onFiles(e.dataTransfer.files); }}
            onClick={() => document.getElementById("admin-file-in").click()}>
            <input id="admin-file-in" type="file" multiple accept="image/*" style={{ display: "none" }}
              onChange={e => onFiles(e.target.files)} />
            <div className="serif" style={{ fontSize: 20, marginBottom: 6 }}>拖拽一组照片到这里，或点击选择</div>
            <div className="mono" style={{ fontSize: 11, color: "var(--muted)", letterSpacing: ".12em" }}>
              系统会逐张调用 AI 识别 → 给出 Top-3 候选 → 你确认后入库
            </div>
          </div>

          {queue.length > 0 && (
            <>
              <div className="admin-toolbar">
                <span className="count">队列 {queue.length} 张 · 已完成 {queue.filter(q => q.status === "done").length}</span>
                <div className="sp"></div>
                <button className="btn btn-sm btn-ghost" onClick={() => setQueue([])}>清空</button>
                <button className="btn btn-sm" onClick={runAll}>▶ 运行全部 AI</button>
                <button className="btn btn-sm btn-accent" onClick={acceptAll} disabled={!queue.some(q => q.status === "done")}>
                  接受全部 Top-1 并入库 →
                </button>
              </div>
              <div className="admin-table-wrap">
                <table className="admin-table">
                  <thead>
                    <tr>
                      <th style={{ width: 80 }}>缩略</th>
                      <th>文件名</th>
                      <th style={{ width: 100 }}>状态</th>
                      <th>Top-3 候选</th>
                      <th style={{ width: 180 }}>归属到</th>
                      <th style={{ width: 80 }}></th>
                    </tr>
                  </thead>
                  <tbody>
                    {queue.map(q => (
                      <tr key={q.id}>
                        <td><img src={q.url} style={{ width: 60, height: 60, objectFit: "cover", border: "1px solid var(--rule)" }} /></td>
                        <td className="mono" style={{ fontSize: 11 }}>{q.name}</td>
                        <td>
                          {q.status === "queued" && <span className="pill muted">待识别</span>}
                          {q.status === "running" && <span className="pill warn">分析中…</span>}
                          {q.status === "done" && <span className="pill ok">已识别{q.error ? "（离线）" : ""}</span>}
                        </td>
                        <td>
                          {q.top3 ? q.top3.map((c, i) => {
                            const deer = s.deer.find(d => d.id === c.id);
                            return <div key={i} style={{ fontSize: 11.5, marginBottom: 2 }}>
                              <span className="mono" style={{ color: i === 0 ? "var(--accent)" : "var(--ink)", fontWeight: 600 }}>{c.score}%</span>
                              {" "}{deer?.name || c.id} · {deer?.species || "野生动物"} · <span style={{ color: "var(--muted)" }}>{c.reason}</span>
                            </div>;
                          }) : <span style={{ color: "var(--muted)" }}>—</span>}
                        </td>
                        <td>
                          <select className="cell-select" value={q.picked || ""}
                            onChange={e => setQueue(qq => qq.map(x => x.id === q.id ? { ...x, picked: e.target.value } : x))}>
                            <option value="">— 不归属 —</option>
                            {s.deer.map(d => <option key={d.id} value={d.id}>{d.name} · {d.species || "野生动物"} · {d.id}</option>)}
                          </select>
                        </td>
                        <td>
                          {q.status === "queued" && <button className="btn btn-sm btn-ghost" onClick={() => runAIOne(q)}>识别</button>}
                          <a className="admin-link" onClick={() => setQueue(qq => qq.filter(x => x.id !== q.id))}>移除</a>
                        </td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
            </>
          )}
        </>
      )}

      {mode === "csv" && (
        <>
          <div className="admin-card">
            <div className="admin-card-head">
              <h3>CSV / Excel 导入动物档案</h3>
              <a className="admin-link" onClick={() => setCsvText(sampleCSV)}>载入样例 ↓</a>
            </div>
            <p style={{ fontSize: 12.5, color: "var(--ink-soft)", marginBottom: 12, lineHeight: 1.6 }}>
              第一行为字段名。支持字段：<code>id, name, nameEn, speciesKey, species, firstSeen, lastSeen, sex, estAge, region, tags (用 | 分隔), photo, photoTone, photoLicense, photoSource, photoSourceNote, sourceStatus, story, verified, candidate</code>。粘贴或拖拽 .csv 文件文本到下方即可。
            </p>
            <textarea value={csvText} onChange={e => setCsvText(e.target.value)}
              placeholder="粘贴 CSV 文本…" style={{ width: "100%", minHeight: 240, fontFamily: "JetBrains Mono, monospace", fontSize: 12, padding: 12, border: "1px solid var(--rule)", background: "var(--paper-2)", color: "var(--ink)" }} />
            <div style={{ display: "flex", justifyContent: "flex-end", marginTop: 12, gap: 10 }}>
              <button className="btn btn-sm btn-ghost" onClick={() => setCsvText("")}>清空</button>
              <button className="btn btn-sm btn-accent" onClick={importCSV} disabled={!csvText.trim()}>解析并导入 →</button>
            </div>
          </div>
        </>
      )}
    </div>
  );
}

// ============ REVIEW (admin) ============
function AdminReview({ who }) {
  const s = window.useStore();
  const [openId, setOpenId] = React.useState(null);
  const [activeMedia, setActiveMedia] = React.useState(0);
  const pending = s.sightings.filter(o => o.status === "AI 初审" || o.status === "志愿者审核中");
  const update = (id, patch) => window.dzStore.update("sightings", id, patch, who);
  const open = openId ? s.sightings.find(o => o.id === openId) : null;
  const openDeer = open?.deerId ? s.deer.find(d => d.id === open.deerId) : null;
  const openMedia = open ? getSightingMedia(open, openDeer) : [];
  const currentMedia = openMedia[Math.min(activeMedia, Math.max(0, openMedia.length - 1))];

  React.useEffect(() => setActiveMedia(0), [openId]);

  const appendSupportMedia = (sighting, files) => {
    const accepted = [...(files || [])].filter(f => /^image\//.test(f.type) || /^video\//.test(f.type));
    if (!accepted.length) return;
    const base = Array.isArray(sighting.media)
      ? sighting.media
      : (sighting.photo ? [{
          id: sighting.id + "-MAIN",
          type: sighting.photoMime?.startsWith("video/") ? "video" : "image",
          url: sighting.photo,
          role: "识别主图",
          recognition: true,
        }] : []);
    const additions = accepted.map((f, i) => ({
      id: sighting.id + "-SUP-" + Date.now() + "-" + i,
      type: f.type.startsWith("video/") ? "video" : "image",
      mime: f.type,
      name: f.name,
      size: f.size,
      url: URL.createObjectURL(f),
      role: f.type.startsWith("video/") ? "补充视频" : "补充图片",
      recognition: false,
      note: "审核台补充素材，不进入 AI 识别",
    }));
    update(sighting.id, {
      media: [...base, ...additions],
      supportMediaCount: (sighting.supportMediaCount || 0) + additions.length,
    });
  };

  return (
    <>
      <div className="admin-toolbar">
        <span className="count">{pending.length} 条待处理 · 超级管理员视图</span>
        <div className="sp"></div>
        <button className="btn btn-sm btn-ghost" onClick={() => {
          pending.forEach(o => window.dzStore.setSightingStatus(o.id, "已归档", who));
        }}>全部通过</button>
      </div>
      <div className="admin-table-wrap">
        <table className="admin-table">
          <thead><tr><th style={{ width: 82 }}>预览</th><th>ID</th><th>时间</th><th>地点</th><th>贡献者</th><th>当前归属</th><th>备注</th><th style={{ width: 260 }}>操作</th></tr></thead>
          <tbody>
            {pending.map(o => {
              const deer = o.deerId ? s.deer.find(d => d.id === o.deerId) : null;
              const media = getSightingMedia(o, deer);
              return (
                <tr key={o.id}>
                  <td>
                    <div onClick={() => setOpenId(o.id)} style={{ width: 58, cursor: "pointer" }}>
                      <MediaPreview media={media[0]} tone={o.photoTone || deer?.photoTone} aspect="1 / 1" compact label={media.length > 1 ? `+${media.length - 1}` : ""} />
                    </div>
                  </td>
                  <td className="mono" style={{ fontWeight: 600 }}>{o.id}</td>
                  <td className="mono" style={{ fontSize: 11 }}>{o.time}</td>
                  <td>{o.loc}</td>
                  <td>{o.contributor}</td>
                  <td>{o.deerName}</td>
                  <td style={{ maxWidth: 280, fontSize: 12 }}>{o.note}</td>
                  <td>
                    <button className="btn btn-sm btn-ghost" onClick={() => setOpenId(o.id)}>预览</button>
                    <button className="btn btn-sm btn-ghost" onClick={() => window.dzStore.setSightingStatus(o.id, "已归档", who)}>通过</button>
                    <button className="btn btn-sm btn-ghost" onClick={() => window.dzStore.setSightingStatus(o.id, "驳回", who)}>驳回</button>
                    <button className="btn btn-sm btn-ghost" onClick={() => {
                      const tid = prompt("合并到个体 ID (如 DL-001 / FX-001)：");
                      if (tid) window.dzStore.mergeSighting(o.id, tid, who);
                    }}>合并</button>
                  </td>
                </tr>
              );
            })}
            {pending.length === 0 && <tr><td colSpan={8} style={{ padding: 40, textAlign: "center", color: "var(--muted)" }}>队列为空，做得好。</td></tr>}
          </tbody>
        </table>
      </div>

      <Drawer open={!!open} onClose={() => setOpenId(null)} width={900}
        title={open ? `审核预览 · ${open.id}` : ""}
        subtitle={open ? `${open.time} · ${open.loc}` : ""}
        footer={open ? <>
          <button className="btn btn-ghost" onClick={() => window.dzStore.setSightingStatus(open.id, "驳回", who)}>驳回</button>
          <button className="btn btn-ghost" onClick={() => {
            const tid = prompt("合并到个体 ID (如 DL-001 / FX-001)：", open.deerId || "");
            if (tid) window.dzStore.mergeSighting(open.id, tid, who);
          }}>合并到档案</button>
          <button className="btn btn-accent" onClick={() => window.dzStore.setSightingStatus(open.id, "已归档", who)}>通过归档</button>
        </> : null}>
        {open && (
          <div style={{ display: "grid", gridTemplateColumns: "360px 1fr", gap: 22 }}>
            <div>
              <MediaPreview media={currentMedia} tone={open.photoTone || openDeer?.photoTone} aspect="4 / 5" label={open.loc} />
              <MediaStrip media={openMedia} active={activeMedia} onPick={setActiveMedia} tone={open.photoTone || openDeer?.photoTone} />
              <input id={"review-media-" + open.id} type="file" accept="image/*,video/*" multiple style={{ display: "none" }}
                onChange={e => { appendSupportMedia(open, e.target.files); e.target.value = ""; }} />
              <button className="btn btn-sm btn-ghost" style={{ width: "100%", marginTop: 10 }} onClick={() => document.getElementById("review-media-" + open.id)?.click()}>
                添加非识别补充图片/视频
              </button>
              <div style={{ marginTop: 10, padding: 12, background: "var(--paper-2)", borderLeft: "3px solid var(--moss)", fontSize: 12, color: "var(--ink-soft)", lineHeight: 1.6 }}>
                补充素材只作为人工审核参考，不会进入 AI 相似度比对；适合放现场环境、连续动作、远近景或视频片段。
              </div>
            </div>
            <div>
              <div className="form-label">观察信息</div>
              <div style={{ display: "grid", gap: 8, marginBottom: 18 }}>
                {[
                  ["当前归属", open.deerName || "（待归并）"],
                  ["贡献者", open.contributor || "匿名"],
                  ["状态", open.status],
                  ["补充素材", `${Math.max(0, openMedia.filter(m => m.recognition === false).length)} 个`],
                ].map(([k, v]) => (
                  <div key={k} style={{ display: "grid", gridTemplateColumns: "96px 1fr", gap: 12, fontSize: 12.5 }}>
                    <span className="mono" style={{ color: "var(--muted)", letterSpacing: ".08em" }}>{k}</span>
                    <span>{v}</span>
                  </div>
                ))}
              </div>
              <div style={{ padding: 14, background: "var(--paper-2)", fontSize: 12.5, color: "var(--ink-soft)", lineHeight: 1.65, marginBottom: 18 }}>
                <div className="form-label">观察备注</div>
                {open.note || "无备注"}
              </div>
              <div className="form-label">AI 候选</div>
              <div style={{ display: "grid", gap: 10 }}>
                {(open.aiCandidates || [open.deerId].filter(Boolean).map(id => ({ id, score: Math.round((open.score || .72) * 100), reason: "当前归属" }))).slice(0, 3).map((c, i) => {
                  const d = s.deer.find(x => x.id === c.id) || openDeer;
                  if (!d) return null;
                  return (
                    <div key={i} style={{ display: "flex", gap: 12, border: "1px solid var(--rule)", padding: 10, background: i === 0 ? "var(--paper-2)" : "transparent" }}>
                      <div style={{ width: 64 }}>
                        <MediaPreview media={{ type: "image", url: d.photo, role: "档案主图" }} tone={d.photoTone} aspect="4 / 5" compact />
                      </div>
                      <div style={{ flex: 1 }}>
                        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
                          <span className="serif" style={{ fontSize: 16 }}>{d.name}</span>
                          <span className="mono" style={{ color: i === 0 ? "var(--accent)" : "var(--ink)", fontWeight: 600 }}>{c.score || 0}%</span>
                        </div>
                        <div className="mono" style={{ fontSize: 10, color: "var(--muted)", letterSpacing: ".1em" }}>{d.species || "野生动物"} · {d.id}</div>
                        <div style={{ fontSize: 11.5, color: "var(--ink-soft)", marginTop: 6 }}>{c.reason || d.tags?.slice(0, 2).join(" · ")}</div>
                      </div>
                    </div>
                  );
                })}
              </div>
            </div>
          </div>
        )}
      </Drawer>
    </>
  );
}

// ============ VOLUNTEERS (CERTIFICATION WORKFLOW) ============
function AdminVolunteers({ who }) {
  const s = window.useStore();
  const [tab, setTab] = React.useState("pending");
  const [openId, setOpenId] = React.useState(null);
  const [, bump] = React.useReducer(x => x + 1, 0);

  const update = (id, patch) => window.dzStore.update("volunteers", id, patch, who);

  const tabs = [
    { k: "pending", zh: "待认证", en: "Pending", test: v => v.certStatus === "pending" },
    { k: "certified", zh: "已认证", en: "Certified", test: v => v.certStatus === "certified" },
    { k: "revoked", zh: "已吊销", en: "Revoked", test: v => v.certStatus === "revoked" },
    { k: "all", zh: "全部", en: "All", test: () => true },
  ];
  const cur = tabs.find(t => t.k === tab);
  const list = s.volunteers.filter(cur.test);
  const open = openId ? s.volunteers.find(v => v.id === openId) : null;

  const certify = (id) => {
    const v = s.volunteers.find(x => x.id === id);
    if (!v) return;
    const note = prompt(`认证 ${v.name} 为审核员。\n请填写认证备注（例：通过线下培训 + 3 次试审）：`, v.certNote || "");
    if (note === null) return;
    const roleChoice = prompt("初始角色：\n1) 实习审核员\n2) 审核员\n3) 高级审核员\n\n输入数字 1/2/3", "1");
    const roleMap = { "1": "实习审核员", "2": "审核员", "3": "高级审核员" };
    const role = roleMap[roleChoice] || "实习审核员";
    update(id, {
      certStatus: "certified",
      certifiedAt: new Date().toISOString().slice(0, 10),
      certifiedBy: who,
      certNote: note,
      role,
      active: true,
      revokedAt: undefined,
      revokedBy: undefined,
    });
    // Offer to create linked login
    if (!v.loginUser) {
      if (confirm(`认证成功！\n\n是否同时为 ${v.name} 创建登录账号？\n（角色：volunteer，可登录审核台）`)) {
        createLogin(id, v);
      }
    }
  };

  const revoke = (id) => {
    const v = s.volunteers.find(x => x.id === id);
    if (!v) return;
    const reason = prompt(`吊销 ${v.name} 的审核员资格。\n请填写原因：`, "");
    if (reason === null) return;
    update(id, {
      certStatus: "revoked",
      revokedAt: new Date().toISOString().slice(0, 10),
      revokedBy: who,
      certNote: reason,
      active: false,
      role: "已吊销",
    });
    // Disable login if any
    if (v.loginUser) {
      const accounts = window.dzAuth.listAccounts();
      const acc = accounts.find(a => a.username === v.loginUser);
      if (acc && acc.active) {
        window.dzAuth.toggleActive(v.loginUser);
      }
    }
  };

  const reject = (id) => {
    const v = s.volunteers.find(x => x.id === id);
    if (!v) return;
    if (!confirm(`驳回 ${v.name} 的申请？（将从列表移除）`)) return;
    const reason = prompt("驳回说明（可留空）：", "");
    window.dzStore.remove("volunteers", id, who);
  };

  const createLogin = (id, v0) => {
    const v = v0 || s.volunteers.find(x => x.id === id);
    if (!v) return;
    let username = v.loginUser || prompt("为该审核员设置登录用户名：", v.name);
    if (!username) return;
    const existing = window.dzAuth.listAccounts().find(a => a.username === username);
    if (existing) {
      alert("该用户名已存在，请换一个。");
      return;
    }
    const pwd = prompt("设置初始密码（至少 4 位）：", "review" + Math.floor(Math.random() * 9000 + 1000));
    if (!pwd || pwd.length < 4) { alert("密码太短，未创建。"); return; }
    window.dzAuth.createAccount({ username, password: pwd, role: "volunteer" });
    update(id, { loginUser: username });
    alert(`已创建登录账号：\n\n用户名：${username}\n初始密码：${pwd}\n\n请线下转交本人，首次登录后建议重置。`);
  };

  const resetPwd = (v) => {
    if (!v.loginUser) { alert("此审核员尚未创建登录账号"); return; }
    const pwd = prompt(`为 ${v.loginUser} 重置密码：`, "reset" + Math.floor(Math.random() * 9000 + 1000));
    if (!pwd || pwd.length < 4) return;
    window.dzAuth.resetPassword(v.loginUser, pwd);
    alert(`已重置。新密码：${pwd}`);
  };

  const unlinkLogin = (v) => {
    if (!v.loginUser) return;
    if (!confirm(`解除 ${v.name} 与登录账号 ${v.loginUser} 的绑定？\n（登录账号本身保留，仅断开关联）`)) return;
    update(v.id, { loginUser: "" });
  };

  const addManual = () => {
    const n = s.volunteers.length + 1;
    window.dzStore.add("volunteers", {
      id: "V-" + String(n).padStart(2, "0"),
      name: "新审核员",
      role: "申请人",
      since: new Date().toISOString().slice(0, 10),
      reviews: 0, specialty: "",
      active: false,
      certStatus: "pending",
      certNote: "",
      loginUser: "",
      contact: "",
      appliedAt: new Date().toISOString().slice(0, 10),
      appliedStatement: "",
    }, who);
  };

  const statusChip = (v) => {
    if (v.certStatus === "pending") return <span className="chip warn">待认证</span>;
    if (v.certStatus === "certified") return <span className="chip ok">已认证</span>;
    if (v.certStatus === "revoked") return <span className="chip" style={{ background: "rgba(180,60,50,.12)", color: "#a93a2a", borderColor: "rgba(180,60,50,.3)" }}>已吊销</span>;
    return <span className="chip">未知</span>;
  };

  const counts = {
    pending: s.volunteers.filter(v => v.certStatus === "pending").length,
    certified: s.volunteers.filter(v => v.certStatus === "certified").length,
    revoked: s.volunteers.filter(v => v.certStatus === "revoked").length,
    all: s.volunteers.length,
  };

  return (
    <>
      <div className="admin-tabs">
        {tabs.map(t => (
          <button key={t.k} className={"admin-tab" + (tab === t.k ? " active" : "")} onClick={() => setTab(t.k)}>
            <span>{t.zh}</span>
            <span className="en">{t.en}</span>
            <span className="admin-tab-count">{counts[t.k]}</span>
          </button>
        ))}
        <div className="sp"></div>
        <button className="btn btn-sm btn-ghost" onClick={addManual}>+ 手动录入申请</button>
      </div>

      <div className="admin-table-wrap">
        <table className="admin-table">
          <thead>
            <tr>
              <th style={{ width: 80 }}>ID</th>
              <th style={{ width: 140 }}>昵称</th>
              <th style={{ width: 120 }}>角色</th>
              <th style={{ width: 160 }}>专长片区</th>
              <th style={{ width: 100 }}>审核次数</th>
              <th style={{ width: 110 }}>{tab === "pending" ? "申请于" : "加入/认证"}</th>
              <th style={{ width: 150 }}>登录账号</th>
              <th style={{ width: 100 }}>认证状态</th>
              <th>操作</th>
            </tr>
          </thead>
          <tbody>
            {list.map(v => (
              <tr key={v.id}>
                <td className="mono" style={{ fontWeight: 600 }}>{v.id}</td>
                <td><EditableCell value={v.name} onChange={x => update(v.id, { name: x })} /></td>
                <td>
                  {v.certStatus === "certified" ? (
                    <EditableSelect value={v.role} options={["实习审核员", "审核员", "高级审核员", "片区负责人"]} onChange={x => update(v.id, { role: x })} />
                  ) : (
                    <span style={{ color: "var(--muted)", fontSize: 12 }}>{v.role}</span>
                  )}
                </td>
                <td><EditableCell value={v.specialty} onChange={x => update(v.id, { specialty: x })} placeholder="例：棒棰岛片区" /></td>
                <td className="mono">{v.reviews || 0}</td>
                <td className="mono" style={{ fontSize: 11, color: "var(--muted)" }}>
                  {tab === "pending" ? (v.appliedAt || v.since) : (v.certifiedAt || v.since)}
                </td>
                <td>
                  {v.loginUser ? (
                    <span className="mono" style={{ fontSize: 11.5, color: "var(--ink)", fontWeight: 600 }}>
                      ● {v.loginUser}
                    </span>
                  ) : (
                    <span style={{ fontSize: 11, color: "var(--muted)" }}>—</span>
                  )}
                </td>
                <td>{statusChip(v)}</td>
                <td style={{ whiteSpace: "nowrap" }}>
                  <a className="admin-link" onClick={() => setOpenId(v.id)}>详情</a>
                  {v.certStatus === "pending" && <>
                    <span className="admin-sep">·</span>
                    <a className="admin-link" style={{ color: "var(--accent)" }} onClick={() => certify(v.id)}>认证</a>
                    <span className="admin-sep">·</span>
                    <a className="admin-link" style={{ color: "var(--candidate)" }} onClick={() => reject(v.id)}>驳回</a>
                  </>}
                  {v.certStatus === "certified" && <>
                    <span className="admin-sep">·</span>
                    {!v.loginUser ? (
                      <a className="admin-link" onClick={() => createLogin(v.id)}>创建登录</a>
                    ) : (
                      <a className="admin-link" onClick={() => resetPwd(v)}>重置密码</a>
                    )}
                    <span className="admin-sep">·</span>
                    <a className="admin-link" style={{ color: "var(--candidate)" }} onClick={() => revoke(v.id)}>吊销</a>
                  </>}
                  {v.certStatus === "revoked" && <>
                    <span className="admin-sep">·</span>
                    <a className="admin-link" style={{ color: "var(--accent)" }} onClick={() => certify(v.id)}>重新认证</a>
                  </>}
                </td>
              </tr>
            ))}
            {list.length === 0 && (
              <tr><td colSpan={9} style={{ padding: 40, textAlign: "center", color: "var(--muted)" }}>
                {tab === "pending" ? "当前没有待认证的申请" : tab === "certified" ? "还没有已认证的审核员" : tab === "revoked" ? "没有被吊销的审核员" : "没有记录"}
              </td></tr>
            )}
          </tbody>
        </table>
      </div>

      <Drawer open={!!open} onClose={() => setOpenId(null)}
        title={open ? `审核员 · ${open.name}` : ""}
        subtitle={open ? `${open.id} · ${open.certStatus === "pending" ? "待认证" : open.certStatus === "certified" ? "已认证" : "已吊销"}` : ""}>
        {open && (
          <div>
            <div className="admin-form-row"><label>ID</label><div><span className="mono">{open.id}</span></div></div>
            <div className="admin-form-row"><label>昵称</label><div><EditableCell value={open.name} onChange={v => update(open.id, { name: v })} /></div></div>
            <div className="admin-form-row"><label>联系方式</label><div><EditableCell value={open.contact} onChange={v => update(open.id, { contact: v })} placeholder="邮箱 / 手机 / 微信" /></div></div>
            <div className="admin-form-row"><label>专长片区</label><div><EditableCell value={open.specialty} onChange={v => update(open.id, { specialty: v })} /></div></div>
            <div className="admin-form-row"><label>申请日期</label><div><span className="mono">{open.appliedAt || open.since}</span></div></div>

            {open.certStatus === "pending" && (
              <>
                <div className="admin-form-row" style={{ alignItems: "flex-start" }}>
                  <label>申请陈述</label>
                  <div>
                    <div style={{ padding: 12, background: "var(--paper-2)", borderLeft: "3px solid var(--accent)", fontSize: 12.5, lineHeight: 1.7, color: "var(--ink-soft)" }}>
                      {open.appliedStatement || "（无申请陈述）"}
                    </div>
                  </div>
                </div>
                <div style={{ marginTop: 20, padding: 16, background: "rgba(200,122,73,.06)", border: "1px solid rgba(200,122,73,.18)" }}>
                  <div className="mono" style={{ fontSize: 10.5, letterSpacing: ".14em", color: "var(--accent)", marginBottom: 8 }}>CERTIFICATION CHECKLIST</div>
                  <ul style={{ fontSize: 12.5, color: "var(--ink-soft)", lineHeight: 1.8, marginLeft: 18 }}>
                    <li>核对已有上传记录的准确率（建议 ≥ 10 条且通过率 80%+）</li>
                    <li>线下或视频会面 / 片区熟悉度评估</li>
                    <li>对"文明观察公约"的书面确认</li>
                    <li>为其分配初始片区与试审范围</li>
                  </ul>
                  <div style={{ display: "flex", gap: 10, marginTop: 14 }}>
                    <button className="btn btn-sm btn-accent" onClick={() => { certify(open.id); setOpenId(null); }}>通过认证 →</button>
                    <button className="btn btn-sm btn-ghost" onClick={() => { reject(open.id); setOpenId(null); }}>驳回申请</button>
                  </div>
                </div>
              </>
            )}

            {open.certStatus === "certified" && (
              <>
                <div className="admin-form-row"><label>认证于</label><div><span className="mono">{open.certifiedAt} · by {open.certifiedBy}</span></div></div>
                <div className="admin-form-row" style={{ alignItems: "flex-start" }}>
                  <label>认证备注</label>
                  <div><EditableCell value={open.certNote} onChange={v => update(open.id, { certNote: v })} multiline /></div>
                </div>
                <div className="admin-form-row"><label>审核次数</label><div><EditableCell value={open.reviews} onChange={v => update(open.id, { reviews: Number(v) || 0 })} format="number" /></div></div>
                <div className="admin-form-row"><label>状态</label><div>
                  <EditableSelect value={open.active ? "active" : "paused"} options={[["active", "活跃"], ["paused", "暂停"]]} onChange={v => update(open.id, { active: v === "active" })} />
                </div></div>
                <div style={{ marginTop: 20, padding: 16, background: "var(--paper-2)", border: "1px solid var(--rule)" }}>
                  <div className="mono" style={{ fontSize: 10.5, letterSpacing: ".14em", color: "var(--muted)", marginBottom: 8 }}>LOGIN ACCOUNT</div>
                  {open.loginUser ? (
                    <>
                      <div style={{ fontSize: 13, marginBottom: 10 }}>
                        已绑定 · <span className="mono" style={{ fontWeight: 600 }}>{open.loginUser}</span>
                      </div>
                      <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
                        <button className="btn btn-sm btn-ghost" onClick={() => resetPwd(open)}>重置密码</button>
                        <button className="btn btn-sm btn-ghost" onClick={() => unlinkLogin(open)}>解除绑定</button>
                      </div>
                    </>
                  ) : (
                    <>
                      <div style={{ fontSize: 13, color: "var(--muted)", marginBottom: 10 }}>尚未创建登录账号。没有账号则无法登录审核台。</div>
                      <button className="btn btn-sm btn-accent" onClick={() => createLogin(open.id, open)}>+ 创建登录账号</button>
                    </>
                  )}
                </div>
                <div style={{ marginTop: 14 }}>
                  <button className="btn btn-sm" style={{ borderColor: "var(--candidate)", color: "var(--candidate)" }}
                    onClick={() => { revoke(open.id); setOpenId(null); }}>吊销认证</button>
                </div>
              </>
            )}

            {open.certStatus === "revoked" && (
              <>
                <div className="admin-form-row"><label>吊销于</label><div><span className="mono">{open.revokedAt} · by {open.revokedBy}</span></div></div>
                <div className="admin-form-row" style={{ alignItems: "flex-start" }}>
                  <label>吊销原因</label>
                  <div><EditableCell value={open.certNote} onChange={v => update(open.id, { certNote: v })} multiline /></div>
                </div>
                {open.loginUser && (
                  <div style={{ padding: 12, background: "rgba(180,60,50,.08)", border: "1px solid rgba(180,60,50,.2)", fontSize: 12.5, color: "#8f3220", marginTop: 12 }}>
                    登录账号 <span className="mono">{open.loginUser}</span> 已被停用。
                  </div>
                )}
                <div style={{ marginTop: 16 }}>
                  <button className="btn btn-sm btn-accent" onClick={() => { certify(open.id); setOpenId(null); }}>重新认证</button>
                </div>
              </>
            )}
          </div>
        )}
      </Drawer>
    </>
  );
}

function AdminContributors({ who }) {
  const s = window.useStore();
  const update = (id, patch) => window.dzStore.update("contributors", id, patch, who);
  const add = () => {
    const n = s.contributors.length + 1;
    window.dzStore.add("contributors", { id: "C-" + String(n).padStart(2, "0"), name: "新贡献者", uploads: 0, verified: 0, points: 0, joined: new Date().toISOString().slice(0, 10), contact: "—" }, who);
  };
  return (
    <>
      <div className="admin-toolbar">
        <span className="count">{s.contributors.length} 位贡献者</span>
        <div className="sp"></div>
        <button className="btn btn-sm btn-accent" onClick={add}>+ 新增贡献者</button>
      </div>
      <div className="admin-table-wrap">
        <table className="admin-table">
          <thead><tr><th>ID</th><th>昵称</th><th>上传</th><th>已验证</th><th>积分</th><th>加入</th><th>联系方式</th><th></th></tr></thead>
          <tbody>
            {s.contributors.map(c => (
              <tr key={c.id}>
                <td className="mono" style={{ fontWeight: 600 }}>{c.id}</td>
                <td><EditableCell value={c.name} onChange={x => update(c.id, { name: x })} /></td>
                <td><EditableCell value={c.uploads} onChange={x => update(c.id, { uploads: Number(x) || 0 })} format="number" /></td>
                <td><EditableCell value={c.verified} onChange={x => update(c.id, { verified: Number(x) || 0 })} format="number" /></td>
                <td><EditableCell value={c.points} onChange={x => update(c.id, { points: Number(x) || 0 })} format="number" /></td>
                <td><EditableCell value={c.joined} onChange={x => update(c.id, { joined: x })} format="date" /></td>
                <td><EditableCell value={c.contact} onChange={x => update(c.id, { contact: x })} /></td>
                <td><a className="admin-link" onClick={() => { if (confirm("删除此贡献者？")) window.dzStore.remove("contributors", c.id, who); }}>删除</a></td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </>
  );
}

// ============ HOTSPOTS ============
function AdminHotspots({ who }) {
  const s = window.useStore();
  // Ensure every hotspot has a stable id
  const list = s.hotspots.map((h, i) => ({ ...h, id: h.id || ("HS-" + String(i + 1).padStart(2, "0")) }));
  React.useEffect(() => {
    const missing = s.hotspots.some(h => !h.id);
    if (missing) {
      const state = window.dzStore.get();
      state.hotspots = list;
      // Nudge a save via a no-op log-free path: use setFeatured with existing featured
      window.dzStore.setFeatured(state.featured, "system");
    }
  }, []);

  const addHS = () => {
    const n = list.length + 1;
    window.dzStore.add("hotspots", { id: "HS-" + String(n).padStart(2, "0"), name: "新热点", x: 50, y: 50, count: 0, ratio: 0 }, who);
  };
  const u = (id, patch) => window.dzStore.update("hotspots", id, patch, who);

  return (
    <>
      <div className="admin-toolbar">
        <span className="count">{list.length} 个热点</span>
        <div className="sp"></div>
        <button className="btn btn-sm btn-accent" onClick={addHS}>+ 新增热点</button>
      </div>
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 20 }}>
        <div className="admin-table-wrap">
          <table className="admin-table">
            <thead><tr><th>ID</th><th>名称</th><th>X%</th><th>Y%</th><th>次数</th><th>占比</th><th></th></tr></thead>
            <tbody>
              {list.map(h => (
                <tr key={h.id}>
                  <td className="mono" style={{ fontWeight: 600 }}>{h.id}</td>
                  <td><EditableCell value={h.name} onChange={v => u(h.id, { name: v })} /></td>
                  <td><EditableCell value={h.x} onChange={v => u(h.id, { x: Number(v) || 0 })} format="number" /></td>
                  <td><EditableCell value={h.y} onChange={v => u(h.id, { y: Number(v) || 0 })} format="number" /></td>
                  <td><EditableCell value={h.count} onChange={v => u(h.id, { count: Number(v) || 0 })} format="number" /></td>
                  <td><EditableCell value={h.ratio} onChange={v => u(h.id, { ratio: Number(v) || 0 })} format="number" /></td>
                  <td><a className="admin-link" onClick={() => { if (confirm("删除此热点？")) window.dzStore.remove("hotspots", h.id, who); }}>×</a></td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
        <div className="admin-card">
          <div className="admin-card-head"><h3>预览 · 点击地图设定坐标</h3></div>
          <div style={{ position: "relative", aspectRatio: "8/5", background: "var(--paper-2)", border: "1px solid var(--rule)", cursor: "crosshair" }}
            onClick={e => {
              const r = e.currentTarget.getBoundingClientRect();
              const x = Math.round(((e.clientX - r.left) / r.width) * 100);
              const y = Math.round(((e.clientY - r.top) / r.height) * 100);
              const id = prompt(`在 (${x}%, ${y}%) 新建/移动热点\n\n留空 = 新建\n填 ID = 移动该热点:\n${list.map(l => "  " + l.id + " · " + l.name).join("\n")}`);
              if (id === null) return;
              if (id === "") {
                const n = list.length + 1;
                window.dzStore.add("hotspots", { id: "HS-" + String(n).padStart(2, "0"), name: "新热点", x, y, count: 0, ratio: 0 }, who);
              } else if (list.find(l => l.id === id)) {
                u(id, { x, y });
              } else {
                alert("未找到该 ID");
              }
            }}>
            {list.map(h => (
              <div key={h.id} style={{ position: "absolute", left: h.x + "%", top: h.y + "%", transform: "translate(-50%, -50%)", width: 14 + (h.ratio || 0) * 30, height: 14 + (h.ratio || 0) * 30, borderRadius: "50%", background: "var(--accent)", opacity: .7, boxShadow: "0 0 0 2px var(--paper)", pointerEvents: "none" }}>
                <div style={{ position: "absolute", top: "100%", left: "50%", transform: "translateX(-50%)", fontFamily: "JetBrains Mono, monospace", fontSize: 10, color: "var(--ink)", whiteSpace: "nowrap", marginTop: 4 }}>{h.name}</div>
              </div>
            ))}
          </div>
          <p style={{ fontSize: 11.5, color: "var(--muted)", marginTop: 10 }}>点击地图空白处添加，或输入 ID 移动已有热点。</p>
        </div>
      </div>
    </>
  );
}

// ============ FEATURED ============
function AdminFeatured({ who }) {
  const s = window.useStore();
  const verified = s.deer.filter(d => d.verified);
  const feat = s.featured || [];
  const toggle = (id) => {
    const next = feat.includes(id) ? feat.filter(x => x !== id) : [...feat, id];
    window.dzStore.setFeatured(next, who);
  };
  return (
    <>
      <div className="admin-toolbar">
        <span className="count">首页推荐位 · 当前 {feat.length} 只</span>
        <div className="sp"></div>
        <span className="mono" style={{ fontSize: 10.5, color: "var(--muted)", letterSpacing: ".1em" }}>建议 3–4 只</span>
      </div>
      <div className="admin-photo-grid" style={{ gridTemplateColumns: "repeat(4, 1fr)" }}>
        {verified.map(d => {
          const on = feat.includes(d.id);
          return (
            <div key={d.id} className={"admin-photo" + (on ? " selected" : "")} onClick={() => toggle(d.id)}>
              <img src={d.photo} style={{ filter: d.photoTone }} />
              <div className="admin-photo-check">{on ? "✓" : "＋"}</div>
              <div className="admin-photo-meta"><span>{d.name}</span><span>{d.id}</span></div>
            </div>
          );
        })}
      </div>
    </>
  );
}

// ============ EXPORT ============
function AdminExport({ who }) {
  const s = window.useStore();
  const downloadText = (filename, content) => {
    const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob); a.download = filename; a.click();
  };
  return (
    <div style={{ display: "grid", gap: 14 }}>
      <div className="admin-card">
        <div className="admin-card-head"><h3>数据导出</h3></div>
        <p style={{ fontSize: 12.5, color: "var(--ink-soft)", marginBottom: 12 }}>下载当前 localStorage 中的数据，用于备份、迁移或离线分析。</p>
        <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 10 }}>
          {[
            ["deer", "动物档案"],
            ["sightings", "观察记录"],
            ["photos", "照片库"],
            ["volunteers", "志愿审核员"],
            ["contributors", "贡献者"],
            ["hotspots", "热点地点"],
          ].map(([k, l]) => (
            <div key={k} className="card" style={{ padding: 16 }}>
              <div className="serif" style={{ fontSize: 16 }}>{l}</div>
              <div className="mono" style={{ fontSize: 10.5, color: "var(--muted)", letterSpacing: ".1em", marginTop: 2 }}>{(s[k] || []).length} 条</div>
              <div style={{ display: "flex", gap: 6, marginTop: 12 }}>
                <button className="btn btn-sm btn-ghost" onClick={() => downloadText(`dz_${k}.csv`, window.dzStore.exportCSV(k))}>CSV</button>
                <button className="btn btn-sm btn-ghost" onClick={() => downloadText(`dz_${k}.json`, JSON.stringify(s[k], null, 2))}>JSON</button>
              </div>
            </div>
          ))}
        </div>
      </div>
      <div className="admin-card">
        <div className="admin-card-head"><h3>整体快照</h3></div>
        <p style={{ fontSize: 12.5, color: "var(--ink-soft)", marginBottom: 12 }}>导出完整的系统状态（JSON）。</p>
        <button className="btn btn-sm btn-accent" onClick={() => downloadText("dz_snapshot.json", window.dzStore.exportJSON())}>下载整体 JSON →</button>
      </div>
    </div>
  );
}

// ============ LOGS ============
function AdminLogs() {
  const s = window.useStore();
  return (
    <div className="admin-table-wrap">
      <table className="admin-table">
        <thead><tr><th style={{ width: 130 }}>时间</th><th style={{ width: 100 }}>操作者</th><th style={{ width: 80 }}>类型</th><th>对象</th><th>详情</th></tr></thead>
        <tbody>
          {s.logs.map((l, i) => (
            <tr key={i}>
              <td className="mono" style={{ fontSize: 11 }}>{l.t}</td>
              <td className="mono" style={{ fontSize: 11 }}>{l.who}</td>
              <td><span className={"admin-tag " + l.action}>{l.action}</span></td>
              <td style={{ fontWeight: 500 }}>{l.target}</td>
              <td style={{ color: "var(--ink-soft)" }}>{l.detail}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

// ============ SETTINGS ============
function AdminSettings({ who }) {
  const s = window.useStore();
  const labels = {
    knownDeer: "已命名个体数 · Named",
    candidateDeer: "候选个体 · Candidates",
    sightings: "累计观察 · Sightings",
    contributors: "贡献者 · Contributors",
    volunteers: "志愿审核员 · Volunteers",
    regions: "覆盖片区 · Regions",
    photos: "照片 · Photos",
    schools: "学校合作 · Schools",
  };
  return (
    <div className="admin-card" style={{ maxWidth: 760 }}>
      <div className="admin-card-head"><h3>项目设置 · 首页统计数字</h3></div>
      <p style={{ fontSize: 12.5, color: "var(--ink-soft)", marginBottom: 18 }}>这些数字会显示在主站首页的 STATS 区块。</p>
      {Object.entries(s.stats).map(([k, v]) => (
        <div key={k} className="admin-form-row">
          <label>{labels[k] || k}</label>
          <div>
            <input type="number" defaultValue={v}
              onBlur={e => window.dzStore.setStat(k, Number(e.target.value) || 0, who)}
              onKeyDown={e => { if (e.key === "Enter") e.currentTarget.blur(); }}
              style={{ width: 160 }} />
          </div>
        </div>
      ))}
    </div>
  );
}

// ============ HERO CAROUSEL ============
function AdminHero({ who }) {
  const s = window.useStore();
  const slides = s.heroSlides || [];
  const deerOpts = [["", "— 无关联 —"], ...s.deer.map(d => [d.id, `${d.name} · ${d.id}`])];
  const [previewIdx, setPreviewIdx] = React.useState(0);

  React.useEffect(() => {
    if (slides.filter(x => x.active !== false).length < 2) return;
    const active = slides.filter(x => x.active !== false);
    const t = setInterval(() => setPreviewIdx(i => (i + 1) % active.length), 3000);
    return () => clearInterval(t);
  }, [slides.length]);

  const u = (id, patch) => window.dzStore.update("heroSlides", id, patch, who);
  const del = (id) => { if (confirm("从轮播中移除这张？")) window.dzStore.remove("heroSlides", id, who); };
  const move = (id, dir) => window.dzStore.move("heroSlides", id, dir, who);

  const addFromDeer = () => {
    const pickId = prompt("从动物档案添加：输入个体 ID（如 DL-003 / FX-001）");
    if (!pickId) return;
    const d = s.deer.find(x => x.id === pickId);
    if (!d) { alert("未找到该个体"); return; }
    const n = slides.length + 1;
    window.dzStore.add("heroSlides", {
      id: "HS-" + String(100 + Math.floor(Math.random() * 900)).padStart(3, "0") + "-" + n,
      deerId: d.id, deerName: d.name, url: d.photo, tone: d.photoTone || "none",
      caption: (d.tags || []).slice(0, 2).join(" · "), region: d.region, active: true,
    }, who);
  };

  const addFromURL = () => {
    const url = prompt("粘贴一张照片 URL：");
    if (!url) return;
    const n = slides.length + 1;
    window.dzStore.add("heroSlides", {
      id: "HS-CUST-" + String(n).padStart(3, "0"),
      deerId: "", deerName: "（未关联）", url, tone: "none",
      caption: "自定义宣传位", region: "", active: true,
    }, who);
  };

  const active = slides.filter(x => x.active !== false);
  const preview = active[previewIdx % Math.max(1, active.length)];

  return (
    <>
      <div className="admin-toolbar">
        <span className="count">{slides.length} 张轮播图</span>
        <span className="count" style={{ color: "var(--accent)" }}>· 启用 {active.length} 张</span>
        <div className="sp"></div>
        <button className="btn btn-sm btn-ghost" onClick={addFromDeer}>+ 从动物档案添加</button>
        <button className="btn btn-sm btn-accent" onClick={addFromURL}>+ 自定义 URL</button>
      </div>

      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 24 }}>
        {/* Slot list */}
        <div>
          <div className="admin-nav-label" style={{ padding: "0 0 10px" }}>轮播顺序 · 上下箭头排序</div>
          {slides.map((sl, idx) => (
            <div key={sl.id} className="hero-slot" style={{ opacity: sl.active === false ? .45 : 1 }}>
              <div className="hero-slot-thumb" style={{
                backgroundImage: `url(${sl.url})`,
                filter: sl.tone && sl.tone !== "none" ? sl.tone : undefined,
              }} />
              <div className="hero-slot-meta">
                <span className="k">ID</span>
                <span className="mono" style={{ fontWeight: 600 }}>{sl.id}</span>
                <span className="k">关联个体</span>
                <EditableSelect value={sl.deerId || ""} options={deerOpts}
                  onChange={v => {
                    const d = s.deer.find(x => x.id === v);
                    u(sl.id, { deerId: v, deerName: d ? d.name : "", region: d ? d.region : sl.region, url: d ? d.photo : sl.url, tone: d ? d.photoTone : sl.tone });
                  }} />
                <span className="k">图片 URL</span>
                <EditableCell value={sl.url} onChange={v => u(sl.id, { url: v })} />
                <span className="k">文字</span>
                <EditableCell value={sl.caption} onChange={v => u(sl.id, { caption: v })} placeholder="特征 / 故事一句话" />
                <span className="k">副信息</span>
                <EditableCell value={sl.region} onChange={v => u(sl.id, { region: v })} placeholder="区域" />
                <span className="k">色调</span>
                <EditableCell value={sl.tone} onChange={v => u(sl.id, { tone: v })} />
                <span className="k">启用</span>
                <label style={{ fontSize: 12.5, display: "flex", alignItems: "center", gap: 6 }}>
                  <input type="checkbox" checked={sl.active !== false} onChange={e => u(sl.id, { active: e.target.checked })} />
                  {sl.active !== false ? "展示中" : "已隐藏"}
                </label>
              </div>
              <div className="hero-slot-actions">
                <button className="btn btn-sm btn-ghost" style={{ padding: "2px 8px" }} disabled={idx === 0} onClick={() => move(sl.id, -1)}>↑</button>
                <button className="btn btn-sm btn-ghost" style={{ padding: "2px 8px" }} disabled={idx === slides.length - 1} onClick={() => move(sl.id, 1)}>↓</button>
                <button className="btn btn-sm" style={{ padding: "2px 8px", borderColor: "var(--candidate)", color: "var(--candidate)" }} onClick={() => del(sl.id)}>×</button>
              </div>
            </div>
          ))}
          {slides.length === 0 && (
            <div style={{ padding: 40, textAlign: "center", color: "var(--muted)", border: "1px dashed var(--rule)" }}>
              还没有轮播图，点击上方添加。
            </div>
          )}
        </div>

        {/* Live preview */}
        <div>
          <div className="admin-nav-label" style={{ padding: "0 0 10px" }}>实时预览 · 主站首页 Hero</div>
          <div style={{ position: "relative", aspectRatio: "16/10", overflow: "hidden", background: "var(--ink)" }}>
            {active.map((sl, i) => (
              <div key={sl.id} style={{
                position: "absolute", inset: 0,
                backgroundImage: `url(${sl.url})`, backgroundSize: "cover", backgroundPosition: "center",
                filter: sl.tone && sl.tone !== "none" ? sl.tone : undefined,
                opacity: i === previewIdx % active.length ? 1 : 0,
                transition: "opacity 1.2s ease",
              }} />
            ))}
            <div style={{
              position: "absolute", inset: 0,
              background: "linear-gradient(100deg, rgba(22,20,18,.8) 0%, rgba(22,20,18,.2) 55%, transparent 85%)",
            }} />
            <div style={{ position: "absolute", left: 20, bottom: 20, color: "#f3e4be", maxWidth: "60%" }}>
              <div className="mono" style={{ fontSize: 10.5, letterSpacing: ".14em", color: "rgba(240,224,186,.7)" }}>● {preview?.deerId || "HS"}</div>
              <div className="serif" style={{ fontSize: 22, marginTop: 4, lineHeight: 1.25 }}>{preview?.caption || "—"}</div>
              <div className="mono" style={{ fontSize: 10.5, color: "rgba(240,224,186,.55)", marginTop: 6 }}>{preview?.region || ""}</div>
            </div>
          </div>
          <div style={{ marginTop: 10, display: "flex", gap: 6, justifyContent: "center" }}>
            {active.map((_, i) => (
              <button key={i} onClick={() => setPreviewIdx(i)}
                style={{ width: 22, height: 3, border: "none", cursor: "pointer", padding: 0,
                  background: i === previewIdx % active.length ? "var(--accent)" : "var(--rule)" }} />
            ))}
          </div>
          <p style={{ fontSize: 11.5, color: "var(--muted)", marginTop: 14, lineHeight: 1.6 }}>
            首页 Hero 区域会以约 5 秒节奏轮播"启用"中的图。关闭 active 即临时下架，不删除。
          </p>
        </div>
      </div>
    </>
  );
}

// ============ ACCOUNTS ============
function AdminAccounts({ who }) {
  const accounts = window.dzAuth.listAccounts();
  const [, bump] = React.useReducer(x => x + 1, 0);

  const addUser = () => {
    const username = prompt("新用户名：");
    if (!username) return;
    if (accounts.find(a => a.username === username)) { alert("该用户名已存在"); return; }
    const role = prompt("角色：super / admin / volunteer", "admin");
    const pwd = prompt("初始密码（6 位以上）：");
    if (!pwd || pwd.length < 4) { alert("密码太短"); return; }
    window.dzAuth.createAccount({ username, role: role || "admin", password: pwd });
    bump();
  };

  const resetPwd = (u) => {
    const pwd = prompt(`为 ${u} 重置密码：`);
    if (!pwd || pwd.length < 4) return;
    window.dzAuth.resetPassword(u, pwd);
    alert("已重置");
  };

  const toggleActive = (u) => {
    window.dzAuth.toggleActive(u);
    bump();
  };

  const remove = (u) => {
    if (u === "admin") { alert("超级管理员不可删除"); return; }
    if (!confirm(`删除账号 ${u}？`)) return;
    window.dzAuth.deleteAccount(u);
    bump();
  };

  return (
    <>
      <div className="admin-toolbar">
        <span className="count">{accounts.length} 个账号</span>
        <div className="sp"></div>
        <button className="btn btn-sm btn-accent" onClick={addUser}>+ 新增账号</button>
      </div>
      <div className="admin-table-wrap">
        <table className="admin-table">
          <thead>
            <tr>
              <th>用户名</th>
              <th>角色</th>
              <th>创建于</th>
              <th>最近登录</th>
              <th>状态</th>
              <th>操作</th>
            </tr>
          </thead>
          <tbody>
            {accounts.map(a => (
              <tr key={a.username}>
                <td className="mono" style={{ fontWeight: 600 }}>{a.username}</td>
                <td>
                  <span className="chip" style={{
                    background: a.role === "super" ? "var(--accent)" : a.role === "admin" ? "var(--moss)" : "var(--paper-2)",
                    color: a.role === "volunteer" ? "var(--ink)" : "white",
                    fontSize: 10.5,
                  }}>{a.role === "super" ? "超级管理员" : a.role === "admin" ? "管理员" : "志愿审核员"}</span>
                </td>
                <td className="mono" style={{ fontSize: 11, color: "var(--muted)" }}>{a.createdAt}</td>
                <td className="mono" style={{ fontSize: 11, color: "var(--muted)" }}>{a.lastLogin || "—"}</td>
                <td>
                  <span className={"chip " + (a.active ? "ok" : "warn")} style={{ fontSize: 10.5 }}>
                    {a.active ? "正常" : "已停用"}
                  </span>
                </td>
                <td style={{ whiteSpace: "nowrap" }}>
                  <a className="admin-link" onClick={() => resetPwd(a.username)}>重置密码</a>
                  <span style={{ margin: "0 8px", color: "var(--rule)" }}>·</span>
                  <a className="admin-link" onClick={() => toggleActive(a.username)}>
                    {a.active ? "停用" : "启用"}
                  </a>
                  {a.username !== "admin" && <>
                    <span style={{ margin: "0 8px", color: "var(--rule)" }}>·</span>
                    <a className="admin-link" style={{ color: "var(--candidate)" }} onClick={() => remove(a.username)}>删除</a>
                  </>}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      <div className="admin-card" style={{ marginTop: 20, maxWidth: 600 }}>
        <div className="admin-card-head"><h3>角色说明</h3></div>
        <div style={{ display: "grid", gap: 10, fontSize: 12.5, color: "var(--ink-soft)", lineHeight: 1.6 }}>
          <div><strong style={{ color: "var(--accent)" }}>超级管理员 super</strong> — 全部权限，可管理账号、项目设置、导出所有数据。</div>
          <div><strong style={{ color: "var(--moss)" }}>管理员 admin</strong> — 可编辑动物档案、观察记录、照片库、批量导入；不可管理账号。</div>
          <div><strong>志愿审核员 volunteer</strong> — 仅可审核观察队列（AI 初审 → 志愿者审核 → 归档 / 驳回）。</div>
        </div>
      </div>
    </>
  );
}

Object.assign(window, { AdminImport, AdminReview, AdminVolunteers, AdminContributors, AdminHotspots, AdminFeatured, AdminExport, AdminLogs, AdminSettings, AdminHero, AdminAccounts });
