// Admin: drag-and-drop a day's files, validate, preview, and download a publish-ready ZIP.
// Internal tool — accessed via #admin in the URL hash.

// ── Helpers ──────────────────────────────────────────────────────────────────

// "20260527" → "2026-05-27", already-dashed pass-through
function toIso(raw) {
  const s = String(raw).replace(/-/g, "");
  if (s.length === 8) return `${s.slice(0,4)}-${s.slice(4,6)}-${s.slice(6,8)}`;
  return raw;
}

function bookFrom(ref, lang) {
  if (!ref) return lang === "cht" ? "未知" : "Unknown";
  if (lang === "cht") return ref.replace(/[\s\d:：\-—–]+.*$/, "").trim() || "未知";
  return ref.replace(/\s+\d.*$/, "").replace(/\s+NIV.*/i, "").trim() || "Unknown";
}

// Detect new format (history uses world_events[]/births[]/deaths[] arrays)
function isNewFormat(j) {
  const h = j.history || {};
  return Array.isArray(h.world_events) || Array.isArray(h.births) || Array.isArray(h.deaths);
}

// Convert the approved JSON shape → internal Day shape used by window.DAYS
// Handles both new format (world_events[] arrays) and old format (flat fields)
function convertJsonToDay(j) {
  const refCht = j.scripture && j.scripture.ref_cht || "";
  const refEng = j.scripture && j.scripture.ref_eng || "";
  const wkCht = j.date && j.date.cht ? (j.date.cht.match(/星期(.)/) || [])[1] || "" : "";
  const wkMatch = j.date && j.date.eng ? j.date.eng.match(/,\s*([A-Za-z]+)/) : null;
  const wkEng = wkMatch ? wkMatch[1].slice(0, 3) : "";

  let history;
  if (isNewFormat(j)) {
    const h  = j.history || {};
    const we = (h.world_events || [])[0] || {};
    const b  = (h.births       || [])[0] || {};
    const d  = (h.deaths       || [])[0] || {};
    const cu = h.custom || {};
    history = {
      world:  { year: we.year || "", cht: we.cht || "", eng: we.eng || "" },
      birth:  { year: b.year  || "", cht: b.cht  || "", eng: b.eng  || "" },
      death:  { year: d.year  || "", cht: d.cht  || "", eng: d.eng  || "" },
      custom: { year: cu.year || "", cht: cu.name_cht || cu.cht || "", eng: cu.name_eng || cu.eng || "" }
    };
  } else {
    const h = j.history || {};
    history = {
      world:  { year: h.world_event_year || "", cht: h.world_event_cht  || "", eng: h.world_event_eng  || "" },
      birth:  { year: h.birth_year       || "", cht: h.birth_person_cht || "", eng: h.birth_person_eng || "" },
      death:  { year: h.death_year       || "", cht: h.death_person_cht || "", eng: h.death_person_eng || "" },
      custom: { year: h.custom_year      || "", cht: h.custom_cht       || "", eng: h.custom_eng       || "" }
    };
  }

  return {
    iso: toIso(j.date.iso),
    date_cht: j.date.cht,
    date_eng: j.date.eng,
    weekday_short_cht: wkCht,
    weekday_short_eng: wkEng,
    weather: {
      cht:       j.weather.cht       || "",
      eng:       j.weather.eng       || "",
      mood:      j.weather.mood      || "",
      mood_eng:  j.weather.mood_eng  || j.weather.mood || "",
      badge_cht: j.weather.badge_cht || "",
      badge_eng: j.weather.badge_eng || ""
    },
    history,
    scripture: {
      cht:      j.scripture.cht || "",
      ref_cht:  refCht,
      niv:      j.scripture.niv || "",
      ref_eng:  refEng,
      book_cht: bookFrom(refCht, "cht"),
      book_eng: bookFrom(refEng, "eng")
    },
    reflection: {
      cht: j.reflection.cht || "",
      eng: j.reflection.eng || ""
    },
    has_real_infographic: true,
    _source_json: j
  };
}

// Classify a file by name + type
function classifyFile(f) {
  const lower = f.name.toLowerCase();
  if (lower.endsWith(".json") || f.type === "application/json") return "json";
  if (f.type.startsWith("image/")) {
    if (/(cht|繁|tc|zh\b|zh-?tw|zh-?hk|chinese|中)/i.test(f.name)) return "imgCht";
    if (/(eng|en\b|english|英)/i.test(f.name)) return "imgEng";
    return "imgAuto";
  }
  if (lower.endsWith(".html") || lower.endsWith(".htm")) {
    if (/(cht|繁|tc|zh\b|chinese|中)/i.test(f.name)) return "htmlCht";
    if (/(eng|en\b|english|英)/i.test(f.name)) return "htmlEng";
    return "htmlAuto";
  }
  return "unknown";
}

// Read image dimensions
function readImageDims(file) {
  return new Promise((resolve) => {
    const url = URL.createObjectURL(file);
    const img = new Image();
    img.onload = () => resolve({ w: img.naturalWidth, h: img.naturalHeight, url });
    img.onerror = () => resolve({ w: 0, h: 0, url });
    img.src = url;
  });
}

// Read text (strips UTF-8 BOM so JSON.parse doesn't fail on Windows-saved files)
function readText(file) {
  return new Promise((resolve, reject) => {
    const r = new FileReader();
    r.onload = () => {
      let result = r.result;
      if (result.charCodeAt(0) === 0xFEFF) result = result.slice(1);
      resolve(result);
    };
    r.onerror = reject;
    r.readAsText(file);
  });
}

// Simplified-Chinese sniffer (subset from guideline §16)
const SIMPLIFIED_SET = ["神迹", "启示", "经历", "灵修", "祷告", "圣经", "历史", "对", "风", "云", "为", "这", "让", "体", "会", "里", "经"];
function detectSimplified(text) {
  if (!text) return [];
  return SIMPLIFIED_SET.filter(s => text.includes(s));
}

// Validation
function validate(day, files, imgDims) {
  const v = [];

  if (!day) {
    v.push({ level: "wait", msg: "等待 JSON 檔 / Waiting for JSON" });
    return v;
  }

  // Date
  if (/^\d{4}-\d{2}-\d{2}$/.test(day.iso)) {
    v.push({ level: "ok", msg: `日期格式正確 · Date format valid (${day.iso})` });
    if (window.DAYS.some(d => d.iso === day.iso)) {
      v.push({ level: "warn", msg: `已存在 ${day.iso} 的內容 — 發佈將覆蓋 / Will overwrite existing entry` });
    }
  } else {
    v.push({ level: "fail", msg: "date.iso 必須為 YYYY-MM-DD / must be YYYY-MM-DD" });
  }

  // Required text fields
  ["weather.cht","weather.eng","scripture.cht","scripture.niv","reflection.cht","reflection.eng"].forEach(path => {
    const val = path.split(".").reduce((o, k) => o && o[k], day);
    if (!val || !val.trim()) v.push({ level: "fail", msg: `缺少 / Missing: ${path}` });
  });

  // NIV rule (guideline §5)
  if (day.scripture.niv && day._source_json && day._source_json.scripture && day._source_json.scripture.needs_manual_niv === true) {
    v.push({ level: "fail", msg: "needs_manual_niv = true · 請先補上精確 NIV / paste exact NIV first" });
  } else if (day.scripture.niv) {
    v.push({ level: "ok", msg: "NIV 經文已提供 · NIV scripture present" });
  }
  if (day.scripture.ref_eng && !/NIV/i.test(day.scripture.ref_eng)) {
    v.push({ level: "warn", msg: 'ref_eng 應以 "NIV" 結尾 / should end with "NIV"' });
  } else if (day.scripture.ref_eng) {
    v.push({ level: "ok", msg: 'ref_eng 標明 NIV · Marked NIV' });
  }

  // HKO source rule (guideline §4)
  const wUrl = day._source_json && day._source_json.weather && day._source_json.weather.source_url || "";
  if (wUrl && !/hko\.gov\.hk/i.test(wUrl)) {
    v.push({ level: "fail", msg: "天氣來源非 HKO · Weather source is not HKO" });
  } else if (wUrl) {
    v.push({ level: "ok", msg: "天氣來源為 HKO · Weather source is HKO" });
  }

  // Simplified Chinese check
  const fullCht = [
    day.weather.cht, day.weather.badge_cht,
    day.history.world.cht, day.history.birth.cht, day.history.death.cht, day.history.custom.cht,
    day.scripture.cht, day.reflection.cht
  ].join(" ");
  const simpHits = detectSimplified(fullCht);
  if (simpHits.length > 0) {
    v.push({ level: "warn", msg: `疑似簡體字 / Possible simplified chars: ${simpHits.join(" · ")}` });
  } else {
    v.push({ level: "ok", msg: "未發現簡體字 · Traditional Chinese verified" });
  }

  // Length limits (guideline §15)
  const cjkCount = (s) => (s || "").match(/[\u3400-\u9FFF]/g)?.length || 0;
  if (cjkCount(day.weather.cht) > 35) v.push({ level: "warn", msg: `weather.cht 過長 (${cjkCount(day.weather.cht)}>35 字)` });
  if (cjkCount(day.scripture.cht) > 65) v.push({ level: "warn", msg: `scripture.cht 過長 (${cjkCount(day.scripture.cht)}>65 字)` });
  if (cjkCount(day.reflection.cht) > 48) v.push({ level: "warn", msg: `reflection.cht 過長 (${cjkCount(day.reflection.cht)}>48 字)` });
  const wordCount = (s) => (s || "").trim().split(/\s+/).filter(Boolean).length;
  if (wordCount(day.reflection.eng) > 22) v.push({ level: "warn", msg: `reflection.eng 過長 (${wordCount(day.reflection.eng)}>22 words)` });

  // Image dimensions
  ["cht","eng"].forEach(side => {
    const d = imgDims[side];
    if (!d) {
      v.push({ level: "wait", msg: `等待 ${side.toUpperCase()} 圖片 / Waiting for ${side.toUpperCase()} infographic` });
      return;
    }
    const ratio = d.h / d.w;
    const target = 16 / 9;
    const ok = Math.abs(ratio - target) < 0.05;
    if (!ok) v.push({ level: "warn", msg: `${side.toUpperCase()} 圖片比例 ${d.w}×${d.h} ≠ 9:16` });
    else v.push({ level: "ok", msg: `${side.toUpperCase()} 圖片比例正確 ${d.w}×${d.h}` });
  });

  return v;
}

