// Upload flow with Bailian recognition and local fallback.
const AI_IMAGE_MAX_SIDE = 1600;
const AI_IMAGE_JPEG_QUALITY = 0.86;

function readFileAsDataUrl(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = e => resolve(e.target.result);
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
}

function loadImage(src) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => resolve(img);
    img.onerror = reject;
    img.src = src;
  });
}

async function prepareAIImageDataUrl(file) {
  const raw = await readFileAsDataUrl(file);
  if (!/^image\/(jpe?g|png|webp)$/i.test(file.type || "")) return raw;
  try {
    const img = await loadImage(raw);
    const naturalWidth = img.naturalWidth || img.width;
    const naturalHeight = img.naturalHeight || img.height;
    const scale = Math.min(1, AI_IMAGE_MAX_SIDE / Math.max(naturalWidth, naturalHeight));
    const width = Math.max(1, Math.round(naturalWidth * scale));
    const height = Math.max(1, Math.round(naturalHeight * scale));
    const canvas = document.createElement("canvas");
    canvas.width = width;
    canvas.height = height;
    const ctx = canvas.getContext("2d");
    ctx.drawImage(img, 0, 0, width, height);
    return canvas.toDataURL("image/jpeg", AI_IMAGE_JPEG_QUALITY);
  } catch (err) {
    console.warn("image compression skipped", err);
    return raw;
  }
}

