// 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 ──────────────────────────────────────────────────────────────────

// Convert the approved JSON shape (from your sample) → internal Day shape used by window.DAYS
function convertJsonToDay(j) {
  const refCht = j.scripture && j.scripture.ref_cht || "";
  const refEng = j.scripture && j.scripture.ref_eng || "";
  const bookCht = refCht.replace(/[\s\d:：\-—–]+.*$/, "").trim() || "未知";
  const bookEng = refEng.replace(/\s+\d.*$/, "").replace(/\s+NIV.*/i, "").trim() || "Unknown";
  // Extract weekday short
  const wkCht = j.date && j.date.cht ? (j.date.cht.match(/星期(.)/) ? 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) : "";
  return {
    iso: 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: {
      world:  { year: j.history.world_event_year || "", cht: j.history.world_event_cht || "", eng: j.history.world_event_eng || "" },
      birth:  { year: j.history.birth_year || "",       cht: j.history.birth_person_cht || "", eng: j.history.birth_person_eng || "" },
      death:  { year: j.history.death_year || "",       cht: j.history.death_person_cht || "", eng: j.history.death_person_eng || "" },
      custom: { year: j.history.custom_year || "",      cht: j.history.custom_cht || "",       eng: j.history.custom_eng || "" }
    },
    scripture: {
      cht: j.scripture.cht || "",
      ref_cht: refCht,
      niv: j.scripture.niv || "",
      ref_eng: refEng,
      book_cht: bookCht,
      book_eng: bookEng
    },
    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
function readText(file) {
  return new Promise((resolve, reject) => {
    const r = new FileReader();
    r.onload = () => resolve(r.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;
}

// ── GitHub publishing ──────────────────────────────────────────────────────

// Settings live in localStorage so users only set them once.
const GH_KEYS = {
  pat: "adm:gh_pat",
  owner: "adm:gh_owner",
  repo: "adm:gh_repo",
  branch: "adm:gh_branch"
};
function loadGhSettings() {
  return {
    pat: localStorage.getItem(GH_KEYS.pat) || "",
    owner: localStorage.getItem(GH_KEYS.owner) || "",
    repo: localStorage.getItem(GH_KEYS.repo) || "",
    branch: localStorage.getItem(GH_KEYS.branch) || "main"
  };
}
function saveGhSettings(s) {
  localStorage.setItem(GH_KEYS.pat, s.pat || "");
  localStorage.setItem(GH_KEYS.owner, s.owner || "");
  localStorage.setItem(GH_KEYS.repo, s.repo || "");
  localStorage.setItem(GH_KEYS.branch, s.branch || "main");
}

// Read a File/Blob → base64 (no data: prefix)
function fileToBase64(file) {
  return new Promise((resolve, reject) => {
    const r = new FileReader();
    r.onload = () => {
      const result = r.result;
      const comma = result.indexOf(",");
      resolve(comma >= 0 ? result.slice(comma + 1) : result);
    };
    r.onerror = reject;
    r.readAsDataURL(file);
  });
}

// UTF-8 string → base64
function utf8ToBase64(s) {
  return btoa(unescape(encodeURIComponent(s)));
}
function base64ToUtf8(s) {
  return decodeURIComponent(escape(atob(s.replace(/\s/g, ""))));
}

// GitHub Contents API
async function ghGetFile(s, path) {
  const url = `https://api.github.com/repos/${s.owner}/${s.repo}/contents/${encodeURI(path)}?ref=${encodeURIComponent(s.branch)}`;
  const r = await fetch(url, { headers: { "Authorization": `Bearer ${s.pat}`, "Accept": "application/vnd.github+json" } });
  if (r.status === 404) return null;
  if (!r.ok) throw new Error(`GET ${path} → ${r.status} ${await r.text()}`);
  return r.json();
}
async function ghPutFile(s, path, base64Content, message, sha) {
  const url = `https://api.github.com/repos/${s.owner}/${s.repo}/contents/${encodeURI(path)}`;
  const body = { message, content: base64Content, branch: s.branch };
  if (sha) body.sha = sha;
  const r = await fetch(url, {
    method: "PUT",
    headers: {
      "Authorization": `Bearer ${s.pat}`,
      "Accept": "application/vnd.github+json",
      "Content-Type": "application/json"
    },
    body: JSON.stringify(body)
  });
  if (!r.ok) throw new Error(`PUT ${path} → ${r.status} ${await r.text()}`);
  return r.json();
}
// Verify creds + repo by probing one file
async function ghProbe(s) {
  const url = `https://api.github.com/repos/${s.owner}/${s.repo}`;
  const r = await fetch(url, { headers: { "Authorization": `Bearer ${s.pat}`, "Accept": "application/vnd.github+json" } });
  if (!r.ok) throw new Error(`Repo check failed: ${r.status} ${r.statusText}`);
  return r.json();
}

// 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 };

// ── The admin view ──────────────────────────────────────────────────────────

function AdminView({ 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 [ghSettings, setGhSettings] = React.useState(loadGhSettings);
  const [showSettings, setShowSettings] = React.useState(() => !loadGhSettings().pat);
  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": "2026-05-17" },
      "title": { "cht": "每日靜思", "eng": "A Daily Moment" },
      "weather": {
        "cht": "天色漸晴，海面風浪稍緩，宜把握黃昏散步。",
        "eng": "Clearing skies and easing winds; a good time for an evening walk.",
        "mood": "短暫陽光", "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_event_year": "1792", "world_event_cht": "紐約證券交易所成立", "world_event_eng": "New York Stock Exchange founded",
        "birth_year": "1749", "birth_person_cht": "愛德華・詹納：天花疫苗之父", "birth_person_eng": "Edward Jenner: pioneer of vaccination",
        "death_year": "1727", "death_person_cht": "凱薩琳一世：俄國女皇", "death_person_eng": "Catherine I: Empress of Russia",
        "custom_year": "1990", "custom_cht": "國際反恐同日設立", "custom_eng": "International Day Against Homophobia established"
      },
      "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,
        "source_url_cht": "https://cnbible.com/romans/13-8.htm",
        "source_url_eng": "https://www.biblegateway.com/passage/?search=Romans%2013%3A8&version=NIV"
      },
      "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-2026-05-17.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 directly to GitHub via the Contents API.
  async function publishToGitHub() {
    if (!ready) return;
    const s = ghSettings;
    if (!s.pat || !s.owner || !s.repo) {
      setShowSettings(true);
      return;
    }
    setBusy(true);
    setPublishLog([{ level: "info", msg: lang === "cht" ? "正在連線 GitHub…" : "Connecting to GitHub…" }]);

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

    try {
      // 0. Verify repo
      await ghProbe(s);
      log("ok", lang === "cht" ? "GitHub 連線正常" : "GitHub authenticated");

      const iso = parsedDay.iso;
      const folder = `data/days/${iso}`;
      const commitMsg = `publish ${iso} via admin`;

      // 1. day.json
      const jsonPath = `${folder}/day.json`;
      const jsonExisting = await ghGetFile(s, jsonPath);
      await ghPutFile(s, jsonPath, utf8ToBase64(JSON.stringify(json.parsed, null, 2)), commitMsg, jsonExisting?.sha);
      log("ok", `${jsonPath}`);

      // 2. infographic-cht.png
      const chtPath = `${folder}/infographic-cht.png`;
      const chtExisting = await ghGetFile(s, chtPath);
      await ghPutFile(s, chtPath, await fileToBase64(imgCht.file), commitMsg, chtExisting?.sha);
      log("ok", `${chtPath} (${imgCht.w}×${imgCht.h})`);

      // 3. infographic-eng.png
      const engPath = `${folder}/infographic-eng.png`;
      const engExisting = await ghGetFile(s, engPath);
      await ghPutFile(s, engPath, await fileToBase64(imgEng.file), commitMsg, engExisting?.sha);
      log("ok", `${engPath} (${imgEng.w}×${imgEng.h})`);

      // 4. articles (optional)
      if (htmlCht) {
        const p = `${folder}/article-cht.html`;
        const ex = await ghGetFile(s, p);
        await ghPutFile(s, p, utf8ToBase64(htmlCht.content), commitMsg, ex?.sha);
        log("ok", p);
      }
      if (htmlEng) {
        const p = `${folder}/article-eng.html`;
        const ex = await ghGetFile(s, p);
        await ghPutFile(s, p, utf8ToBase64(htmlEng.content), commitMsg, ex?.sha);
        log("ok", p);
      }

      // 5. Rewrite days.js with the new day prepended
      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 daysExisting = await ghGetFile(s, "days.js");
      await ghPutFile(s, "days.js", utf8ToBase64(daysJsContent), commitMsg, daysExisting?.sha);
      log("ok", "days.js");

      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}
          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>

      {/* GitHub settings panel — collapsible */}
      <div className="gh-panel">
        <button className="gh-toggle" onClick={() => setShowSettings(!showSettings)}>
          <Icon name="globe" size={14} />
          <span>{lang === "cht" ? "GitHub 連線設定" : "GitHub connection"}</span>
          <span className="gh-status">
            {ghSettings.pat && ghSettings.owner && ghSettings.repo
              ? <span className="gh-ok">{ghSettings.owner}/{ghSettings.repo} · {ghSettings.branch}</span>
              : <span className="gh-warn">{lang === "cht" ? "尚未設定 / not configured" : "Not configured"}</span>}
          </span>
          <Icon name={showSettings ? "chevron-left" : "chevron-right"} size={14} />
        </button>
        {showSettings && (
          <div className="gh-form">
            <div className="gh-row">
              <label>Owner</label>
              <input type="text" placeholder="your-github-username"
                value={ghSettings.owner}
                onChange={(e) => { const v = { ...ghSettings, owner: e.target.value }; setGhSettings(v); saveGhSettings(v); }} />
            </div>
            <div className="gh-row">
              <label>Repository</label>
              <input type="text" placeholder="A-Daily-Moment"
                value={ghSettings.repo}
                onChange={(e) => { const v = { ...ghSettings, repo: e.target.value }; setGhSettings(v); saveGhSettings(v); }} />
            </div>
            <div className="gh-row">
              <label>Branch</label>
              <input type="text" placeholder="main"
                value={ghSettings.branch}
                onChange={(e) => { const v = { ...ghSettings, branch: e.target.value }; setGhSettings(v); saveGhSettings(v); }} />
            </div>
            <div className="gh-row">
              <label>Personal Access Token</label>
              <input type="password" placeholder="ghp_… (Fine-grained, Contents: write)"
                value={ghSettings.pat}
                onChange={(e) => { const v = { ...ghSettings, pat: e.target.value }; setGhSettings(v); saveGhSettings(v); }} />
            </div>
            <p className="gh-hint">
              {lang === "cht"
                ? "Token 儲存於本機瀏覽器（localStorage），不會上傳。請於 GitHub Settings → Developer settings → Personal access tokens (Fine-grained) 建立，授予此 repo 的 Contents: Read & write 權限。"
                : "Token is stored in this browser only (localStorage). Create at GitHub → Settings → Developer settings → Personal access tokens (Fine-grained), with Contents: Read & write scope for this repo only."}
            </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>
  );
}

Object.assign(window, { AdminView });