// ── Password (for /api/publish) ───────────────────────────────────────────
// Stored in sessionStorage (clears on tab close / refresh) + an in-memory
// expiry so the password auto-clears after 15 minutes of inactivity.

const PASSWORD_KEY    = "adm:password";
const PASSWORD_EXPIRY = "adm:password_expiry";
const TIMEOUT_MS      = 15 * 60 * 1000; // 15 minutes

function loadPassword() {
  const expiry = Number(sessionStorage.getItem(PASSWORD_EXPIRY) || 0);
  if (Date.now() > expiry) {
    sessionStorage.removeItem(PASSWORD_KEY);
    sessionStorage.removeItem(PASSWORD_EXPIRY);
    return "";
  }
  return sessionStorage.getItem(PASSWORD_KEY) || "";
}
function savePassword(p) {
  sessionStorage.setItem(PASSWORD_KEY, p);
  sessionStorage.setItem(PASSWORD_EXPIRY, String(Date.now() + TIMEOUT_MS));
}
function touchPassword() {
  if (sessionStorage.getItem(PASSWORD_KEY)) {
    sessionStorage.setItem(PASSWORD_EXPIRY, String(Date.now() + TIMEOUT_MS));
  }
}
function clearPassword() {
  sessionStorage.removeItem(PASSWORD_KEY);
  sessionStorage.removeItem(PASSWORD_EXPIRY);
}

// Reset idle timer on any user interaction
(function() {
  ["click","keydown","mousemove","touchstart"].forEach(evt =>
    window.addEventListener(evt, touchPassword, { passive: true })
  );
})();

// Build new days.js content from existing window.DAYS array + new day
function buildDaysFile(updatedDays) {
  return `// ─── A Daily Moment · Days archive ───────────────────────────────────────────
// This file is auto-managed by the admin uploader (#admin).
// To add a new day manually, prepend an entry to the array.

window.DAYS = ${JSON.stringify(updatedDays, null, 2)};
`;
}

// Group validations
const levelOrder = { fail: 0, warn: 1, wait: 2, ok: 3 };

// ── Publish (new day) view ───────────────────────────────────────────────────