function UploadPage({ onNav }) {
  const [step, setStep] = React.useState(0);
  const [data, setData] = React.useState({
    photoUrl: null,
    photoDataUrl: null,
    location: "棒棰岛 · 林海路附近",
    time: new Date().toISOString().slice(0, 16).replace("T", " "),
    behavior: ["站立觅食"],
    distance: "10–20m",
    suspectedSpecies: "不确定",
    note: "",
    contact: "",
    nickname: "晨光漫步",
    ethics: false,
    supportMedia: [],
  });
  const [aiState, setAiState] = React.useState({ status: "idle", features: null, candidates: null, error: null, provider: null, engine: null, species: null });
  const fileRef = React.useRef(null);
  const supportRef = React.useRef(null);

  const steps = [
    { k: "photo", z: "上传照片", e: "Photo" },
    { k: "when", z: "时间与地点", e: "Time & place" },
    { k: "what", z: "行为与备注", e: "Behavior" },
    { k: "who", z: "贡献者信息", e: "You" },
    { k: "ai", z: "AI 候选比对", e: "AI triage" },
    { k: "done", z: "提交完成", e: "Submitted" },
  ];

  const update = (k, v) => setData(d => ({ ...d, [k]: v }));
  const canProceed = step !== 3 || data.ethics;

  const onFile = async (file) => {
    if (!file) return;
    const previewUrl = URL.createObjectURL(file);
    try {
      const dataUrl = await prepareAIImageDataUrl(file);
      if (data.photoUrl?.startsWith("blob:")) URL.revokeObjectURL(data.photoUrl);
      update("photoDataUrl", dataUrl);
      update("photoUrl", previewUrl);
      setAiState({ status: "idle", features: null, candidates: null, error: null, provider: null, engine: null, species: null });
    } catch (err) {
      URL.revokeObjectURL(previewUrl);
      setAiState({
        status: "idle",
        features: null,
        candidates: null,
        error: "图片读取失败，请换一张 JPG/PNG 照片再试",
        provider: null,
        engine: null,
        species: null,
      });
    }
  };

  const onSupportFiles = (files) => {
    const items = [...(files || [])].filter(f => /^image\//.test(f.type) || /^video\//.test(f.type)).map((f, i) => ({
      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: "非识别素材，仅供审核员参考",
    }));
    if (items.length) update("supportMedia", [...data.supportMedia, ...items]);
  };

  const removeSupportMedia = (id) => {
    const item = data.supportMedia.find(m => m.id === id);
    if (item?.url?.startsWith("blob:")) URL.revokeObjectURL(item.url);
    update("supportMedia", data.supportMedia.filter(m => m.id !== id));
  };

  const submitObservation = () => {
    const picked = aiState.candidates?.[0]?.deer;
    const id = "OBS-" + Math.floor(5000 + Math.random() * 3999);
    const media = [
      ...(data.photoUrl ? [{
        id: id + "-MAIN",
        type: "image",
        url: data.photoUrl,
        role: "识别主图",
        recognition: true,
        title: "本次观察主图",
      }] : []),
      ...data.supportMedia.map(m => ({ ...m, role: m.role || "补充素材", recognition: false })),
    ];

    window.dzStore.add("sightings", {
      id,
      deerId: picked?.id || "",
      deerName: picked?.name || "（待归并）",
      species: picked?.species || data.suspectedSpecies,
      speciesKey: picked?.speciesKey || "",
      time: data.time,
      loc: data.location,
      contributor: data.nickname || "匿名",
      note: data.note || "用户未填写补充说明。",
      status: aiState.status === "done" ? "志愿者审核中" : "AI 初审",
      photo: data.photoUrl,
      photoTone: "none",
      media,
      supportMediaCount: data.supportMedia.length,
      aiFeatures: aiState.features || [],
      aiCandidates: (aiState.candidates || []).map(c => ({ id: c.deer.id, score: Math.round(c.score * 100), reason: c.reason })),
      aiProvider: aiState.provider,
      aiEngine: aiState.engine,
      aiSpecies: aiState.species,
      aiQuality: aiState.quality,
      contact: data.contact,
    }, data.nickname || "contributor");
    setStep(5);
  };

  const offlineWildlifeTriage = () => {
    const text = [data.location, data.suspectedSpecies, data.distance, data.note, ...(data.behavior || [])].join(" ").toLowerCase();
    const speciesProfiles = {
      sika_deer: {
        features: ["鹿科轮廓与四肢比例", "角形/耳缘/白斑等个体特征", "林缘、步道或滨海山体活动"],
        words: ["鹿", "梅花鹿", "小鹿", "角", "白斑", "莲花山", "棒棰", "滨海", "动物园", "觅食"],
        reason: "区域与鹿类特征吻合",
      },
      fox: {
        features: ["犬科轮廓与尖吻", "蓬松尾部与夜行行为", "需排除犬只或养殖逸散个体"],
        words: ["狐狸", "赤狐", "犬科", "尖嘴", "尾巴", "蓬松", "夜", "大黑山", "庄河", "林缘"],
        reason: "备注接近狐狸类线索",
      },
      spotted_seal: {
        features: ["海洋哺乳动物体型", "头部轮廓与体表斑点", "登岸/换气/海面活动"],
        words: ["海豹", "斑海豹", "海", "老铁山", "虎平岛", "水道", "换气", "登岸", "游泳"],
        reason: "地点与海豹线索吻合",
      },
      white_tailed_eagle: {
        features: ["大型猛禽轮廓", "白尾与黄嘴特征", "海岸巡飞或停歇行为"],
        words: ["白尾海雕", "海雕", "鹰", "猛禽", "鸟", "白尾", "黄嘴", "飞行", "长山"],
        reason: "备注接近海岸猛禽",
      },
    };
    const scored = window.DEER_DATA.map(animal => {
      const profile = speciesProfiles[animal.speciesKey] || speciesProfiles.sika_deer;
      let score = animal.verified ? 38 : 30;
      if (data.suspectedSpecies !== "不确定" && animal.species === data.suspectedSpecies) score += 24;
      profile.words.forEach(w => { if (text.includes(w.toLowerCase())) score += 12; });
      (animal.tags || []).forEach(tag => { if (text.includes(String(tag).toLowerCase())) score += 8; });
      String(animal.region || "").split(/[·\s/（），,]+/).filter(Boolean).forEach(part => {
        if (part.length >= 2 && text.includes(part.toLowerCase())) score += 7;
      });
      if (!data.photoDataUrl) score -= 8;
      return { deer: animal, score: Math.max(18, Math.min(96, score)) / 100, reason: profile.reason };
    }).sort((a, b) => b.score - a.score).slice(0, 3);
    const topProfile = speciesProfiles[scored[0]?.deer?.speciesKey] || speciesProfiles.sika_deer;
    return {
      features: topProfile.features,
      candidates: scored,
      error: window.claude && window.claude.complete ? null : "当前使用离线规则模型；上线时请接入后端视觉模型并保留人工复核",
    };
  };

  const runAI = async () => {
    setAiState({ status: "running", features: null, candidates: null, error: null, provider: null, engine: null, species: null });
    try {
      const apiPayload = {
        imageDataUrl: data.photoDataUrl,
        location: data.location,
        time: data.time,
        distance: data.distance,
        suspectedSpecies: data.suspectedSpecies,
        behavior: data.behavior,
        note: data.note,
        speciesCatalog: window.SPECIES_CATALOG || [],
        animals: window.DEER_DATA.map(d => ({
          id: d.id,
          name: d.name,
          species: d.species,
          speciesKey: d.speciesKey,
          speciesEn: d.speciesEn,
          sex: d.sex,
          region: d.region,
          tags: d.tags || [],
          photo: d.photo,
          gallery: d.gallery || [],
          verified: d.verified,
          candidate: d.candidate,
        })),
      };

      try {
        const resp = await fetch("/api/recognize", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(apiPayload),
        });
        if (resp.ok) {
          const result = await resp.json();
          if (result?.ok && Array.isArray(result.candidates) && result.candidates.length) {
            const candidates = result.candidates.map(c => ({
              deer: window.DEER_DATA.find(d => d.id === c.id) || window.DEER_DATA[0],
              score: Math.max(0, Math.min(100, Number(c.score) || 0)) / 100,
              reason: c.reason || "",
              engine: c.engine || result.engine,
            })).slice(0, 3);
            setAiState({
              status: "done",
              features: result.features || [],
              candidates,
              error: (result.warning || []).join("；") || null,
              provider: result.provider,
              engine: result.engine,
              species: result.species,
              quality: result.quality,
              individualIdentifiable: result.individualIdentifiable,
            });
            return;
          }
        }
      } catch (apiErr) {
        console.warn("recognize api unavailable", apiErr);
      }

      if (!window.claude || !window.claude.complete) {
        const offline = offlineWildlifeTriage();
        setAiState({ status: "done", features: offline.features, candidates: offline.candidates, error: offline.error, provider: "local-rules", engine: "browser-rules", species: null });
        return;
      }

      const deerList = window.DEER_DATA.filter(d => d.verified).map(d =>
        `- ${d.name} (${d.id}) · 物种 ${d.species} · 性别/状态 ${d.sex} · 区域 ${d.region} · 特征: ${d.tags.join(", ")}`
      ).join("\n");
      const speciesList = (window.SPECIES_CATALOG || []).map(s => `${s.label}(${s.en})`).join("、");

      const prompt = `你是大连“达里尼动物城 · 野生动物档案”的 AI 初审员。用户${data.photoDataUrl ? "上传了一张照片并" : "没有上传照片，只"}提供了观察备注。请结合常见野生动物视觉辨识知识与用户备注做保守推断。
用户提供的观察备注:
- 地点: ${data.location}
- 时间: ${data.time}
- 距离: ${data.distance}
- 用户疑似物种: ${data.suspectedSpecies}
- 行为: ${data.behavior.join("、")}
- 备注: ${data.note || "（无）"}

平台当前支持物种: ${speciesList}
档案库:
${deerList}

请:
1) 给出 3 条最可能的识别特征（30 字以内每条，中文，例如“犬科轮廓”“黑色斑点”“角形特征”）。
2) 先判断物种，再根据档案库推测 3 个最可能匹配档案，给出相似度（0-100 整数）与一句 15 字以内匹配原因。
3) 对狐狸类、猛禽、海豹等敏感物种保持保守：不确定时降低分数，并说明“需人工复核”。
只输出严格 JSON，不要解释或 markdown:
{"features":["…","…","…"],"candidates":[{"id":"DL-XXX","score":<0-100>,"reason":"…"},{"id":"FX-XXX","score":<0-100>,"reason":"…"},{"id":"SP-XXX","score":<0-100>,"reason":"…"}]}`;

      const raw = await window.claude.complete(prompt);
      const jsonStr = raw.replace(/```json|```/g, "").trim();
      const match = jsonStr.match(/\{[\s\S]*\}/);
      const parsed = JSON.parse(match ? match[0] : jsonStr);
      const candidates = (parsed.candidates || []).map(c => ({
        deer: window.DEER_DATA.find(d => d.id === c.id) || window.DEER_DATA[0],
        score: Math.max(0, Math.min(100, Number(c.score) || 0)) / 100,
        reason: c.reason || "",
      })).slice(0, 3);
      if (!candidates.length) throw new Error("empty candidates");
      setAiState({ status: "done", features: parsed.features || [], candidates, error: null, provider: "window.claude", engine: "llm-text", species: null });
    } catch (err) {
      console.error("AI fail", err);
      const offline = offlineWildlifeTriage();
      setAiState({
        status: "done",
        features: offline.features,
        candidates: offline.candidates,
        error: "AI 服务暂不可用，已切换到离线多物种备选方案",
        provider: "local-rules",
        engine: "browser-rules",
        species: null,
      });
    }
  };

  return (
    <div className="page">
      <section style={{ padding: "56px 0 24px", borderBottom: "1px solid var(--rule)" }}>
        <div className="app">
          <div className="kicker mb-4"><span className="dot">●</span>CONTRIBUTE / 上传观察</div>
          <div style={{ display: "grid", gridTemplateColumns: "1.2fr 1fr", gap: 64, alignItems: "end" }}>
            <h1 className="serif" style={{ fontSize: 52, lineHeight: 1.1 }}>
              你看到的那只动物，<br />
              值得被<span style={{ color: "var(--accent)" }}>认真</span>记下来。
            </h1>
            <p style={{ fontSize: 14, color: "var(--ink-soft)", lineHeight: 1.7, maxWidth: 420 }}>
              上传不强制登录。一张照片 + 一个大致的时间与地点，就足够启动一条观察记录。AI 初审与志愿者人工纠偏会随后进行。
            </p>
          </div>
        </div>
      </section>

      <section style={{ padding: "32px 0 80px" }}>
        <div className="app">
          <div className="upload-stepper">
            {steps.map((s, i) => (
              <div key={s.k}
                className={"upload-step " + (step === i ? "active" : step > i ? "done" : "")}
                onClick={() => step >= i && setStep(i)}>
                <div className="num">STEP {String(i + 1).padStart(2, "0")}</div>
                <div className="label">{s.z}</div>
                <div className="mono" style={{ fontSize: 10, color: "var(--muted)", letterSpacing: ".12em", marginTop: 2 }}>{s.e}</div>
              </div>
            ))}
          </div>

          <div style={{ display: "grid", gridTemplateColumns: "1.4fr 1fr", gap: 40 }}>
            <div>
              {step === 0 && (
                <div>
                  <h2 className="serif" style={{ fontSize: 28, marginBottom: 6 }}>上传识别主图</h2>
                  <p style={{ fontSize: 13, color: "var(--muted)", marginBottom: 24 }}>
                    这张图会进入 AI 初审。拍摄清晰的头部与身侧特征最有助于识别。
                  </p>
                  <input ref={fileRef} type="file" accept="image/*" style={{ display: "none" }}
                    onChange={e => onFile(e.target.files[0])} />
                  <input ref={supportRef} type="file" accept="image/*,video/*" multiple style={{ display: "none" }}
                    onChange={e => { onSupportFiles(e.target.files); e.target.value = ""; }} />
                  {!data.photoUrl ? (
                    <div onClick={() => fileRef.current?.click()}
                      style={{ border: "1.5px dashed var(--rule)", aspectRatio: "16 / 9", background: "var(--paper-2)", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 14, padding: 20, cursor: "pointer" }}>
                      <div style={{ width: 56, height: 56, border: "1px solid var(--rule)", display: "flex", alignItems: "center", justifyContent: "center", fontFamily: "Noto Serif SC", fontSize: 22, color: "var(--muted)" }}>↑</div>
                      <div className="serif" style={{ fontSize: 17, color: "var(--ink)" }}>拖拽或点击上传图片</div>
                      <div className="mono" style={{ fontSize: 11, color: "var(--muted)", letterSpacing: ".1em" }}>JPG · PNG · HEIC · ≤ 20MB / 张</div>
                      <button className="btn btn-sm btn-ghost" onClick={(e) => { e.stopPropagation(); fileRef.current?.click(); }}>选择文件</button>
                    </div>
                  ) : (
                    <div style={{ position: "relative" }}>
                      <img src={data.photoUrl} alt="uploaded" style={{ width: "100%", aspectRatio: "16 / 9", objectFit: "cover", border: "1px solid var(--rule)" }} />
                      <div style={{ position: "absolute", top: 12, right: 12, display: "flex", gap: 8 }}>
                        <button className="btn btn-sm btn-ghost" style={{ background: "rgba(255,255,255,.9)" }} onClick={() => fileRef.current?.click()}>更换</button>
                        <button className="btn btn-sm" onClick={() => { update("photoUrl", null); update("photoDataUrl", null); }}>移除</button>
                      </div>
                    </div>
                  )}
                  <div style={{ marginTop: 18, padding: 18, background: "var(--paper-2)", borderLeft: "3px solid var(--moss)", fontSize: 12.5, color: "var(--ink-soft)", lineHeight: 1.7 }}>
                    <strong style={{ color: "var(--ink)" }}>文明观察 · Ethics of watching.</strong> 拍摄时请保持 15m 以上距离，不投喂、不追逐、不惊扰。不鼓励公开精确坐标，上传后平台会对位置信息进行模糊处理。
                  </div>

                  <div style={{ marginTop: 22, paddingTop: 22, borderTop: "1px solid var(--rule)" }}>
                    <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 12, marginBottom: 10 }}>
                      <div>
                        <h3 className="serif" style={{ fontSize: 20, marginBottom: 4 }}>补充素材</h3>
                        <p style={{ fontSize: 12.5, color: "var(--muted)", lineHeight: 1.6 }}>
                          可上传图片或视频，作为审核员判断行为、环境、连续动作的参考；这些素材不会进入 AI 识别。
                        </p>
                      </div>
                      <button className="btn btn-sm btn-ghost" onClick={() => supportRef.current?.click()}>添加图片/视频</button>
                    </div>
                    {data.supportMedia.length > 0 ? (
                      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(140px, 1fr))", gap: 10 }}>
                        {data.supportMedia.map(m => (
                          <div key={m.id} style={{ border: "1px solid var(--rule)", background: "var(--paper)", padding: 8 }}>
                            <MediaPreview media={m} aspect="4 / 3" compact label={m.name} />
                            <div style={{ display: "flex", justifyContent: "space-between", gap: 8, alignItems: "center", marginTop: 8 }}>
                              <span className="mono" style={{ fontSize: 9.5, color: "var(--muted)", letterSpacing: ".08em", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{m.name}</span>
                              <button className="btn btn-sm btn-ghost" style={{ padding: "2px 8px", fontSize: 10 }} onClick={() => removeSupportMedia(m.id)}>移除</button>
                            </div>
                          </div>
                        ))}
                      </div>
                    ) : (
                      <div onClick={() => supportRef.current?.click()} style={{ padding: 18, border: "1px dashed var(--rule)", background: "var(--paper-2)", cursor: "pointer", color: "var(--ink-soft)", fontSize: 12.5 }}>
                        暂无补充素材。点击添加现场环境图、动物移动视频或远近景参考。
                      </div>
                    )}
                  </div>
                </div>
              )}

              {step === 1 && (
                <div>
                  <h2 className="serif" style={{ fontSize: 28, marginBottom: 24 }}>时间与地点</h2>
                  <div className="form-grid" style={{ gridTemplateColumns: "1fr 1fr" }}>
                    <div>
                      <label className="form-label">观察时间 · When</label>
                      <input className="form-input mono" value={data.time} onChange={e => update("time", e.target.value)} />
                    </div>
                    <div>
                      <label className="form-label">大致区域 · Where</label>
                      <select className="form-select" value={data.location} onChange={e => update("location", e.target.value)}>
                        <option>棒棰岛 · 林海路附近</option>
                        <option>莲花山 · 木栈道</option>
                        <option>星海湾后山 · 黑石礁林带</option>
                        <option>大黑山景区</option>
                        <option>老铁山自然保护区周边</option>
                        <option>其他（在备注中说明）</option>
                      </select>
                    </div>
                    <div style={{ gridColumn: "1 / -1" }}>
                      <label className="form-label">观察距离 · Distance</label>
                      <div style={{ display: "flex", gap: 8 }}>
                        {["< 10m", "10–20m", "20–50m", "> 50m", "记不清"].map(d => (
                          <button key={d}
                            className={"btn btn-sm " + (data.distance === d ? "" : "btn-ghost")}
                            onClick={() => update("distance", d)}>{d}</button>
                        ))}
                      </div>
                    </div>
                  </div>
                  <div className="mt-10" style={{ padding: 18, background: "var(--paper-2)", borderLeft: "3px solid var(--moss)", fontSize: 12.5, color: "var(--ink-soft)", lineHeight: 1.7 }}>
                    为保护个体，平台公开展示时<strong style={{ color: "var(--ink)" }}>只保留大致区域</strong>，精确坐标（若有）仅保存在审核后台，不对外公开。
                  </div>
                </div>
              )}

              {step === 2 && (
                <div>
                  <h2 className="serif" style={{ fontSize: 28, marginBottom: 24 }}>它当时在做什么？</h2>
                  <div className="form-grid">
                    <div>
                      <label className="form-label">疑似物种 · Species hint</label>
                      <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
                        {["不确定", ...(window.SPECIES_CATALOG || []).map(s => s.label)].map(s => (
                          <button key={s}
                            className={"chip" + (data.suspectedSpecies === s ? " accent" : "")}
                            style={{ cursor: "pointer" }}
                            onClick={() => update("suspectedSpecies", s)}>{s}</button>
                        ))}
                      </div>
                    </div>
                    <div>
                      <label className="form-label">行为标签 · Behavior (可多选)</label>
                      <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
                        {["觅食", "行走/巡游", "饮水/换气", "躺卧/停歇", "警戒/回望", "幼体同行", "群体（≥2只）", "飞行/游泳", "其他"].map(b => {
                          const on = data.behavior.includes(b);
                          return (
                            <button key={b}
                              className={"chip" + (on ? " accent" : "")}
                              style={{ cursor: "pointer" }}
                              onClick={() => {
                                const next = on ? data.behavior.filter(x => x !== b) : [...data.behavior, b];
                                update("behavior", next);
                              }}>{b}</button>
                          );
                        })}
                      </div>
                    </div>
                    <div>
                      <label className="form-label">补充故事 · Story (选填)</label>
                      <textarea className="form-textarea" placeholder="那一刻你看到了什么？体型、颜色、尾巴、翅形、斑点、距离、是否在海面或林缘？这段文字会和照片一起沉淀到动物档案里。" value={data.note} onChange={e => update("note", e.target.value)} />
                    </div>
                  </div>
                </div>
              )}

              {step === 3 && (
                <div>
                  <h2 className="serif" style={{ fontSize: 28, marginBottom: 8 }}>关于你</h2>
                  <p style={{ fontSize: 13, color: "var(--muted)", marginBottom: 24 }}>非必填。留下昵称会出现在档案时间线上；留下联系方式便于志愿者回访与致谢。</p>
                  <div className="form-grid" style={{ gridTemplateColumns: "1fr 1fr" }}>
                    <div>
                      <label className="form-label">昵称 · Nickname</label>
                      <input className="form-input" value={data.nickname} onChange={e => update("nickname", e.target.value)} />
                    </div>
                    <div>
                      <label className="form-label">联系方式（选填） · Contact</label>
                      <input className="form-input" placeholder="手机 / 邮箱 / 微信号" value={data.contact} onChange={e => update("contact", e.target.value)} />
                    </div>
                    <div style={{ gridColumn: "1 / -1" }}>
                      <label style={{ display: "flex", gap: 12, alignItems: "flex-start", fontSize: 13, color: "var(--ink-soft)", lineHeight: 1.6, padding: "14px 16px", border: "1px solid var(--rule)", cursor: "pointer" }}>
                        <input type="checkbox" checked={data.ethics} onChange={e => update("ethics", e.target.checked)} style={{ marginTop: 3 }} />
                        <span>
                          <strong style={{ color: "var(--ink)" }}>我已阅读并同意文明观察须知</strong>：拍摄过程中未投喂、未追逐、未惊扰野生动物；上传的图片为我本人拍摄或获得授权；同意位置信息按平台规则模糊处理。
                        </span>
                      </label>
                      {!data.ethics && (
                        <div className="form-hint warn">提交前需要确认文明观察须知，这是公开参与入口的必填安全边界。</div>
                      )}
                    </div>
                  </div>
                </div>
              )}

              {step === 4 && (
                <div>
                  <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 6 }}>
                    <h2 className="serif" style={{ fontSize: 28 }}>AI 初审：Top 3 候选</h2>
                    {aiState.status === "idle" && (
                      <button className="btn btn-accent btn-sm" onClick={runAI}>运行 AI 分析 →</button>
                    )}
                    {aiState.status === "done" && (
                      <button className="btn btn-ghost btn-sm" onClick={runAI}>重新运行</button>
                    )}
                  </div>
                  <p style={{ fontSize: 13, color: "var(--muted)", marginBottom: 24 }}>
                    AI 会基于你上传的照片与观察备注,给出相似度最高的 3 个候选。人工审核是最终权威。
                  </p>

                  {aiState.status === "idle" && (
                    <div style={{ border: "1px dashed var(--rule)", padding: 40, textAlign: "center", background: "var(--paper-2)" }}>
                      <div className="mono" style={{ fontSize: 10.5, color: "var(--muted)", letterSpacing: ".16em", marginBottom: 12 }}>AI · STANDBY</div>
                      <p style={{ fontSize: 13, color: "var(--ink-soft)", lineHeight: 1.7 }}>
                        点击上方"运行 AI 分析"让系统给出 Top-3 候选。<br/>
                        没上传照片也可以——AI 会基于观察备注做保守推测。
                      </p>
                    </div>
                  )}

                  {aiState.status === "running" && (
                    <div style={{ border: "1px solid var(--rule)", padding: 40, textAlign: "center", background: "var(--paper-2)" }}>
                      <div className="mono" style={{ fontSize: 10.5, color: "var(--accent)", letterSpacing: ".16em", marginBottom: 12 }}>● AI · 正在分析</div>
                      <div className="serif" style={{ fontSize: 18, color: "var(--ink)" }}>正在比对档案库中的个体特征…</div>
                      <div style={{ width: "60%", height: 2, background: "var(--rule)", margin: "20px auto 0", position: "relative", overflow: "hidden" }}>
                        <div style={{ position: "absolute", inset: 0, background: "linear-gradient(90deg, transparent, var(--accent), transparent)", animation: "shimmer 1.4s linear infinite" }}></div>
                      </div>
                    </div>
                  )}

                  {aiState.status === "done" && (
                    <>
                      {aiState.error && (
                        <div style={{ padding: 12, background: "rgba(199,138,42,.1)", border: "1px solid var(--warn)", fontSize: 12, color: "var(--ink)", marginBottom: 16 }}>
                          提醒：{aiState.error}
                        </div>
                      )}
                      {aiState.features && aiState.features.length > 0 && (
                        <div style={{ marginBottom: 18, padding: 16, background: "var(--paper-2)", border: "1px solid var(--rule)" }}>
                          <div style={{ display: "flex", justifyContent: "space-between", gap: 12, alignItems: "baseline", marginBottom: 8 }}>
                            <div className="form-label" style={{ marginBottom: 0 }}>AI 特征摘要 · Feature summary</div>
                            <div className="mono" style={{ fontSize: 10, color: "var(--muted)", letterSpacing: ".12em" }}>
                              {(aiState.provider || "AI").toUpperCase()} · {(aiState.engine || "TRIAGE").toUpperCase()}
                            </div>
                          </div>
                          {aiState.species && (
                            <div style={{ fontSize: 12, color: "var(--ink-soft)", marginBottom: 10 }}>
                              识别物种：<strong style={{ color: "var(--ink)" }}>{aiState.species}</strong>
                              {typeof aiState.quality === "number" && <> · 图像质量 {(aiState.quality * 100).toFixed(0)}%</>}
                              {aiState.individualIdentifiable === false && <> · 个体识别需人工谨慎复核</>}
                            </div>
                          )}
                          <div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginTop: 2 }}>
                            {aiState.features.map((f, i) => <span key={i} className="chip accent">{f}</span>)}
                          </div>
                        </div>
                      )}
                      <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 14 }}>
                        {aiState.candidates.map((c, i) => (
                          <div key={i} style={{ border: i === 0 ? "1px solid var(--accent)" : "1px solid var(--rule)", padding: 14, cursor: "pointer", background: i === 0 ? "var(--paper-2)" : "var(--paper)" }}>
                            <Portrait palette={c.deer.palette} photo={c.deer.photo} photoTone={c.deer.photoTone} aspect="4 / 5" />
                            <div style={{ marginTop: 12, display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
                              <div>
                                <div className="serif" style={{ fontSize: 17 }}>{c.deer.name}</div>
                                <div className="mono" style={{ fontSize: 10, color: "var(--muted)", letterSpacing: ".1em", marginTop: 2 }}>{c.deer.id}</div>
                              </div>
                              <div style={{ textAlign: "right" }}>
                                <div className="mono" style={{ fontSize: 18, color: i === 0 ? "var(--accent)" : "var(--ink)", fontWeight: 600 }}>{(c.score * 100).toFixed(0)}%</div>
                                <div className="mono" style={{ fontSize: 9, color: "var(--muted)", letterSpacing: ".14em" }}>SIMILARITY</div>
                              </div>
                            </div>
                            <div style={{ marginTop: 10, fontSize: 11.5, color: "var(--ink-soft)", lineHeight: 1.5 }}>
                              {c.reason || `特征：${c.deer.tags.slice(0, 2).join(" · ")}`}
                            </div>
                          </div>
                        ))}
                      </div>
                      <div style={{ display: "flex", gap: 10, marginTop: 22 }}>
                        <button className="btn btn-ghost btn-sm">都不是 → 可能是新个体</button>
                        <button className="btn btn-ghost btn-sm">不确定 → 交给志愿者</button>
                      </div>
                    </>
                  )}
                </div>
              )}

              {step === 5 && (
                <div style={{ textAlign: "center", padding: "40px 0" }}>
                  <div className="mono" style={{ fontSize: 10.5, color: "var(--accent)", letterSpacing: ".2em", marginBottom: 14 }}>● OBS-4823 / SUBMITTED</div>
                  <h2 className="serif" style={{ fontSize: 40, lineHeight: 1.15, marginBottom: 18 }}>
                    你已经把这次相遇，<br />交给这座城市。
                  </h2>
                  <p style={{ fontSize: 14, color: "var(--ink-soft)", maxWidth: 460, margin: "0 auto", lineHeight: 1.7 }}>
                    志愿审核员会在 24–48 小时内完成纠偏、合并或建档。结果会通过你留下的昵称，在档案页面的时间线上出现。
                  </p>
                  <div style={{ display: "flex", gap: 10, justifyContent: "center", marginTop: 32 }}>
                    <button className="btn btn-accent" onClick={() => { setStep(0); setAiState({ status: "idle", features: null, candidates: null, error: null }); }}>再上传一次</button>
                    <button className="btn btn-ghost" onClick={() => onNav("archive")}>浏览动物档案</button>
                    <button className="btn btn-ghost" onClick={() => onNav("home")}>返回首页</button>
                  </div>
                  <div style={{ marginTop: 48, padding: 22, border: "1px solid var(--rule)", maxWidth: 520, margin: "48px auto 0", textAlign: "left", background: "var(--paper-2)" }}>
                    <div className="form-label">贡献者积分 · Your points</div>
                    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginTop: 6 }}>
                      <span className="serif" style={{ fontSize: 32, fontWeight: 500 }}>+8</span>
                      <span className="mono" style={{ fontSize: 11, color: "var(--muted)", letterSpacing: ".1em" }}>累计 142 分 · LV.2 持续贡献者</span>
                    </div>
                    <p style={{ fontSize: 12, color: "var(--ink-soft)", marginTop: 10 }}>本次奖励：有效照片 +5 · 补充故事 +2 · 首次 Top-3 候选参与 +1</p>
                  </div>
                </div>
              )}

              {step < 5 && (
                <div style={{ display: "flex", justifyContent: "space-between", marginTop: 40, paddingTop: 22, borderTop: "1px solid var(--rule)" }}>
                  <button className="btn btn-ghost" onClick={() => step > 0 && setStep(step - 1)} disabled={step === 0}>← 上一步</button>
                  <button className="btn btn-accent" disabled={!canProceed} onClick={() => {
                    if (!canProceed) return;
                    if (step === 4) submitObservation();
                    else setStep(step + 1);
                  }}>
                    {step === 4 ? "确认提交 →" : "下一步 →"}
                  </button>
                </div>
              )}
            </div>

            <aside>
              <div style={{ position: "sticky", top: 100 }}>
                <div className="form-label">Live preview · 记录预览</div>
                <div style={{ border: "1px solid var(--rule)", padding: 18, background: "var(--paper)" }}>
                  {data.photoUrl ? (
                    <img src={data.photoUrl} alt="" style={{ width: "100%", aspectRatio: "4 / 3", objectFit: "cover", border: "1px solid var(--rule)" }} />
                  ) : (
                    <div style={{ aspectRatio: "4 / 3", background: "linear-gradient(135deg, #3a2a1f, #9a7452)", border: "1px solid var(--rule)", position: "relative" }}>
                      <div className="mono" style={{ position: "absolute", bottom: 8, left: 10, fontSize: 9, color: "rgba(255,255,255,.7)", letterSpacing: ".12em" }}>IMG · 等待上传</div>
                    </div>
                  )}
                  <div className="mono" style={{ fontSize: 10.5, color: "var(--muted)", letterSpacing: ".1em", marginTop: 12 }}>OBS-DRAFT-4823</div>
                  <div className="serif" style={{ fontSize: 18, marginTop: 4 }}>{data.location}</div>
                  <div className="mono" style={{ fontSize: 11, color: "var(--ink-soft)", marginTop: 4 }}>{data.time} · {data.distance} · {data.suspectedSpecies}</div>
                  <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginTop: 12 }}>
                    {data.behavior.map(b => <span key={b} className="chip" style={{ fontSize: 10.5 }}>{b}</span>)}
                    {data.supportMedia.length > 0 && <span className="chip accent" style={{ fontSize: 10.5 }}>补充素材 {data.supportMedia.length}</span>}
                  </div>
                  <div style={{ fontSize: 12, color: "var(--ink-soft)", marginTop: 12, fontStyle: "italic", lineHeight: 1.55 }}>
                    {data.note || "（补充故事会出现在这里 · 选填）"}
                  </div>
                  <hr className="rule-soft" style={{ margin: "14px 0" }} />
                  <div className="mono" style={{ fontSize: 10.5, color: "var(--muted)", letterSpacing: ".08em" }}>
                    贡献者 · {data.nickname || "匿名"}
                  </div>
                </div>

                <div style={{ marginTop: 22, padding: 16, border: "1px dashed var(--rule)", fontSize: 12, color: "var(--ink-soft)", lineHeight: 1.6 }}>
                  <div className="mono" style={{ fontSize: 10, color: "var(--muted)", letterSpacing: ".14em", textTransform: "uppercase", marginBottom: 6 }}>TIP · 上传小贴士</div>
                  识别主图尽量清晰；补充图片/视频适合记录动作、环境和前后位置，仅供审核员参考。
                </div>
              </div>
            </aside>
          </div>
        </div>
      </section>
    </div>
  );
}

Object.assign(window, { UploadPage });