function PublishView({ lang }) {
  const [json, setJson]         = React.useState(null); // {name, raw, parsed}
  const [imgCht, setImgCht]     = React.useState(null); // {name, file, url, w, h}
  const [imgEng, setImgEng]     = React.useState(null);
  const [htmlCht, setHtmlCht]   = React.useState(null); // {name, content}
  const [htmlEng, setHtmlEng]   = React.useState(null);
  const [parsedDay, setParsedDay] = React.useState(null);
  const [busy, setBusy]         = React.useState(false);
  const [dragOver, setDragOver] = React.useState(false);
  const [password, setPassword] = React.useState(loadPassword);
  const [showPassword, setShowPassword] = React.useState(() => !loadPassword());
  const [publishLog, setPublishLog] = React.useState([]); // [{level, msg}]
  const fileInputRef = React.useRef(null);

  // Re-derive parsedDay when json changes
  React.useEffect(() => {
    if (!json) { setParsedDay(null); return; }
    try {
      setParsedDay(convertJsonToDay(json.parsed));
    } catch (e) {
      setParsedDay(null);
    }
  }, [json]);

  // Inject articles flag whenever HTMLs change
  React.useEffect(() => {
    if (!parsedDay) return;
    const articles = { cht: !!htmlCht, eng: !!htmlEng };
    if (JSON.stringify(parsedDay.articles) !== JSON.stringify(articles)) {
      setParsedDay(prev => prev ? { ...prev, articles } : prev);
    }
  }, [htmlCht, htmlEng, parsedDay && parsedDay.iso]);

  async function handleFiles(fileList) {
    for (const f of Array.from(fileList)) {
      const kind = classifyFile(f);
      if (kind === "json") {
        try {
          const text = await readText(f);
          const parsed = JSON.parse(text);
          setJson({ name: f.name, raw: text, parsed });
        } catch (e) {
          alert("Invalid JSON: " + e.message);
        }
      } else if (kind === "imgCht" || kind === "imgAuto" && !imgCht) {
        const d = await readImageDims(f);
        setImgCht({ name: f.name, file: f, url: d.url, w: d.w, h: d.h });
      } else if (kind === "imgEng" || kind === "imgAuto") {
        const d = await readImageDims(f);
        setImgEng({ name: f.name, file: f, url: d.url, w: d.w, h: d.h });
      } else if (kind === "htmlCht" || kind === "htmlAuto" && !htmlCht) {
        const content = await readText(f);
        setHtmlCht({ name: f.name, content, file: f });
      } else if (kind === "htmlEng" || kind === "htmlAuto") {
        const content = await readText(f);
        setHtmlEng({ name: f.name, content, file: f });
      }
    }
  }

  function loadSample() {
    const sample = {
      "date": { "cht": "2026年5月17日，星期日", "eng": "17 May 2026, Sunday", "iso": "20260517" },
      "title": { "cht": "每日靜思", "eng": "A Daily Moment" },
      "weather": {
        "cht": "天色漸晴，海面風浪稍緩，宜把握黃昏散步。",
        "eng": "Clearing skies and easing winds; a good time for an evening walk.",
        "mood": "sunny-breaks", "mood_eng": "Sunny Breaks",
        "badge_cht": "天朗氣清", "badge_eng": "Clear & Bright",
        "source_name_cht": "香港天文台", "source_name_eng": "Hong Kong Observatory",
        "source_url": "https://www.hko.gov.hk/tc/index.html"
      },
      "history": {
        "world_events": [
          { "year": "1792", "cht": "紐約證券交易所成立", "eng": "New York Stock Exchange founded" }
        ],
        "births": [
          { "year": "1749", "cht": "愛德華・詹納：天花疫苗之父", "eng": "Edward Jenner: pioneer of vaccination" }
        ],
        "deaths": [
          { "year": "1727", "cht": "凱薩琳一世：俄國女皇", "eng": "Catherine I: Empress of Russia" }
        ],
        "custom": { "year": "1969", "name_cht": "世界電信和信息社會日", "name_eng": "World Telecommunication & Information Society Day" }
      },
      "scripture": {
        "label_cht": "今日金句", "label_eng": "Today's Scripture",
        "cht": "凡事都不可虧欠人，惟有彼此相愛，要常以為虧欠。",
        "ref_cht": "羅馬書 13：8 和合本",
        "niv": "Let no debt remain outstanding, except the continuing debt to love one another.",
        "ref_eng": "Romans 13:8 NIV",
        "needs_manual_niv": false
      },
      "reflection": {
        "label_cht": "靈修心語", "label_eng": "Reflection",
        "cht": "晴朗的傍晚，願我們以愛還清每一份溫柔的欠。",
        "eng": "On a clearing evening, may we repay every tender debt with love."
      },
      "footer": {
        "source_line_cht": "資料來源：香港天文台、維基百科、cnbible.com",
        "source_line_eng": "Sources: Hong Kong Observatory, Wikipedia, BibleGateway NIV",
        "brand": "Bridge & Build"
      }
    };
    setJson({ name: "sample-20260517.json", raw: JSON.stringify(sample, null, 2), parsed: sample });
  }

  function reset() {
    setJson(null); setImgCht(null); setImgEng(null); setHtmlCht(null); setHtmlEng(null);
  }

  const imgDims = {
    cht: imgCht ? { w: imgCht.w, h: imgCht.h } : null,
    eng: imgEng ? { w: imgEng.w, h: imgEng.h } : null
  };
  const validations = validate(parsedDay, { json, imgCht, imgEng }, imgDims);
  const blockingFails = validations.filter(v => v.level === "fail").length;
  const ready = parsedDay && imgCht && imgEng && blockingFails === 0;

  async function publish() {
    if (!ready) return;
    setBusy(true);
    try {
      const zip = new JSZip();
      const iso = parsedDay.iso;
      const folder = zip.folder(`data/days/${iso}`);
      folder.file("day.json", JSON.stringify(json.parsed, null, 2));
      if (imgCht) folder.file("infographic-cht.png", imgCht.file);
      if (imgEng) folder.file("infographic-eng.png", imgEng.file);
      if (htmlCht) folder.file("article-cht.html", htmlCht.content);
      if (htmlEng) folder.file("article-eng.html", htmlEng.content);

      // Build updated data.js (prepend new day, dedupe)
      const updatedDays = [parsedDay, ...window.DAYS.filter(d => d.iso !== iso)]
        .sort((a, b) => b.iso.localeCompare(a.iso))
        .map(d => { const { _source_json, ...clean } = d; return clean; });
      const dataJsBlob =
`// Auto-generated by the admin uploader at ${new Date().toISOString()}
// Replace the contents of data.js with this file (UI labels remain in data.js — keep window.UI + window.PIPELINE unchanged).

window.DAYS = ${JSON.stringify(updatedDays, null, 2)};
`;
      zip.file("data.js.partial.txt", dataJsBlob);

      const readme =
`PUBLISH BUNDLE — ${iso}
═════════════════════════════════════════════

This ZIP contains everything needed to add ${iso} to the live site.

CONTENTS
  data/days/${iso}/day.json              — structured day data
  data/days/${iso}/infographic-cht.png   — Traditional Chinese 9:16 image
  data/days/${iso}/infographic-eng.png   — English 9:16 image
  ${htmlCht ? `data/days/${iso}/article-cht.html       — optional long-form article (CHT)\n  ` : ""}${htmlEng ? `data/days/${iso}/article-eng.html       — optional long-form article (ENG)\n  ` : ""}data.js.partial.txt                    — drop into data.js (replaces window.DAYS array)

HOW TO PUBLISH
1. Unzip into the repo root (data/days/${iso}/ will appear).
2. Open data.js, replace the existing 'window.DAYS = [...]' assignment
   with the one in data.js.partial.txt.
3. git add . && git commit -m "publish ${iso}" && git push
4. Pages/Netlify/Cloudflare rebuild in ~30 seconds.

GENERATED ${new Date().toISOString()}
`;
      zip.file("README.txt", readme);

      const blob = await zip.generateAsync({ type: "blob" });
      const a = document.createElement("a");
      a.href = URL.createObjectURL(blob);
      a.download = `daily-moment-${iso}.zip`;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
    } finally {
      setBusy(false);
    }
  }

  // Publish via Cloudflare Function (/api/publish) — PAT stays server-side.
  async function publishToGitHub() {
    if (!ready) return;
    if (!password) { setShowPassword(true); return; }
    setBusy(true);
    setPublishLog([{ level: "info", msg: lang === "cht" ? "正在連線伺服器…" : "Connecting to server…" }]);

    const log = (level, msg) => setPublishLog(prev => [...prev, { level, msg }]);

    try {
      const iso = parsedDay.iso;

      const updatedDays = [parsedDay, ...window.DAYS.filter(d => d.iso !== iso)]
        .sort((a, b) => b.iso.localeCompare(a.iso))
        .map(d => { const { _source_json, _admin_preview, ...clean } = d; return clean; });
      const daysJsContent = buildDaysFile(updatedDays);

      const ext = (file) => file.name.split(".").pop().toLowerCase() || "webp";

      const form = new FormData();
      form.append("password", password);
      form.append("iso", iso);
      form.append("day_json", JSON.stringify(json.parsed, null, 2));
      form.append("days_js", daysJsContent);
      form.append("img_cht", imgCht.file);
      form.append("img_cht_ext", ext(imgCht.file));
      form.append("img_eng", imgEng.file);
      form.append("img_eng_ext", ext(imgEng.file));
      if (htmlCht) form.append("html_cht", htmlCht.file);
      if (htmlEng) form.append("html_eng", htmlEng.file);

      log("info", lang === "cht" ? "上傳中…" : "Uploading…");
      const r = await fetch("/api/publish", { method: "POST", body: form });
      const data = await r.json();

      if (!r.ok || !data.ok) {
        throw new Error(data.detail || data.error || `HTTP ${r.status}`);
      }

      log("ok", `days.js · ${data.file_count} files`);
      log("done", lang === "cht"
        ? `✓ 已發佈 ${iso}。Cloudflare Pages 將在約 30 秒後重新部署。`
        : `✓ Published ${iso}. Cloudflare Pages will redeploy in ~30 seconds.`);
    } catch (e) {
      log("fail", String(e.message || e));
    } finally {
      setBusy(false);
    }
  }

  function previewEntry() {
    if (!parsedDay) return;
    // Temporarily inject into window.DAYS for the entry view
    const existing = window.DAYS.findIndex(d => d.iso === parsedDay.iso);
    const { _source_json, ...clean } = parsedDay;
    const temp = { ...clean, _admin_preview: true };
    // Stash image URLs (Blob) for the entry view to pick up
    window.__ADMIN_PREVIEW_URLS = {
      [parsedDay.iso]: {
        cht: imgCht ? imgCht.url : null,
        eng: imgEng ? imgEng.url : null
      }
    };
    // Stash article HTML as Blob URLs for the iframe
    const articleUrls = {};
    if (htmlCht) articleUrls.cht = URL.createObjectURL(new Blob([htmlCht.content], { type: "text/html" }));
    if (htmlEng) articleUrls.eng = URL.createObjectURL(new Blob([htmlEng.content], { type: "text/html" }));
    window.__ADMIN_PREVIEW_ARTICLES = { [parsedDay.iso]: articleUrls };
    if (existing >= 0) window.DAYS[existing] = temp;
    else window.DAYS.unshift(temp);
    window.location.hash = "entry/" + parsedDay.iso;
  }

  // ── Render ──
  return (
    <div className="admin-page">
      <div className="admin-header">
        <div className="admin-eyebrow">
          <Icon name="upload" size={12} />
          <span>{lang === "cht" ? "管理員介面" : "Internal tool"}</span>
        </div>
        <h2 className="admin-title">{lang === "cht" ? "發佈今日靜思" : "Publish today's moment"}</h2>
        <p className="admin-lede">
          {lang === "cht"
            ? "拖入 JSON + 兩張 9:16 圖片（可加 HTML 文章），系統會驗證並產生可上傳的檔案包。"
            : "Drop in your JSON + two 9:16 PNGs (optional HTML articles). The validator checks every rule from your guideline and gives you a ready-to-commit ZIP."}
        </p>
      </div>

      <div
        className={"dropzone" + (dragOver ? " active" : "")}
        onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
        onDragLeave={() => setDragOver(false)}
        onDrop={(e) => { e.preventDefault(); setDragOver(false); handleFiles(e.dataTransfer.files); }}
        onClick={() => fileInputRef.current?.click()}
      >
        <input
          ref={fileInputRef}
          id="batch-file-input"
          name="batch-file-input"
          type="file"
          multiple
          accept=".json,.html,.htm,image/png,image/jpeg"
          style={{ display: "none" }}
          onChange={(e) => handleFiles(e.target.files)}
        />
        <div className="dropzone-icon"><Icon name="upload" size={32} /></div>
        <div className="dropzone-title">
          {lang === "cht" ? "拖放檔案到此或點擊選擇" : "Drag & drop files here, or click to choose"}
        </div>
        <div className="dropzone-sub">
          {lang === "cht"
            ? "需要：day.json · infographic-cht.png · infographic-eng.png（HTML 文章為選擇性）"
            : "Needed: day.json · infographic-cht.png · infographic-eng.png (HTML articles optional)"}
        </div>
        <div className="dropzone-actions" onClick={(e) => e.stopPropagation()}>
          <button className="ghost-btn" onClick={loadSample}>
            <Icon name="sparkle" size={13} />
            {lang === "cht" ? "載入範例 (5月17日)" : "Load sample (May 17)"}
          </button>
          {(json || imgCht || imgEng) && (
            <button className="ghost-btn" onClick={reset}>
              <Icon name="x" size={13} />
              {lang === "cht" ? "清除" : "Clear"}
            </button>
          )}
        </div>
      </div>

      <div className="admin-grid">
        {/* Left: detected files + validation */}
        <div className="admin-col">
          <h3 className="admin-section-title">{lang === "cht" ? "已偵測檔案" : "Detected files"}</h3>
          <ul className="file-list">
            <FileRow have={!!json}    label="day.json"               sub={json ? json.name : (lang === "cht" ? "結構化資料 / required" : "structured data / required")} />
            <FileRow have={!!imgCht}  label="infographic-cht.png"    sub={imgCht ? `${imgCht.name} · ${imgCht.w}×${imgCht.h}` : (lang === "cht" ? "繁中 9:16 圖 / required" : "Traditional Chinese 9:16 / required")} />
            <FileRow have={!!imgEng}  label="infographic-eng.png"    sub={imgEng ? `${imgEng.name} · ${imgEng.w}×${imgEng.h}` : (lang === "cht" ? "英文 9:16 圖 / required" : "English 9:16 / required")} />
            <FileRow have={!!htmlCht} optional label="article-cht.html" sub={htmlCht ? htmlCht.name : (lang === "cht" ? "長文章繁中 / optional" : "long-form CHT / optional")} />
            <FileRow have={!!htmlEng} optional label="article-eng.html" sub={htmlEng ? htmlEng.name : (lang === "cht" ? "長文章英文 / optional" : "long-form ENG / optional")} />
          </ul>

          <h3 className="admin-section-title" style={{ marginTop: 28 }}>{lang === "cht" ? "驗證結果" : "Validation"}</h3>
          {validations.length === 0 ? (
            <div className="hint">{lang === "cht" ? "上傳檔案後顯示驗證結果。" : "Validation appears once files are uploaded."}</div>
          ) : (
            <ul className="validation-list">
              {validations.sort((a, b) => levelOrder[a.level] - levelOrder[b.level]).map((v, i) => (
                <li key={i} className={"val-" + v.level}>
                  <span className="val-icon">
                    {v.level === "ok" && "✓"}
                    {v.level === "warn" && "!"}
                    {v.level === "fail" && "✕"}
                    {v.level === "wait" && "…"}
                  </span>
                  <span>{v.msg}</span>
                </li>
              ))}
            </ul>
          )}
        </div>

        {/* Right: preview */}
        <div className="admin-col">
          <h3 className="admin-section-title">{lang === "cht" ? "預覽（雙語）" : "Preview (both languages)"}</h3>
          {parsedDay ? (
            <div className="admin-preview-wrap">
              <div className="admin-preview-row">
                <div>
                  <div className="admin-preview-lang">繁體中文</div>
                  <div className="preview-card">
                    {imgCht ? (
                      <img src={imgCht.url} alt="" />
                    ) : (
                      <MiniInfographic day={parsedDay} lang="cht" />
                    )}
                  </div>
                </div>
                <div>
                  <div className="admin-preview-lang">English</div>
                  <div className="preview-card">
                    {imgEng ? (
                      <img src={imgEng.url} alt="" />
                    ) : (
                      <MiniInfographic day={parsedDay} lang="eng" />
                    )}
                  </div>
                </div>
              </div>

              <div className="admin-preview-meta">
                <div><strong>{lang === "cht" ? "日期" : "Date"}</strong>: {parsedDay.date_cht} · {parsedDay.date_eng}</div>
                <div><strong>{lang === "cht" ? "經文" : "Scripture"}</strong>: {parsedDay.scripture.ref_cht} · {parsedDay.scripture.ref_eng}</div>
                <div><strong>{lang === "cht" ? "天氣" : "Weather"}</strong>: {parsedDay.weather.badge_cht} · {parsedDay.weather.badge_eng}</div>
              </div>
            </div>
          ) : (
            <div className="hint">{lang === "cht" ? "預覽會在上傳 JSON 後顯示。" : "Preview appears once JSON is uploaded."}</div>
          )}
        </div>
      </div>

      {/* Password panel — collapsible */}
      <div className="gh-panel">
        <button className="gh-toggle" onClick={() => setShowPassword(!showPassword)}>
          <Icon name="globe" size={14} />
          <span>{lang === "cht" ? "發佈密碼" : "Publish password"}</span>
          <span className="gh-status">
            {password
              ? <span className="gh-ok">{lang === "cht" ? "已設定" : "Saved"}</span>
              : <span className="gh-warn">{lang === "cht" ? "尚未設定" : "Not set"}</span>}
          </span>
          <Icon name={showPassword ? "chevron-left" : "chevron-right"} size={14} />
        </button>
        {showPassword && (
          <div className="gh-form">
            <div className="gh-row">
              <label>{lang === "cht" ? "密碼" : "Password"}</label>
              <input id="publish-password" name="publish-password" type="password" placeholder={lang === "cht" ? "輸入發佈密碼" : "Enter publish password"}
                value={password}
                onChange={(e) => { setPassword(e.target.value); savePassword(e.target.value); }} />
            </div>
            <p className="gh-hint">
              {lang === "cht"
                ? "密碼儲存於本機瀏覽器（localStorage）。GitHub Token 由伺服器保管，不會出現在瀏覽器中。"
                : "Password is stored in this browser only (localStorage). The GitHub token is kept server-side and never exposed to the browser."}
            </p>
          </div>
        )}
      </div>

      {/* Publish bar */}
      <div className="publish-bar">
        <div className="publish-status">
          {ready ? (
            <><span className="dot ok"></span><span>{lang === "cht" ? "全部就緒，可發佈" : "All set — ready to publish"}</span></>
          ) : (
            <><span className="dot wait"></span><span>{lang === "cht" ? "完成所需檔案與驗證後方可發佈" : "Resolve required files & failures to enable publish"}</span></>
          )}
        </div>
        <div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
          <button className="ghost-btn" onClick={previewEntry} disabled={!parsedDay}>
            <Icon name="image" size={13} />
            {lang === "cht" ? "預覽完整頁面" : "Preview full page"}
          </button>
          <button className="ghost-btn" onClick={publish} disabled={!ready || busy}>
            <Icon name="download" size={13} />
            {lang === "cht" ? "下載 ZIP" : "Download ZIP"}
          </button>
          <button className="primary-btn" onClick={publishToGitHub} disabled={!ready || busy}>
            <Icon name="upload" size={14} />
            {busy ? (lang === "cht" ? "發佈中…" : "Publishing…") : (lang === "cht" ? "發佈到 GitHub" : "Publish to GitHub")}
          </button>
        </div>
      </div>

      {/* Publish log */}
      {publishLog.length > 0 && (
        <div className="publish-log">
          {publishLog.map((l, i) => (
            <div key={i} className={"log-row log-" + l.level}>
              <span className="log-mark">
                {l.level === "ok" && "✓"}
                {l.level === "fail" && "✕"}
                {l.level === "info" && "…"}
                {l.level === "done" && "★"}
              </span>
              <span>{l.msg}</span>
            </div>
          ))}
        </div>
      )}

      <div className="admin-hint">
        {lang === "cht"
          ? "發佈到 GitHub 後，Cloudflare Pages 會在約 30 秒內重新部署你的網站。"
          : "After publishing to GitHub, Cloudflare Pages redeploys your site in ~30 seconds."}
      </div>
    </div>
  );
}

function FileRow({ have, label, sub, optional }) {
  return (
    <li className={"file-row" + (have ? " have" : "") + (optional ? " optional" : "")}>
      <span className="file-status">{have ? "✓" : (optional ? "○" : "—")}</span>
      <div>
        <div className="file-name">{label}</div>
        <div className="file-sub">{sub}</div>
      </div>
    </li>
  );
}

// ── Manage (edit existing days) view ─────────────────────────────────────────

const MONTHS_CHT = ["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"];
const MONTHS_ENG = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];

function ManageView({ lang }) {
  const days = window.DAYS;
  const isoSet = React.useMemo(() => new Set(days.map(d => d.iso)), []);

  const [calYear, setCalYear]   = React.useState(() => parseInt((days[0]?.iso || new Date().toISOString().slice(0,10)).slice(0,4)));
  const [calMonth, setCalMonth] = React.useState(() => parseInt((days[0]?.iso || new Date().toISOString().slice(0,10)).slice(5,7)) - 1);
  const [selectedIso, setSelectedIso] = React.useState(null);

  const [editJson,    setEditJson]    = React.useState(null);
  const [editImgCht,  setEditImgCht]  = React.useState(null);
  const [editImgEng,  setEditImgEng]  = React.useState(null);
  const [editHtmlCht, setEditHtmlCht] = React.useState(null);
  const [editHtmlEng, setEditHtmlEng] = React.useState(null);

  // Staged changes queue: each item = { id, iso, type, label, path, file, isText, parsedDay, oldParsedDay, newSrc, oldSrc, htmlContent }
  const [staged,        setStaged]       = React.useState([]);
  const [diffItem,      setDiffItem]     = React.useState(null); // item being reviewed in the diff modal
  const [commitMsg,     setCommitMsg]    = React.useState("");
  const [showCommitBar, setShowCommitBar]= React.useState(false);

  const [busy,          setBusy]          = React.useState(false);
  const [log,           setLog]           = React.useState([]);
  const [confirmDelete, setConfirmDelete] = React.useState(false);
  const [password,      setPasswordState] = React.useState(loadPassword);
  const [showPassword,  setShowPassword]  = React.useState(() => !loadPassword());

  const selectedDay = days.find(d => d.iso === selectedIso) || null;

  function selectDay(iso) {
    setSelectedIso(iso);
    setEditJson(null); setEditImgCht(null); setEditImgEng(null);
    setEditHtmlCht(null); setEditHtmlEng(null);
    setLog([]); setConfirmDelete(false);
  }

  function stageChange(item) {
    // Replace any existing staged item for the same path
    setStaged(prev => {
      const filtered = prev.filter(s => s.path !== item.path);
      return [...filtered, item];
    });
    setShowCommitBar(true);
    // Clear the editor slot
    if (item.type === "json")     setEditJson(null);
    if (item.type === "img_cht")  setEditImgCht(null);
    if (item.type === "img_eng")  setEditImgEng(null);
    if (item.type === "html_cht") setEditHtmlCht(null);
    if (item.type === "html_eng") setEditHtmlEng(null);
  }

  function unstage(id) {
    setStaged(prev => {
      const next = prev.filter(s => s.id !== id);
      if (!next.length) setShowCommitBar(false);
      return next;
    });
  }

  function prevMonth() {
    if (calMonth === 0) { setCalMonth(11); setCalYear(y => y - 1); }
    else setCalMonth(m => m - 1);
  }
  function nextMonth() {
    if (calMonth === 11) { setCalMonth(0); setCalYear(y => y + 1); }
    else setCalMonth(m => m + 1);
  }

  const calCells = React.useMemo(() => {
    const firstDay = new Date(calYear, calMonth, 1).getDay();
    const daysInMonth = new Date(calYear, calMonth + 1, 0).getDate();
    const cells = [];
    for (let i = 0; i < firstDay; i++) cells.push(null);
    for (let d = 1; d <= daysInMonth; d++) {
      const iso = `${calYear}-${String(calMonth+1).padStart(2,"0")}-${String(d).padStart(2,"0")}`;
      cells.push({ d, iso, has: isoSet.has(iso) });
    }
    return cells;
  }, [calYear, calMonth, isoSet]);

  const addLog = (level, msg) => setLog(prev => [...prev, { level, msg }]);

  function handleStageJson() {
    if (!editJson || !selectedIso) return;
    let parsed;
    try { parsed = JSON.parse(editJson.raw); } catch(e) { alert("JSON error: " + e.message); return; }
    const newDay = convertJsonToDay(parsed);
    if (selectedDay?.articles) newDay.articles = selectedDay.articles;
    const isoFlat = selectedIso.replace(/-/g, "");
    stageChange({
      id: `${selectedIso}:json:${Date.now()}`,
      iso: selectedIso, type: "json",
      label: `${selectedIso} / day.json`,
      path: `assets/${isoFlat}/day.json`,
      file: editJson.file, isText: true,
      parsedDay: newDay, oldParsedDay: selectedDay,
      newSrc: null, oldSrc: null, htmlContent: null,
    });
  }

  function handleStageImage(side) {
    const img = side === "cht" ? editImgCht : editImgEng;
    if (!img || !selectedIso) return;
    const isoFlat = selectedIso.replace(/-/g, "");
    const ext = img.file.name.split(".").pop().toLowerCase() || "webp";
    stageChange({
      id: `${selectedIso}:img_${side}:${Date.now()}`,
      iso: selectedIso, type: `img_${side}`,
      label: `${selectedIso} / ${side}.${ext}`,
      path: `assets/${isoFlat}/${side}.${ext}`,
      file: img.file, isText: false,
      parsedDay: null, oldParsedDay: null,
      newSrc: img.url,
      oldSrc: `assets/${isoFlat}/${side}.webp`,
      htmlContent: null,
      dims: `${img.w}×${img.h}`,
    });
  }

  function handleStageHtml(side) {
    const html = side === "cht" ? editHtmlCht : editHtmlEng;
    if (!html || !selectedIso) return;
    const isoFlat = selectedIso.replace(/-/g, "");
    stageChange({
      id: `${selectedIso}:html_${side}:${Date.now()}`,
      iso: selectedIso, type: `html_${side}`,
      label: `${selectedIso} / devotional_${side}_${isoFlat}.html`,
      path: `assets/${isoFlat}/devotional_${side}_${isoFlat}.html`,
      file: html.file, isText: true,
      parsedDay: null, oldParsedDay: null,
      newSrc: null, oldSrc: null,
      htmlContent: html.content,
    });
  }

  async function handleBulkCommit() {
    const pw = loadPassword();
    if (!pw) { setShowPassword(true); return; }
    if (!staged.length) return;
    setBusy(true); setLog([]);

    const msg = commitMsg.trim() || `bulk update: ${staged.map(s => s.label).join(", ")}`;
    addLog("info", lang === "cht" ? `準備提交 ${staged.length} 個檔案…` : `Preparing ${staged.length} file(s)…`);

    try {
      // Build days.js incorporating all staged JSON changes
      let updatedDays = [...window.DAYS];
      for (const item of staged) {
        if (item.type === "json" && item.parsedDay) {
          updatedDays = [item.parsedDay, ...updatedDays.filter(d => d.iso !== item.parsedDay.iso)]
            .sort((a, b) => b.iso.localeCompare(a.iso));
        }
        if (item.type.startsWith("html_")) {
          const side = item.type.slice(5);
          updatedDays = updatedDays.map(d =>
            d.iso === item.iso ? { ...d, articles: { ...(d.articles || {}), [side]: true } } : d
          );
        }
      }
      const daysJs = buildDaysFile(
        updatedDays.map(d => { const { _source_json, _admin_preview, ...clean } = d; return clean; })
      );

      const form = new FormData();
      form.append("password", pw);
      form.append("commit_msg", msg);
      form.append("days_js", daysJs);
      form.append("n", String(staged.length));
      staged.forEach((item, i) => {
        form.append(`path_${i}`, item.path);
        form.append(`file_${i}`, item.file);
        form.append(`is_text_${i}`, item.isText ? "1" : "0");
      });

      addLog("info", lang === "cht" ? "上傳中…" : "Uploading…");
      const r = await fetch("/api/bulk-update", { method: "POST", body: form });
      const data = await r.json();
      if (!r.ok || !data.ok) throw new Error(data.detail || data.error || `HTTP ${r.status}`);

      // Apply changes to in-memory window.DAYS
      for (const item of staged) {
        if (item.type === "json" && item.parsedDay) {
          const idx = window.DAYS.findIndex(d => d.iso === item.iso);
          if (idx >= 0) window.DAYS[idx] = { ...item.parsedDay, articles: item.parsedDay.articles };
          else window.DAYS.push(item.parsedDay);
        }
        if (item.type.startsWith("html_")) {
          const side = item.type.slice(5);
          const idx = window.DAYS.findIndex(d => d.iso === item.iso);
          if (idx >= 0) window.DAYS[idx] = { ...window.DAYS[idx], articles: { ...(window.DAYS[idx].articles || {}), [side]: true } };
        }
      }

      addLog("done", `✓ ${data.file_count} ${lang === "cht" ? "個檔案已提交" : "file(s) committed"}`);
      addLog("done", `commit: ${data.commit_sha?.slice(0,7)} — ${data.commit_url}`);
      setStaged([]); setCommitMsg(""); setShowCommitBar(false); setDiffItem(null);
    } catch(e) { addLog("fail", String(e.message || e)); }
    finally { setBusy(false); }
  }

  async function handleDelete() {
    if (!selectedIso) return;
    if (!password) { setShowPassword(true); return; }
    setBusy(true); setLog([]);
    try {
      const updatedDays = window.DAYS.filter(d => d.iso !== selectedIso)
        .map(d => { const { _source_json, _admin_preview, ...clean } = d; return clean; });
      const r = await fetch("/api/delete", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ password, isos: [selectedIso], days_js: buildDaysFile(updatedDays) })
      });
      const data = await r.json();
      if (!r.ok || !data.ok) throw new Error(data.detail || data.error || `HTTP ${r.status}`);
      const idx = window.DAYS.findIndex(d => d.iso === selectedIso);
      if (idx >= 0) window.DAYS.splice(idx, 1);
      addLog("done", lang === "cht" ? `✓ 已刪除 ${selectedIso}` : `✓ Deleted ${selectedIso}`);
      setSelectedIso(null); setConfirmDelete(false);
    } catch(e) { addLog("fail", String(e.message || e)); }
    finally { setBusy(false); }
  }

  const DOW_CHT = ["日","一","二","三","四","五","六"];
  const DOW_ENG = ["Su","Mo","Tu","We","Th","Fr","Sa"];

  return (
    <div className="admin-page">
      <div className="admin-header">
        <div className="admin-eyebrow"><Icon name="upload" size={12} /><span>{lang === "cht" ? "管理員介面" : "Internal tool"}</span></div>
        <h2 className="admin-title">{lang === "cht" ? "管理現有靜思" : "Manage existing days"}</h2>
        <p className="admin-lede">{lang === "cht" ? "從月曆選取日期，替換圖片、JSON、文章，或刪除整個日期。" : "Pick a day from the calendar to replace images, JSON, articles, or delete it."}</p>
      </div>

      {/* Calendar */}
      <div className="manage-calendar">
        <div className="cal-header">
          <button className="ghost-btn" onClick={prevMonth}>‹</button>
          <span className="cal-title">
            {lang === "cht" ? `${calYear}年 ${MONTHS_CHT[calMonth]}` : `${MONTHS_ENG[calMonth]} ${calYear}`}
          </span>
          <button className="ghost-btn" onClick={nextMonth}>›</button>
        </div>
        <div className="cal-grid">
          {(lang === "cht" ? DOW_CHT : DOW_ENG).map(d => (
            <div key={d} className="cal-dow">{d}</div>
          ))}
          {calCells.map((cell, i) => cell ? (
            <button
              key={i}
              className={"cal-day" + (cell.has ? " has-entry" : "") + (cell.iso === selectedIso ? " selected" : "")}
              onClick={() => cell.has && selectDay(cell.iso)}
              disabled={!cell.has}
            >{cell.d}</button>
          ) : <div key={i} className="cal-empty" />)}
        </div>
        <div className="cal-legend">
          <span className="cal-dot" /><span>{lang === "cht" ? "有內容的日期" : "Days with content"}</span>
        </div>
      </div>

      {/* Day editor */}
      {selectedDay && (
        <div className="day-editor">
          <h3 className="admin-section-title" style={{marginBottom:16}}>
            {lang === "cht" ? selectedDay.date_cht : selectedDay.date_eng}
          </h3>

          <div className="editor-grid">
            <EditorSlot lang={lang} label="day.json"
              desc={lang === "cht" ? "結構化資料（天氣、經文、歷史）" : "Structured data (weather, scripture, history)"}
              accept=".json,application/json" pending={editJson}
              currentDay={selectedDay}
              pendingDay={editJson?.parsedDay || null}
              onFile={async (f) => {
                try {
                  const raw = await readText(f);
                  const parsed = JSON.parse(raw);
                  const parsedDay = convertJsonToDay(parsed);
                  setEditJson({ name: f.name, raw, file: f, parsedDay });
                } catch(e) { alert((lang === "cht" ? "JSON 格式錯誤：" : "Invalid JSON: ") + e.message); }
              }}
              onClear={() => setEditJson(null)} onSave={handleStageJson} saveLabel={lang === "cht" ? "加入暫存" : "Stage"} busy={busy} />

            <EditorSlot lang={lang}
              label={lang === "cht" ? "繁中圖片 cht.webp" : "CHT image cht.webp"}
              desc={lang === "cht" ? "9:16 繁體中文直式圖" : "9:16 Traditional Chinese infographic"}
              accept="image/*" pending={editImgCht} isImage
              currentSrc={`assets/${selectedIso.replace(/-/g,"")}/cht.webp`}
              onFile={async (f) => { const d = await readImageDims(f); setEditImgCht({ name: f.name, file: f, url: d.url, w: d.w, h: d.h }); }}
              onClear={() => setEditImgCht(null)} onSave={() => handleStageImage("cht")} saveLabel={lang === "cht" ? "加入暫存" : "Stage"} busy={busy} />

            <EditorSlot lang={lang}
              label={lang === "cht" ? "英文圖片 eng.webp" : "ENG image eng.webp"}
              desc={lang === "cht" ? "9:16 英文直式圖" : "9:16 English infographic"}
              accept="image/*" pending={editImgEng} isImage
              currentSrc={`assets/${selectedIso.replace(/-/g,"")}/eng.webp`}
              onFile={async (f) => { const d = await readImageDims(f); setEditImgEng({ name: f.name, file: f, url: d.url, w: d.w, h: d.h }); }}
              onClear={() => setEditImgEng(null)} onSave={() => handleStageImage("eng")} saveLabel={lang === "cht" ? "加入暫存" : "Stage"} busy={busy} />

            <EditorSlot lang={lang}
              label={lang === "cht" ? "繁中文章 devotional_cht_….html" : "CHT article devotional_cht_….html"}
              desc={lang === "cht" ? "長文章（選擇性）" : "Long-form article (optional)"}
              accept=".html,.htm" pending={editHtmlCht}
              hasExisting={selectedDay.articles?.cht}
              onFile={async (f) => { const content = await readText(f); setEditHtmlCht({ name: f.name, content, file: f }); }}
              onClear={() => setEditHtmlCht(null)} onSave={() => handleStageHtml("cht")} saveLabel={lang === "cht" ? "加入暫存" : "Stage"} busy={busy} />

            <EditorSlot lang={lang}
              label={lang === "cht" ? "英文文章 devotional_eng_….html" : "ENG article devotional_eng_….html"}
              desc={lang === "cht" ? "長文章（選擇性）" : "Long-form article (optional)"}
              accept=".html,.htm" pending={editHtmlEng}
              hasExisting={selectedDay.articles?.eng}
              onFile={async (f) => { const content = await readText(f); setEditHtmlEng({ name: f.name, content, file: f }); }}
              onClear={() => setEditHtmlEng(null)} onSave={() => handleStageHtml("eng")} saveLabel={lang === "cht" ? "加入暫存" : "Stage"} busy={busy} />
          </div>

          {/* Delete */}
          <div className="delete-zone">
            {!confirmDelete ? (
              <button className="ghost-btn danger" onClick={() => setConfirmDelete(true)}>
                <Icon name="x" size={13} />
                {lang === "cht" ? `刪除 ${selectedIso}` : `Delete ${selectedIso}`}
              </button>
            ) : (
              <div className="delete-confirm">
                <span>{lang === "cht" ? "確定刪除？此操作無法復原。" : "Delete permanently? Cannot be undone."}</span>
                <button className="primary-btn danger" onClick={handleDelete} disabled={busy}>
                  {lang === "cht" ? "確定刪除" : "Confirm delete"}
                </button>
                <button className="ghost-btn" onClick={() => setConfirmDelete(false)}>
                  {lang === "cht" ? "取消" : "Cancel"}
                </button>
              </div>
            )}
          </div>
        </div>
      )}

      {/* ── Staged changes panel ── */}
      {staged.length > 0 && (
        <div className="staged-panel">
          <div className="staged-header">
            <span style={{fontWeight:600}}>
              {lang === "cht" ? `暫存變更（${staged.length}）` : `Staged Changes (${staged.length})`}
            </span>
            <span style={{opacity:0.5, fontSize:12}}>
              {lang === "cht" ? "點擊項目以比較前後差異" : "Click an item to compare before / after"}
            </span>
          </div>
          {staged.map(item => (
            <div key={item.id} className="staged-item" onClick={() => setDiffItem(item)}>
              <span className="staged-type-badge">{item.type}</span>
              <span className="staged-label">{item.label}</span>
              <button className="ghost-btn staged-remove" onClick={(e) => { e.stopPropagation(); unstage(item.id); }}
                title={lang === "cht" ? "移除" : "Remove"}>✕</button>
            </div>
          ))}

          {/* Commit bar */}
          <div className="staged-commit-bar">
            <input
              type="text"
              placeholder={lang === "cht" ? "提交訊息（選填）" : "Commit message (optional)"}
              value={commitMsg}
              onChange={e => setCommitMsg(e.target.value)}
              style={{flex:1, padding:"6px 10px", borderRadius:6, border:"1px solid #334155", background:"#1e293b", color:"#f1f5f9", minWidth:0}}
            />
            <button className="primary-btn" onClick={handleBulkCommit} disabled={busy || !staged.length}>
              <Icon name="upload" size={13} />
              {busy
                ? (lang === "cht" ? "提交中…" : "Committing…")
                : (lang === "cht" ? `提交全部 ${staged.length} 個` : `Commit All (${staged.length})`)}
            </button>
          </div>
        </div>
      )}

      {/* ── Diff modal ── */}
      {diffItem && (
        <div className="diff-overlay" onClick={() => setDiffItem(null)}>
          <div className="diff-modal" onClick={e => e.stopPropagation()}>
            <div className="diff-modal-header">
              <span style={{fontWeight:600}}>{lang === "cht" ? "比較差異" : "Compare Changes"}: {diffItem.label}</span>
              <button className="ghost-btn" onClick={() => setDiffItem(null)}>✕</button>
            </div>

            {/* JSON diff */}
            {diffItem.type === "json" && (
              <div className="diff-cols">
                <div className="diff-col diff-old">
                  <div className="diff-col-title">{lang === "cht" ? "現有（GitHub）" : "Current (GitHub)"}</div>
                  {diffItem.oldParsedDay ? <JsonSummary day={diffItem.oldParsedDay} lang={lang} /> : <span style={{opacity:0.4}}>—</span>}
                </div>
                <div className="diff-col diff-new">
                  <div className="diff-col-title">{lang === "cht" ? "新版本（待提交）" : "New (staged)"}</div>
                  {diffItem.parsedDay ? <JsonSummary day={diffItem.parsedDay} lang={lang} highlight={diffItem.oldParsedDay} /> : <span style={{opacity:0.4}}>—</span>}
                </div>
              </div>
            )}

            {/* Image diff */}
            {diffItem.type.startsWith("img_") && (
              <div className="diff-cols">
                <div className="diff-col diff-old">
                  <div className="diff-col-title">{lang === "cht" ? "現有（GitHub）" : "Current (GitHub)"}</div>
                  <img src={diffItem.oldSrc} alt="current"
                    style={{width:"100%", borderRadius:6, border:"1px solid #334155"}}
                    onError={e => { e.target.style.opacity="0.2"; }} />
                </div>
                <div className="diff-col diff-new">
                  <div className="diff-col-title">{lang === "cht" ? `新版本 (${diffItem.dims})` : `New (${diffItem.dims})`}</div>
                  <img src={diffItem.newSrc} alt="new"
                    style={{width:"100%", borderRadius:6, border:"2px solid #3b82f6"}} />
                </div>
              </div>
            )}

            {/* HTML diff */}
            {diffItem.type.startsWith("html_") && (
              <div className="diff-cols">
                <div className="diff-col diff-old">
                  <div className="diff-col-title">{lang === "cht" ? "現有檔案" : "Current file"}</div>
                  <div style={{opacity:0.5, fontSize:12}}>
                    {lang === "cht" ? "（從 GitHub 讀取需時，此處顯示摘要）" : "(Full fetch skipped — summary shown)"}
                  </div>
                  <div style={{marginTop:8, opacity:0.6, fontSize:12}}>
                    {diffItem.type === "html_cht"
                      ? (selectedDay?.articles?.cht ? (lang === "cht" ? "✓ 檔案存在" : "✓ File exists") : (lang === "cht" ? "— 尚無檔案" : "— No file yet"))
                      : (selectedDay?.articles?.eng ? (lang === "cht" ? "✓ 檔案存在" : "✓ File exists") : (lang === "cht" ? "— 尚無檔案" : "— No file yet"))
                    }
                  </div>
                </div>
                <div className="diff-col diff-new">
                  <div className="diff-col-title">{lang === "cht" ? `新版本（${diffItem.htmlContent?.length?.toLocaleString()} 字元）` : `New (${diffItem.htmlContent?.length?.toLocaleString()} chars)`}</div>
                  <pre style={{fontSize:11, overflowX:"auto", maxHeight:320, whiteSpace:"pre-wrap", wordBreak:"break-all", opacity:0.8, marginTop:8}}>
                    {diffItem.htmlContent?.slice(0, 800)}{diffItem.htmlContent?.length > 800 ? "\n…" : ""}
                  </pre>
                </div>
              </div>
            )}

            <div style={{marginTop:16, display:"flex", gap:8, justifyContent:"flex-end"}}>
              <button className="ghost-btn danger" onClick={() => { unstage(diffItem.id); setDiffItem(null); }}>
                {lang === "cht" ? "移除此暫存" : "Remove from stage"}
              </button>
              <button className="primary-btn" onClick={() => setDiffItem(null)}>
                {lang === "cht" ? "關閉" : "Close"}
              </button>
            </div>
          </div>
        </div>
      )}

      {/* Log */}
      {log.length > 0 && (
        <div className="publish-log">
          {log.map((l, i) => (
            <div key={i} className={"log-row log-" + l.level}>
              <span className="log-mark">
                {l.level === "ok" && "✓"}{l.level === "fail" && "✕"}
                {l.level === "info" && "…"}{l.level === "done" && "★"}
              </span>
              <span>{l.msg}</span>
            </div>
          ))}
        </div>
      )}

      {/* Password panel */}
      <div className="gh-panel">
        <button className="gh-toggle" onClick={() => setShowPassword(!showPassword)}>
          <Icon name="globe" size={14} />
          <span>{lang === "cht" ? "發佈密碼" : "Publish password"}</span>
          <span className="gh-status">
            {password ? <span className="gh-ok">{lang === "cht" ? "已設定" : "Saved"}</span>
                      : <span className="gh-warn">{lang === "cht" ? "尚未設定" : "Not set"}</span>}
          </span>
          <Icon name={showPassword ? "chevron-left" : "chevron-right"} size={14} />
        </button>
        {showPassword && (
          <div className="gh-form">
            <div className="gh-row">
              <label>{lang === "cht" ? "密碼" : "Password"}</label>
              <input id="update-password" name="update-password" type="password" placeholder={lang === "cht" ? "輸入發佈密碼" : "Enter publish password"}
                value={password}
                onChange={(e) => { setPasswordState(e.target.value); savePassword(e.target.value); }} />
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

function EditorSlot({ lang, label, desc, accept, pending, isImage, currentSrc, hasExisting, currentDay, pendingDay, onFile, onClear, onSave, saveLabel, busy }) {
  const inputRef = React.useRef(null);
  const showCompare = pending && (isImage || currentDay);

  return (
    <div className={"editor-slot" + (pending ? " has-pending" : "")}>
      <div className="editor-slot-top">
        <div style={{flex:1}}>
          <div className="file-name">{label}</div>
          <div className="file-sub">{desc}</div>
          {!isImage && hasExisting && !pending && (
            <div className="file-sub" style={{color:"var(--ok,#22c55e)"}}>
              ✓ {lang === "cht" ? "檔案存在" : "File exists"}
            </div>
          )}
        </div>
        <button className="ghost-btn" style={{flexShrink:0}} onClick={() => inputRef.current?.click()} disabled={busy}>
          {lang === "cht" ? "選擇" : "Choose"}
        </button>
        <input ref={inputRef} type="file" accept={accept} style={{display:"none"}}
          onChange={(e) => { if (e.target.files[0]) { onFile(e.target.files[0]); e.target.value = ""; } }} />
      </div>

      {/* Image: always show current; show new alongside when pending */}
      {isImage && !pending && (
        <div className="editor-img-row">
          <img className="editor-img-thumb" src={currentSrc} alt=""
            onError={(e) => { e.target.style.opacity = "0.2"; }} />
          <span className="file-sub">{lang === "cht" ? "現有圖片" : "Current"}</span>
        </div>
      )}
      {isImage && pending && (
        <div className="compare-row">
          <div className="compare-col">
            <div className="compare-label">{lang === "cht" ? "現有" : "Current"}</div>
            <img className="editor-img-thumb compare-img" src={currentSrc} alt=""
              onError={(e) => { e.target.style.opacity = "0.2"; }} />
          </div>
          <div className="compare-arrow">→</div>
          <div className="compare-col">
            <div className="compare-label">{lang === "cht" ? "新圖片" : "New"}</div>
            <img className="editor-img-thumb compare-img" src={pending.url} alt="" />
            <div className="file-sub" style={{marginTop:4}}>{pending.w}×{pending.h}</div>
          </div>
        </div>
      )}

      {/* JSON compare: show key fields side by side */}
      {!isImage && pending && currentDay && pendingDay && (
        <div className="compare-row json-compare">
          <div className="compare-col">
            <div className="compare-label">{lang === "cht" ? "現有" : "Current"}</div>
            <JsonSummary day={currentDay} lang={lang} />
          </div>
          <div className="compare-arrow">→</div>
          <div className="compare-col">
            <div className="compare-label">{lang === "cht" ? "新內容" : "New"}</div>
            <JsonSummary day={pendingDay} lang={lang} highlight={currentDay} />
          </div>
        </div>
      )}
      {!isImage && pending && !currentDay && (
        <div className="file-sub" style={{padding:"4px 0"}}>{pending.name}</div>
      )}

      {pending && (
        <div className="editor-slot-actions">
          <button className="primary-btn" onClick={onSave} disabled={busy}>
            <Icon name="upload" size={13} />
            {saveLabel || (lang === "cht" ? "上傳" : "Upload")}
          </button>
          <button className="ghost-btn" onClick={onClear} disabled={busy}>
            {lang === "cht" ? "取消" : "Cancel"}
          </button>
        </div>
      )}
    </div>
  );
}

function JsonSummary({ day, lang, highlight }) {
  if (!day) return null;
  const rows = [
    { k: lang === "cht" ? "日期" : "Date",      v: lang === "cht" ? day.date_cht : day.date_eng },
    { k: lang === "cht" ? "天氣" : "Weather",   v: day.weather?.badge_cht || day.weather?.badge_eng },
    { k: lang === "cht" ? "經文" : "Scripture", v: lang === "cht" ? day.scripture?.ref_cht : day.scripture?.ref_eng },
    { k: lang === "cht" ? "靈修" : "Reflection",v: (lang === "cht" ? day.reflection?.cht : day.reflection?.eng || "")?.slice(0, 40) + "…" },
  ];
  return (
    <ul className="json-summary">
      {rows.map(({ k, v }) => {
        const changed = highlight && highlight[k] !== v;
        return (
          <li key={k} className={"json-summary-row" + (changed ? " changed" : "")}>
            <span className="jskey">{k}</span>
            <span className="jsval">{v || "—"}</span>
          </li>
        );
      })}
    </ul>
  );
}

// ── Security Log viewer ───────────────────────────────────────────────────────

function SecurityLogView({ lang }) {
  const [entries, setEntries]   = React.useState(null); // null = not loaded
  const [loading, setLoading]   = React.useState(false);
  const [error, setError]       = React.useState(null);
  const [password, setPasswordState] = React.useState(loadPassword);

  async function loadLog() {
    const pw = loadPassword();
    if (!pw) { setError(lang === "cht" ? "請先設定密碼" : "Enter password first"); return; }
    setLoading(true); setError(null);
    try {
      const res = await fetch(`/api/seclog?password=${encodeURIComponent(pw)}`);
      const data = await res.json();
      if (!res.ok) { setError(data.error || "error"); return; }
      setEntries(data.entries || []);
    } catch(e) {
      setError(String(e.message || e));
    } finally {
      setLoading(false);
    }
  }

  const resultColor = (r) => r === "ok" ? "var(--ok,#22c55e)" : "#ef4444";
  const actionIcon  = (a) => ({ login:"🔑", publish:"🚀", update:"✏️", delete:"🗑️" }[a] || "📋");

  return (
    <div className="admin-section">
      <div style={{display:"flex", gap:8, alignItems:"center", marginBottom:16, flexWrap:"wrap"}}>
        <input id="seclog-password" name="seclog-password" type="password"
          placeholder={lang === "cht" ? "輸入密碼以載入日誌" : "Enter password to load log"}
          value={password}
          style={{flex:"1 1 200px", padding:"6px 10px", borderRadius:6, border:"1px solid #334155", background:"#1e293b", color:"#f1f5f9"}}
          onChange={(e) => { setPasswordState(e.target.value); savePassword(e.target.value); }} />
        <button className="primary-btn" onClick={loadLog} disabled={loading}>
          {loading ? (lang === "cht" ? "載入中…" : "Loading…") : (lang === "cht" ? "載入日誌" : "Load Log")}
        </button>
      </div>

      {error && <div style={{color:"#ef4444", marginBottom:12}}>{error}</div>}

      {entries !== null && (
        entries.length === 0
          ? <div className="hint">{lang === "cht" ? "尚無安全日誌。" : "No security log entries yet."}</div>
          : <div style={{overflowX:"auto"}}>
              <table style={{width:"100%", borderCollapse:"collapse", fontSize:12}}>
                <thead>
                  <tr style={{background:"#1e293b", textAlign:"left"}}>
                    {["Time","Action","Result","IP","Country","City","Device"].map(h => (
                      <th key={h} style={{padding:"6px 10px", borderBottom:"1px solid #334155", whiteSpace:"nowrap"}}>{h}</th>
                    ))}
                  </tr>
                </thead>
                <tbody>
                  {entries.map((e, i) => (
                    <tr key={i} style={{borderBottom:"1px solid #1e293b", background: i%2===0?"#0f172a":"#111827"}}>
                      <td style={{padding:"5px 10px", whiteSpace:"nowrap", opacity:0.7}}>{new Date(e.ts).toLocaleString()}</td>
                      <td style={{padding:"5px 10px", whiteSpace:"nowrap"}}>{actionIcon(e.action)} {e.action}{e.detail ? <span style={{opacity:0.5, marginLeft:4, fontSize:11}}>{e.detail}</span> : null}</td>
                      <td style={{padding:"5px 10px", fontWeight:600, color:resultColor(e.result)}}>{e.result}</td>
                      <td style={{padding:"5px 10px", fontFamily:"monospace"}}>{e.ip}</td>
                      <td style={{padding:"5px 10px"}}>{e.country}</td>
                      <td style={{padding:"5px 10px", opacity:0.7}}>{e.city || "—"}</td>
                      <td style={{padding:"5px 10px", maxWidth:220, overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap", opacity:0.6, fontSize:11}}>{e.ua}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
              <div style={{marginTop:8, opacity:0.5, fontSize:11}}>
                {entries.length} {lang === "cht" ? "筆記錄（最多保留 500 筆）" : "entries (max 500 kept)"}
              </div>
            </div>
      )}
    </div>
  );
}

// ── Top-level AdminView with tabs ─────────────────────────────────────────────

function AdminView({ lang }) {
  const [tab, setTab] = React.useState("publish");

  // Check every 30 s whether the password has expired; if so, show a notice
  const [expired, setExpired] = React.useState(false);
  React.useEffect(() => {
    const id = setInterval(() => {
      const pw = loadPassword();
      if (!pw && sessionStorage.getItem(PASSWORD_EXPIRY + "_had")) {
        setExpired(true);
      }
    }, 30_000);
    return () => clearInterval(id);
  }, []);

  // Track that a password was set, so we can detect expiry
  React.useEffect(() => {
    if (loadPassword()) sessionStorage.setItem(PASSWORD_EXPIRY + "_had", "1");
  });

  return (
    <div>
      {expired && (
        <div style={{background:"#7c2d12", color:"#fef2f2", padding:"8px 14px", borderRadius:6, marginBottom:12, fontSize:13}}>
          {lang === "cht"
            ? "⏱ 工作階段已逾時（15 分鐘無操作）。請重新輸入密碼。"
            : "⏱ Session expired (15 min inactivity). Please re-enter your password."}
        </div>
      )}
      <div className="admin-tabs">
        <button className={"admin-tab" + (tab === "publish" ? " active" : "")} onClick={() => setTab("publish")}>
          <Icon name="upload" size={13} />
          {lang === "cht" ? "發佈新日期" : "Publish new day"}
        </button>
        <button className={"admin-tab" + (tab === "manage" ? " active" : "")} onClick={() => setTab("manage")}>
          <Icon name="image" size={13} />
          {lang === "cht" ? "管理現有日期" : "Manage existing days"}
          <span className="admin-tab-count">{window.DAYS.length}</span>
        </button>
        <button className={"admin-tab" + (tab === "seclog" ? " active" : "")} onClick={() => setTab("seclog")}>
          <Icon name="globe" size={13} />
          {lang === "cht" ? "安全日誌" : "Security Log"}
        </button>
      </div>
      {tab === "publish" && <PublishView lang={lang} />}
      {tab === "manage"  && <ManageView lang={lang} />}
      {tab === "seclog"  && <SecurityLogView lang={lang} />}
    </div>
  );
}

Object.assign(window, { AdminView });
