/* global React, ReactDOM, RELEASE_DATA, LINEAR_WORKSPACE */
const { useState, useMemo, useEffect, useRef, useCallback } = React;

// ---------- helpers ----------
const PRODUCT_LABEL = { VF: "PredictGo", PG: "Pridict Gaming" };
const PRODUCT_SHORT = { VF: "VF", PG: "PG" };
const LINEAR_WS = (typeof LINEAR_WORKSPACE !== "undefined" ? LINEAR_WORKSPACE : "singdm");
const PM_DISPLAY_KEY = "release-notes-pm-display"; // localStorage — toggle for editors/admins to hide PM features
const WORKER_URL = "https://release-notes-sync.jmlin.workers.dev";
const GUEST_PWD_KEY = "release-notes-guest-pwd";       // sessionStorage — today's date string used as password
const GUEST_DATA_KEY = "release-notes-guest-data";     // sessionStorage — cached release data from Worker

// Daily guest password = today's date in Taipei, formatted as MMDD (e.g. 2026-05-04 → "0504")
const todayPasswordTPE = () => {
  const ymd = new Date().toLocaleDateString("en-CA", { timeZone: "Asia/Taipei" });
  return ymd.slice(5).replace("-", "");
};

// ─── Supabase client ───
const supabase = window.supabase.createClient(
  window.SUPABASE_URL,
  window.SUPABASE_ANON_KEY,
  {
    auth: {
      detectSessionInUrl: true,
      persistSession: true,
      autoRefreshToken: true,
    },
  }
);

// Audit log helper — fire-and-forget
function logAudit(action, target_type = null, target_id = null, metadata = null) {
  supabase.from("audit_log").insert({ action, target_type, target_id, metadata }).then(({ error }) => {
    if (error) console.warn("audit_log insert failed:", error.message);
  });
}

function yearOf(d) { return d.slice(0, 4); }

function getTypeTags(r) {
  const tags = [];
  if (r.hotfix) tags.push("hotfix");
  if ((r.whatsNew?.length ?? 0) > 0) tags.push("new");
  if ((r.bugFixes?.length ?? 0) > 0) tags.push("fix");
  return tags;
}

function escapeRe(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }

// Highlight matches inside a string, returns React fragment
function Highlight({ text, query }) {
  if (!query) return <>{text}</>;
  const re = new RegExp(`(${escapeRe(query)})`, "gi");
  const parts = String(text).split(re);
  return (
    <>
      {parts.map((p, i) =>
        re.test(p) && p.toLowerCase() === query.toLowerCase()
          ? <mark key={i} className="hl">{p}</mark>
          : <React.Fragment key={i}>{p}</React.Fragment>
      )}
    </>
  );
}

function matchesQuery(r, q) {
  if (!q) return true;
  const tickets = [
    ...(r.parentTicket ? [r.parentTicket] : []),
    ...(r.whatsNew || []).flatMap(i => i.tickets || []),
    ...(r.bugFixes || []).flatMap(i => i.tickets || []),
  ];
  const hay = [
    r.version, r.title, PRODUCT_LABEL[r.product],
    ...(r.whatsNew || []).flatMap(i => [i.name, i.desc]),
    ...(r.bugFixes || []).flatMap(i => [i.name, i.desc]),
    ...tickets,
  ].join(" \u0001 ").toLowerCase();
  return hay.includes(q.toLowerCase());
}

// ---------- Ticket chip (PM mode only) ----------
function TicketChip({ id, product }) {
  const tint = product === "VF" ? "tint-vf" : "tint-pg";
  return (
    <a
      href={`https://linear.app/${LINEAR_WS}/issue/${id}`}
      target="_blank"
      rel="noopener noreferrer"
      onClick={(e) => e.stopPropagation()}
      className={`${tint} mono rounded px-1.5 py-0.5 text-[10.5px] font-semibold tracking-wide hover:brightness-125 transition whitespace-nowrap`}
      title={`Open ${id} in Linear`}
    >
      {id}
    </a>
  );
}

// ---------- icons (inline, minimal) ----------
const Icon = {
  Search: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <circle cx="11" cy="11" r="7" /><path d="m20 20-3.5-3.5" />
    </svg>
  ),
  Chev: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="m6 9 6 6 6-6" />
    </svg>
  ),
  X: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M18 6 6 18M6 6l12 12" />
    </svg>
  ),
  SortDesc: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M3 6h13M3 12h9M3 18h5"/><path d="m19 6 0 12"/><path d="m15 14 4 4 4-4"/>
    </svg>
  ),
  SortAsc: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M3 6h5M3 12h9M3 18h13"/><path d="m19 18 0-12"/><path d="m15 10 4-4 4 4"/>
    </svg>
  ),
  Dot: (p) => (
    <svg viewBox="0 0 10 10" {...p}><circle cx="5" cy="5" r="4" fill="currentColor"/></svg>
  ),
  Sun: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <circle cx="12" cy="12" r="4"/>
      <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
    </svg>
  ),
  Moon: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
    </svg>
  ),
};

// ---------- Theme management ----------
const THEME_KEY = "release-notes-theme";

function getInitialTheme() {
  const saved = localStorage.getItem(THEME_KEY);
  if (saved === "light" || saved === "dark") return saved;
  return window.matchMedia?.("(prefers-color-scheme: light)").matches ? "light" : "dark";
}

function applyTheme(theme) {
  if (theme === "light") document.body.classList.add("theme-light");
  else document.body.classList.remove("theme-light");
}

function ThemeToggle({ theme, onToggle }) {
  const isDark = theme === "dark";
  return (
    <button
      onClick={onToggle}
      aria-label={isDark ? "切換至淺色模式" : "切換至深色模式"}
      title={isDark ? "切換至淺色模式" : "切換至深色模式"}
      className="flex items-center justify-center w-9 h-9 rounded-lg border border-border bg-surface hover:border-borderStrong text-fg2 hover:text-fg transition-colors"
    >
      {isDark
        ? <Icon.Sun className="w-4 h-4" />
        : <Icon.Moon className="w-4 h-4" />}
    </button>
  );
}

// ---------- sidebar ----------
function Sidebar({ counts, filters, setFilters }) {
  const FilterGroup = ({ title, items, value, onChange }) => (
    <div className="mb-7">
      <div className="px-3 mb-2 text-[11px] font-medium uppercase tracking-[0.12em] text-fg3">{title}</div>
      <div className="flex flex-col gap-0.5">
        {items.map(it => {
          const active = value === it.key;
          return (
            <button
              key={it.key}
              onClick={() => onChange(it.key)}
              className={`filter-row ${active ? "active" : ""} flex items-center justify-between text-left px-3 py-2 rounded-md text-sm text-fg2`}
            >
              <span className="flex items-center gap-2">
                {it.dot && <span className="inline-block w-1.5 h-1.5 rounded-full" style={{background: it.dot}} />}
                <span className={active ? "text-fg" : ""}>{it.label}</span>
              </span>
              {it.count != null && (
                <span className="mono text-[11px] text-fg3">{it.count}</span>
              )}
            </button>
          );
        })}
      </div>
    </div>
  );

  return (
    <aside className="w-[280px] shrink-0 border-r border-border h-screen sticky top-0 overflow-y-auto hidden lg:block">
      <div className="p-5">
        {/* logo */}
        <div className="logo-grad rounded-xl border border-border p-4 mb-8">
          <div className="flex items-center gap-2.5 mb-2">
            <div className="w-7 h-7 rounded-md" style={{background: "linear-gradient(135deg, #3b82f6 0%, #f97316 100%)"}} />
            <div className="text-[13px] font-semibold tracking-tight">Release Notes</div>
          </div>
          <div className="text-[11px] text-fg2 leading-snug mono">
            PredictGo <span className="text-fg3">×</span> Pridict Gaming
          </div>
        </div>

        <FilterGroup
          title="產品"
          value={filters.product}
          onChange={(v) => setFilters(f => ({ ...f, product: v }))}
          items={[
            { key: "all", label: "全部產品", count: counts.product.all },
            { key: "VF",  label: "PredictGo", dot: "#3b82f6", count: counts.product.VF },
            { key: "PG",  label: "Pridict Gaming", dot: "#f97316", count: counts.product.PG },
          ]}
        />

        <FilterGroup
          title="年份"
          value={filters.year}
          onChange={(v) => setFilters(f => ({ ...f, year: v }))}
          items={[
            { key: "all",  label: "全部年份", count: counts.year.all },
            { key: "2026", label: "2026",    count: counts.year["2026"] || 0 },
            { key: "2025", label: "2025",    count: counts.year["2025"] || 0 },
          ]}
        />

        <FilterGroup
          title="類型"
          value={filters.type}
          onChange={(v) => setFilters(f => ({ ...f, type: v }))}
          items={[
            { key: "all",    label: "全部類型", count: counts.type.all },
            { key: "new",    label: "新功能",   dot: "#10b981", count: counts.type.new },
            { key: "fix",    label: "修復",     dot: "#3b82f6", count: counts.type.fix },
            { key: "hotfix", label: "Hotfix",   dot: "#f59e0b", count: counts.type.hotfix },
          ]}
        />

        <div className="hair pt-5 mt-2 px-3 text-[11px] text-fg3 leading-relaxed mono">
          {counts.product.all} versions tracked<br/>
          last update <span className="text-fg2">{counts.lastUpdate}</span>
        </div>
      </div>
    </aside>
  );
}

// ---------- search bar ----------
function SearchBar({ query, setQuery, inputRef }) {
  return (
    <div className="relative">
      <div className="absolute left-4 top-1/2 -translate-y-1/2 text-fg3">
        <Icon.Search className="w-[18px] h-[18px]" />
      </div>
      <input
        ref={inputRef}
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder={`搜尋關鍵字：例如「推薦碼」「錢包」「Mines」「提款」...`}
        className="w-full h-12 pl-11 pr-24 bg-surface border border-border rounded-xl text-[15px] text-fg placeholder:text-fg3 outline-none focus:border-borderStrong transition-colors"
      />
      <div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1.5 pointer-events-none">
        {query ? (
          <button
            onClick={() => setQuery("")}
            className="pointer-events-auto p-1.5 rounded-md hover:bg-surface2 text-fg3 hover:text-fg2 transition-colors"
            aria-label="Clear"
          ><Icon.X className="w-4 h-4" /></button>
        ) : (
          <>
            <span className="kbd">⌘</span>
            <span className="kbd">K</span>
          </>
        )}
      </div>
    </div>
  );
}

// ---------- keyword summary ----------
function KeywordSummary({ query, hits, onJump }) {
  if (!query || hits.length === 0) return null;
  return (
    <div className="mt-3 rounded-xl border border-border bg-surface px-4 py-3">
      <div className="flex flex-wrap items-center gap-2 text-sm text-fg2">
        <span>
          「<span className="text-fg font-medium">{query}</span>」曾在
          <span className="mono text-fg mx-1">{hits.length}</span>
          個版本出現過：
        </span>
        <div className="flex flex-wrap gap-1.5">
          {hits.map(r => (
            <button
              key={r.id}
              onClick={() => onJump(r.id)}
              className={`chip ${r.product === "VF" ? "tint-vf" : "tint-pg"} rounded-full px-2.5 py-0.5 text-[12px] mono hover:brightness-125`}
            >
              {PRODUCT_SHORT[r.product]} {r.version}
            </button>
          ))}
        </div>
      </div>
    </div>
  );
}

// ---------- chips ----------
function QuickChips({ value, onChange, sort, setSort, visibleCount }) {
  const chips = [
    { key: "all",     label: "全部版本" },
    { key: "latest",  label: "最新版本" },
    { key: "hotfix",  label: "含 Hotfix" },
    { key: "new",     label: "含新功能" },
  ];
  return (
    <div className="flex items-center gap-2 flex-wrap">
      <div className="flex items-center gap-1.5 flex-wrap">
        {chips.map(c => {
          const active = value === c.key;
          return (
            <button
              key={c.key}
              onClick={() => onChange(c.key)}
              className={`chip rounded-full px-3 h-8 text-[13px] border
                ${active
                  ? "bg-fg text-bg border-fg"
                  : "bg-surface text-fg2 border-border hover:text-fg hover:border-borderStrong"}`}
            >
              {c.label}
            </button>
          );
        })}
      </div>
      <div className="flex-1" />
      <div className="flex items-center gap-2 text-[12px] text-fg3">
        <span className="mono">{visibleCount} results</span>
        <button
          onClick={() => setSort(sort === "desc" ? "asc" : "desc")}
          className="flex items-center gap-1.5 h-8 px-2.5 rounded-full border border-border bg-surface text-fg2 hover:text-fg hover:border-borderStrong transition-colors"
          title="切換排序"
        >
          {sort === "desc" ? <Icon.SortDesc className="w-3.5 h-3.5" /> : <Icon.SortAsc className="w-3.5 h-3.5" />}
          <span className="text-[12px]">{sort === "desc" ? "最新優先" : "最舊優先"}</span>
        </button>
      </div>
    </div>
  );
}

// ---------- version card ----------
function VersionCard({ r, query, open, onToggle, flash, cardRef, pmMode }) {
  const tint = r.product === "VF" ? "tint-vf" : "tint-pg";

  return (
    <article
      ref={cardRef}
      className={`rounded-xl border border-border bg-surface overflow-hidden ${flash ? "flash" : ""}`}
    >
      {/* header */}
      <button
        onClick={onToggle}
        className="w-full text-left px-5 py-4 flex items-center gap-4 trace-hover transition-colors"
      >
        {/* LEFT block: badge + version + title + hotfix */}
        <div className="flex items-center gap-3 min-w-0 flex-1 flex-wrap">
          <span className={`${tint} rounded-md px-2 py-0.5 text-[11px] font-semibold mono tracking-wide`}>
            {PRODUCT_SHORT[r.product]}
          </span>
          <span className="mono text-[13px] text-fg font-medium">{r.version}</span>
          <span className="text-fg3">·</span>
          <h3 className="text-[15px] text-fg font-medium truncate min-w-0">
            <Highlight text={r.title} query={query} />
          </h3>
          {r.hotfix && (
            <span className="tint-hot rounded px-1.5 py-0.5 text-[10.5px] font-semibold mono uppercase tracking-wider">Hotfix</span>
          )}
          {pmMode && r.parentTicket && (
            <TicketChip id={r.parentTicket} product={r.product} />
          )}
        </div>

        {/* RIGHT block: date + chev */}
        <div className="flex items-center gap-3 shrink-0">
          <span className="mono text-[12.5px] text-fg2">{r.date}</span>
          <span className={`chev ${open ? "open" : ""} text-fg3`}>
            <Icon.Chev className="w-4 h-4" />
          </span>
        </div>
      </button>

      {/* body */}
      <div className={`card-body ${open ? "open" : ""}`}>
        <div className="inner">
          <div className="px-5 pb-5 pt-1">
            <div className="hair mb-4" />
            {r.whatsNew?.length > 0 && (
              <Section
                color="#10b981"
                title="What's New"
                count={r.whatsNew.length}
                items={r.whatsNew}
                query={query}
                product={r.product}
                pmMode={pmMode}
              />
            )}
            {r.bugFixes?.length > 0 && (
              <Section
                color="#3b82f6"
                title="Bug Fixes"
                count={r.bugFixes.length}
                items={r.bugFixes}
                query={query}
                product={r.product}
                pmMode={pmMode}
                compact={r.whatsNew?.length > 0}
              />
            )}
          </div>
        </div>
      </div>
    </article>
  );
}

// ---------- Tab Bar ----------
function TabBar({ active, onChange, upcomingCount, historyCount, pmUnlocked, isAdmin }) {
  const tabs = [
    { key: "history",  label: "Release History",   icon: "📚", count: historyCount },
    { key: "upcoming", label: "Upcoming Releases", icon: "🚀", count: upcomingCount },
    ...(pmUnlocked ? [{ key: "schedule", label: "Schedule", icon: "📅", count: null, pmOnly: true }] : []),
    ...(isAdmin    ? [{ key: "admin",    label: "Admin",    icon: "👤", count: null, adminOnly: true }] : []),
  ];
  return (
    <div className="flex items-center gap-1 border-b border-border">
      {tabs.map(t => {
        const isActive = active === t.key;
        return (
          <button
            key={t.key}
            onClick={() => onChange(t.key)}
            className={`relative flex items-center gap-2 px-4 py-2.5 text-[13.5px] font-medium transition-colors
              ${isActive ? "text-fg" : "text-fg3 hover:text-fg2"}`}
          >
            <span>{t.icon}</span>
            <span>{t.label}</span>
            {t.count != null && (
              <span className={`mono text-[11px] px-1.5 py-0.5 rounded ${isActive ? "bg-surface2 text-fg2" : "text-fg3"}`}>
                {t.count}
              </span>
            )}
            {t.pmOnly && (
              <span className="tint-fix mono text-[10px] px-1.5 py-0.5 rounded font-semibold uppercase">PM</span>
            )}
            {t.adminOnly && (
              <span className="tint-hot mono text-[10px] px-1.5 py-0.5 rounded font-semibold uppercase">ADMIN</span>
            )}
            {isActive && (
              <span className="absolute left-0 right-0 -bottom-px h-0.5 bg-fg" />
            )}
          </button>
        );
      })}
    </div>
  );
}

// ═══════════════════════════════════════════════════════════════════
// Admin Panel — only visible/accessible when role === 'admin'
// ═══════════════════════════════════════════════════════════════════

const ROLE_META = {
  admin:  { label: "Admin",  cls: "tint-hot",  desc: "完整權限：管理使用者、所有功能" },
  pm:     { label: "PM",     cls: "tint-vf",   desc: "可編輯 Schedule、看 Linear 票號 / Notion 連結" },
  viewer: { label: "Viewer", cls: "tint-none", desc: "只能看 release notes，無 PM 功能" },
  // 'editor' 角色保留 enum 但 UI 不再選用（與 pm 同權限，視為相容舊資料）
  editor: { label: "Editor", cls: "tint-vf",   desc: "（舊角色，等同 PM）", hidden: true },
};

function fmtDateTime(s) {
  if (!s) return "—";
  const d = new Date(s);
  const yy = d.getFullYear();
  const mm = String(d.getMonth() + 1).padStart(2, "0");
  const dd = String(d.getDate()).padStart(2, "0");
  const hh = String(d.getHours()).padStart(2, "0");
  const mi = String(d.getMinutes()).padStart(2, "0");
  return `${yy}-${mm}-${dd} ${hh}:${mi}`;
}

function EditUserModal({ user, onClose, onSave, currentUserId }) {
  const [role, setRole] = useState(user.role);
  const [isActive, setIsActive] = useState(user.is_active);
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState(null);
  const isSelf = user.id === currentUserId;

  const submit = async () => {
    if (saving) return;
    setSaving(true);
    setError(null);
    const { data, error } = await supabase
      .from("profiles")
      .update({ role, is_active: isActive })
      .eq("id", user.id)
      .select()
      .single();
    setSaving(false);
    if (error) {
      setError(error.message);
      return;
    }
    if (data) {
      logAudit("update_user", "user", user.id, {
        email: user.email,
        role_from: user.role, role_to: role,
        active_from: user.is_active, active_to: isActive,
      });
      onSave(data);
    }
  };

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm px-5"
         onClick={onClose}>
      <div className="bg-surface border border-border rounded-2xl p-6 w-full max-w-[440px]"
           onClick={(e) => e.stopPropagation()}>
        <div className="flex items-center justify-between mb-1">
          <h3 className="text-fg text-[15px] font-semibold">編輯使用者</h3>
          <button onClick={onClose} className="text-fg3 hover:text-fg">
            <Icon.X className="w-4 h-4" />
          </button>
        </div>
        <div className="text-fg3 text-[12px] mono mb-5">{user.email}</div>

        <div>
          <label className="block text-fg2 text-[12px] mb-2">角色</label>
          <div className="flex flex-col gap-2">
            {Object.entries(ROLE_META)
              .filter(([key, meta]) => !meta.hidden || role === key) // hide deprecated unless user already has it
              .map(([key, meta]) => (
              <button key={key}
                onClick={() => setRole(key)}
                disabled={isSelf && key !== "admin"}
                title={isSelf && key !== "admin" ? "不能改自己的 admin 角色（避免鎖死）" : ""}
                className={`text-left rounded-lg p-3 border transition-colors ${
                  role === key
                    ? "border-borderStrong bg-surface2"
                    : "border-border hover:border-borderStrong hover:bg-surface2"
                } ${isSelf && key !== "admin" ? "opacity-40 cursor-not-allowed" : ""}`}>
                <div className="flex items-center gap-2 mb-0.5">
                  <span className={`${meta.cls} mono text-[10.5px] font-semibold uppercase tracking-wider px-2 py-0.5 rounded`}>
                    {meta.label}
                  </span>
                  {role === key && <span className="text-fg3 text-[11px]">✓ 目前選擇</span>}
                </div>
                <div className="text-fg3 text-[12px]">{meta.desc}</div>
              </button>
            ))}
          </div>
        </div>

        <div className="mt-5 pt-5 border-t border-border">
          <label className="flex items-center justify-between cursor-pointer">
            <div>
              <div className="text-fg text-[13.5px] font-medium">啟用帳號</div>
              <div className="text-fg3 text-[12px] mt-0.5">關閉後使用者下次重新整理時無法存取</div>
            </div>
            <button
              onClick={() => !isSelf && setIsActive(!isActive)}
              disabled={isSelf}
              title={isSelf ? "不能停用自己" : ""}
              className={`relative w-11 h-6 rounded-full transition-colors
                ${isActive ? "bg-success" : "bg-surface2 border border-border"}
                ${isSelf ? "opacity-50 cursor-not-allowed" : ""}`}>
              <span className={`absolute top-0.5 w-5 h-5 rounded-full bg-bg transition-transform
                ${isActive ? "translate-x-[22px]" : "translate-x-0.5"}`} />
            </button>
          </label>
        </div>

        {error && (
          <div className="mt-4 text-red-400 text-[12px]">儲存失敗：{error}</div>
        )}

        <div className="flex items-center gap-2 mt-5">
          <div className="flex-1" />
          <button onClick={onClose}
            className="rounded-lg px-4 h-9 text-[12.5px] border border-border bg-surface text-fg2 hover:text-fg hover:border-borderStrong transition-colors">
            取消
          </button>
          <button onClick={submit} disabled={saving}
            className="rounded-lg px-4 h-9 text-[12.5px] bg-fg text-bg font-semibold hover:opacity-90 disabled:opacity-50 transition-opacity">
            {saving ? "儲存中..." : "儲存"}
          </button>
        </div>
      </div>
    </div>
  );
}

function AdminPanel({ currentUserId }) {
  const [section, setSection] = useState("users"); // "users" | "audit"
  const [users, setUsers] = useState([]);
  const [audit, setAudit] = useState([]);
  const [lastSync, setLastSync] = useState(null);
  const [loading, setLoading] = useState(true);
  const [editingUser, setEditingUser] = useState(null);
  const [auditFilter, setAuditFilter] = useState("");
  const [syncing, setSyncing] = useState(false);
  const [syncMsg, setSyncMsg] = useState(null);

  const fetchUsers = async () => {
    const { data, error } = await supabase
      .from("profiles")
      .select("*")
      .order("created_at", { ascending: false });
    if (error) console.error("fetchUsers:", error.message);
    return data || [];
  };
  const fetchAudit = async () => {
    const { data, error } = await supabase
      .from("audit_log")
      .select("*")
      .order("created_at", { ascending: false })
      .limit(200);
    if (error) console.error("fetchAudit:", error.message);
    return data || [];
  };
  const fetchLastSync = async () => {
    const { data } = await supabase
      .from("settings")
      .select("value, updated_at")
      .eq("key", "linear_last_sync")
      .maybeSingle();
    return data;
  };

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    Promise.all([fetchUsers(), fetchAudit(), fetchLastSync()]).then(([u, a, s]) => {
      if (cancelled) return;
      setUsers(u);
      setAudit(a);
      setLastSync(s);
      setLoading(false);
    });
    return () => { cancelled = true; };
  }, []);

  const handleUserSaved = (updated) => {
    setUsers(prev => prev.map(u => u.id === updated.id ? updated : u));
    setEditingUser(null);
    // Also refresh audit log to show the change
    fetchAudit().then(setAudit);
  };

  const handleManualSync = async () => {
    if (syncing) return;
    setSyncing(true);
    setSyncMsg(null);
    try {
      const { data: { session } } = await supabase.auth.getSession();
      const token = session?.access_token;
      if (!token) throw new Error("尚未登入或 session 失效");
      const res = await fetch(WORKER_URL, {
        method: "POST",
        headers: { Authorization: `Bearer ${token}` },
      });
      const ct = res.headers.get("content-type") || "";
      const data = ct.includes("application/json") ? await res.json() : { ok: false, error: `HTTP ${res.status}` };
      if (data.ok) {
        setSyncMsg({ type: "ok", text: `同步完成：${data.cycles} cycles · ${data.dueDates} due dates` });
        const [ls, a] = await Promise.all([fetchLastSync(), fetchAudit()]);
        setLastSync(ls);
        setAudit(a);
      } else {
        setSyncMsg({ type: "err", text: data.error || "同步失敗（未知錯誤）" });
      }
    } catch (e) {
      setSyncMsg({ type: "err", text: `${e.message || e}` });
    } finally {
      setSyncing(false);
      setTimeout(() => setSyncMsg(null), 6000);
    }
  };

  const filteredAudit = audit.filter(a => {
    if (!auditFilter) return true;
    const q = auditFilter.toLowerCase();
    return (a.actor_email || "").toLowerCase().includes(q)
      || (a.action || "").toLowerCase().includes(q)
      || (a.target_id || "").toLowerCase().includes(q);
  });

  return (
    <section className="mt-5 mb-4">
      {/* Linear sync status banner */}
      <div className="rounded-xl border border-border bg-surface px-4 py-3 mb-5 flex flex-col gap-2">
        <div className="flex items-center gap-3 flex-wrap">
          <span className="text-[11.5px] mono uppercase tracking-wider text-fg3">Linear Sync</span>
          {lastSync?.value ? (
            <>
              <span className="text-fg2 text-[12.5px]">
                最後同步：<span className="mono text-fg">{fmtDateTime(lastSync.value.timestamp)}</span>
              </span>
              <span className="text-fg3 text-[11.5px]">·</span>
              <span className="text-fg3 text-[11.5px]">
                {lastSync.value.cycles} cycles, {lastSync.value.due_dates} due dates
              </span>
            </>
          ) : (
            <span className="text-fg3 text-[12.5px] italic">尚未執行過自動同步</span>
          )}
          <div className="flex-1" />
          <span className="text-fg3 text-[11px] mono hidden sm:inline">排程：平日 09:00 / 15:00 (TPE)</span>
          <button
            onClick={handleManualSync}
            disabled={syncing}
            className={`inline-flex items-center gap-1.5 rounded-md px-3 h-7 text-[11.5px] font-semibold border transition-colors
              ${syncing
                ? "bg-surface2 border-border text-fg3 cursor-not-allowed"
                : "bg-fg text-bg border-fg hover:opacity-90"}`}
            title="馬上抓 Linear 資料寫進 Supabase"
          >
            <span className={syncing ? "inline-block animate-spin" : ""}>🔄</span>
            {syncing ? "同步中..." : "立即同步"}
          </button>
        </div>
        {syncMsg && (
          <div className={`text-[12px] mono ${syncMsg.type === "ok" ? "text-success" : "text-red-400"}`}>
            {syncMsg.type === "ok" ? "✓ " : "✗ "}{syncMsg.text}
          </div>
        )}
      </div>

      {/* sub-tabs */}
      <div className="flex items-center gap-1 mb-5 border-b border-border">
        {[
          { key: "users", label: "👤 使用者", count: users.length },
          { key: "audit", label: "📜 操作紀錄", count: audit.length },
        ].map(t => {
          const active = section === t.key;
          return (
            <button key={t.key} onClick={() => setSection(t.key)}
              className={`relative flex items-center gap-2 px-4 py-2.5 text-[13.5px] font-medium transition-colors
                ${active ? "text-fg" : "text-fg3 hover:text-fg2"}`}>
              <span>{t.label}</span>
              <span className={`mono text-[11px] px-1.5 py-0.5 rounded ${active ? "bg-surface2 text-fg2" : "text-fg3"}`}>
                {t.count}
              </span>
              {active && <span className="absolute left-0 right-0 -bottom-px h-0.5 bg-fg" />}
            </button>
          );
        })}
      </div>

      {loading && <div className="text-fg3 text-[13px]">載入中...</div>}

      {!loading && section === "users" && (
        <div className="bg-surface border border-border rounded-xl overflow-hidden">
          <div className="px-4 py-3 border-b border-border flex items-center justify-between">
            <h3 className="text-fg text-[14px] font-semibold">使用者列表</h3>
            <span className="text-fg3 text-[11.5px] mono">{users.length} 個帳號</span>
          </div>
          <div className="divide-y divide-border">
            {users.map(u => {
              const meta = ROLE_META[u.role] || ROLE_META.viewer;
              const isCurrent = u.id === currentUserId;
              return (
                <button key={u.id}
                  onClick={() => setEditingUser(u)}
                  className="w-full text-left px-4 py-3 trace-hover transition-colors flex items-center gap-3">
                  <div className="min-w-0 flex-1">
                    <div className="flex items-center gap-2 flex-wrap">
                      <span className="text-fg text-[13.5px] font-medium truncate">{u.email}</span>
                      {isCurrent && <span className="text-fg3 text-[11px]">(你)</span>}
                      {!u.is_active && <span className="rounded px-1.5 py-0.5 text-[10px] font-semibold mono uppercase border border-red-500/40 text-red-400">停用</span>}
                    </div>
                    <div className="text-fg3 text-[11.5px] mono mt-0.5">
                      建立於 {fmtDateTime(u.created_at)}
                    </div>
                  </div>
                  <span className={`${meta.cls} mono text-[10.5px] font-semibold uppercase tracking-wider px-2 py-1 rounded shrink-0`}>
                    {meta.label}
                  </span>
                  <Icon.Chev className="w-4 h-4 text-fg3 -rotate-90 shrink-0" />
                </button>
              );
            })}
            {users.length === 0 && (
              <div className="px-4 py-8 text-center text-fg3 text-[13px]">尚無使用者</div>
            )}
          </div>
        </div>
      )}

      {!loading && section === "audit" && (
        <div className="bg-surface border border-border rounded-xl overflow-hidden">
          <div className="px-4 py-3 border-b border-border flex items-center justify-between gap-3 flex-wrap">
            <h3 className="text-fg text-[14px] font-semibold shrink-0">操作紀錄（最近 200 筆）</h3>
            <input type="text" value={auditFilter}
              onChange={e => setAuditFilter(e.target.value)}
              placeholder="搜尋 email / action / target..."
              className="h-8 px-3 bg-surface2 border border-border rounded-md text-fg text-[12.5px] outline-none focus:border-borderStrong min-w-[200px]" />
          </div>
          <div className="divide-y divide-border max-h-[640px] overflow-y-auto">
            {filteredAudit.map(a => (
              <div key={a.id} className="px-4 py-2.5 text-[12.5px] flex items-start gap-3">
                <span className="mono text-[11px] text-fg3 shrink-0 w-[120px]">{fmtDateTime(a.created_at)}</span>
                <span className="tint-fix mono rounded px-1.5 py-0.5 text-[10.5px] font-semibold whitespace-nowrap shrink-0">
                  {a.action}
                </span>
                <div className="min-w-0 flex-1 text-fg2">
                  <span className="text-fg">{a.actor_email || "system"}</span>
                  {a.target_type && (
                    <>
                      <span className="text-fg3"> · {a.target_type}</span>
                      {a.target_id && <span className="mono text-fg3"> {a.target_id}</span>}
                    </>
                  )}
                  {a.metadata && Object.keys(a.metadata).length > 0 && (
                    <div className="text-fg3 text-[11px] mt-0.5 mono">
                      {Object.entries(a.metadata).map(([k, v]) => (
                        <span key={k} className="mr-3">{k}: {String(v).slice(0, 40)}</span>
                      ))}
                    </div>
                  )}
                </div>
              </div>
            ))}
            {filteredAudit.length === 0 && (
              <div className="px-4 py-8 text-center text-fg3 text-[13px]">
                {auditFilter ? "無符合的紀錄" : "尚無操作紀錄"}
              </div>
            )}
          </div>
        </div>
      )}

      {editingUser && (
        <EditUserModal
          user={editingUser}
          onClose={() => setEditingUser(null)}
          onSave={handleUserSaved}
          currentUserId={currentUserId}
        />
      )}
    </section>
  );
}

// ---------- Schedule Calendar (Editor / Admin only) ----------

const EVENT_CATEGORIES = {
  milestone: { label: "重要事件時程", emoji: "🚩", color: "rgb(234, 179, 8)" },  // gold
  meeting:   { label: "會議",     emoji: "🗓",  color: "rgb(168, 85, 247)" },  // purple
  demo:      { label: "Demo",     emoji: "🎤",  color: "rgb(var(--success-rgb))" },
  freeze:    { label: "凍結日",   emoji: "❄️",  color: "rgb(var(--hotfix-rgb))" },
  deadline:  { label: "Deadline", emoji: "⏰",  color: "rgb(239, 68, 68)" },   // red
  other:     { label: "其他",     emoji: "📌",  color: "rgb(var(--fg2-rgb))" },
};

// Custom events are now stored in Supabase `custom_events` table.
// localStorage cache is kept only for the offline fallback / migration scenario.
async function fetchCustomEvents() {
  const { data, error } = await supabase
    .from("custom_events")
    .select("*")
    .order("date", { ascending: true });
  if (error) {
    console.error("fetchCustomEvents:", error.message);
    return [];
  }
  return data || [];
}

async function upsertCustomEvent(event, userId) {
  // event has: id (optional, uuid), date, title, category, description
  const payload = {
    date: event.date,
    title: event.title,
    category: event.category,
    description: event.description || null,
  };
  if (event.id && /^[0-9a-f-]{36}$/i.test(event.id)) {
    // existing UUID → update
    const { data, error } = await supabase
      .from("custom_events")
      .update(payload)
      .eq("id", event.id)
      .select()
      .single();
    return { data, error };
  }
  // new → insert (let DB generate UUID)
  const { data, error } = await supabase
    .from("custom_events")
    .insert({ ...payload, created_by: userId || null })
    .select()
    .single();
  return { data, error };
}

async function deleteCustomEvent(id) {
  const { error } = await supabase.from("custom_events").delete().eq("id", id);
  return { error };
}

async function batchInsertCustomEvents(rows, userId) {
  const payload = rows.map(r => ({
    date: r.date,
    title: r.title,
    category: r.category,
    description: r.description || null,
    created_by: userId || null,
  }));
  const { data, error } = await supabase.from("custom_events").insert(payload).select();
  return { data, error };
}

// Generate milestone events from upcoming release cycles.
// For each unique releaseDate, creates: (1) release-day milestone, (2) freeze-day milestone (previous day).
function generateMilestonesFromCycles() {
  const out = [];
  const seenReleaseDates = new Set();
  ((typeof window !== "undefined" && window.NEXT_RELEASES) || []).forEach(c => {
    if (!c.releaseDate) return;
    if (seenReleaseDates.has(c.releaseDate)) return;
    seenReleaseDates.add(c.releaseDate);

    const release = new Date(c.releaseDate + "T00:00:00");
    const freeze = new Date(release.getTime() - 86400000);
    const freezeStr = `${freeze.getFullYear()}-${pad2(freeze.getMonth()+1)}-${pad2(freeze.getDate())}`;

    // Find all products releasing on this date
    const products = (window.NEXT_RELEASES || [])
      .filter(x => x.releaseDate === c.releaseDate)
      .map(x => `${x.product === "VF" ? "PredictGo" : "Pridict Gaming"} ${x.plannedVersion}`);
    const versionLabel = products.join(" + ");

    out.push({
      id: `seed-freeze-${c.releaseDate}`,
      date: freezeStr,
      title: `${c.plannedVersion} Release Freeze`,
      category: "milestone",
      description: "凍結日，不再收新需求進此版本。隔天發版。",
    });
    out.push({
      id: `seed-release-${c.releaseDate}`,
      date: c.releaseDate,
      title: `${c.plannedVersion} 雙產品同步上線`,
      category: "milestone",
      description: versionLabel,
    });
  });
  return out;
}

function buildScheduleEvents(customEvents) {
  const map = {};
  const push = (dateStr, ev) => {
    if (!dateStr) return;
    (map[dateStr] = map[dateStr] || []).push(ev);
  };
  ((typeof window !== "undefined" && window.RELEASE_DATA) || []).forEach(r => {
    push(r.date, {
      kind: "release",
      product: r.product,
      version: r.version,
      title: r.title,
      hotfix: r.hotfix,
      isFuture: false,
    });
  });
  ((typeof window !== "undefined" && window.NEXT_RELEASES) || []).forEach(c => {
    push(c.releaseDate, {
      kind: "upcoming",
      product: c.product,
      version: c.plannedVersion,
      title: `${c.issues?.length || 0} 張票規劃中`,
      hotfix: false,
      isFuture: true,
      issueCount: c.issues?.length || 0,
      isCurrent: c.isCurrent,
    });
  });
  ((typeof window !== "undefined" && window.LINEAR_DUE_DATES) || []).forEach(t => {
    push(t.dueDate, {
      kind: "linear",
      id: t.id,
      product: t.product,
      title: t.title,
      status: t.status,
      statusType: t.statusType,
      priority: t.priority,
      assignee: t.assignee,
      url: t.url,
    });
  });
  (customEvents || []).forEach(c => {
    push(c.date, {
      kind: "custom",
      id: c.id,
      category: c.category || "other",
      title: c.title,
      description: c.description || "",
    });
  });
  return map;
}

function CustomEventModal({ initialData, onSave, onClose, onDelete }) {
  const [date, setDate] = useState(initialData?.date || "");
  const [title, setTitle] = useState(initialData?.title || "");
  const [category, setCategory] = useState(initialData?.category || "milestone");
  const [description, setDescription] = useState(initialData?.description || "");
  const editing = !!initialData?.id;

  const submit = () => {
    if (!date || !title.trim()) return;
    onSave({
      id: initialData?.id || `evt-${Date.now()}`,
      date,
      title: title.trim(),
      category,
      description: description.trim(),
    });
  };

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm px-5"
         onClick={onClose}>
      <div className="bg-surface border border-border rounded-2xl p-6 w-full max-w-[440px]"
           onClick={(e) => e.stopPropagation()}>
        <div className="flex items-center justify-between mb-4">
          <h3 className="text-fg text-[15px] font-semibold">
            {editing ? "編輯事件" : "新增事件"}
          </h3>
          <button onClick={onClose} className="text-fg3 hover:text-fg">
            <Icon.X className="w-4 h-4" />
          </button>
        </div>

        <div className="flex flex-col gap-3.5">
          <div>
            <label className="block text-fg2 text-[12px] mb-1.5">日期</label>
            <input type="date" value={date} onChange={e => setDate(e.target.value)}
              className="w-full h-10 px-3 bg-surface2 border border-border rounded-lg text-fg mono text-[13px] outline-none focus:border-borderStrong"/>
          </div>

          <div>
            <label className="block text-fg2 text-[12px] mb-1.5">標題</label>
            <input type="text" value={title} onChange={e => setTitle(e.target.value)}
              placeholder="例：Demo 給站長"
              className="w-full h-10 px-3 bg-surface2 border border-border rounded-lg text-fg text-[13px] outline-none focus:border-borderStrong"/>
          </div>

          <div>
            <label className="block text-fg2 text-[12px] mb-1.5">類型</label>
            <div className="flex flex-wrap gap-1.5">
              {Object.entries(EVENT_CATEGORIES).map(([key, meta]) => (
                <button key={key}
                  onClick={() => setCategory(key)}
                  className={`chip rounded-md px-2.5 h-8 text-[12px] border inline-flex items-center gap-1.5
                    ${category === key
                      ? "bg-fg text-bg border-fg"
                      : "bg-surface2 text-fg2 border-border hover:text-fg hover:border-borderStrong"}`}>
                  <span>{meta.emoji}</span>
                  {meta.label}
                </button>
              ))}
            </div>
          </div>

          <div>
            <label className="block text-fg2 text-[12px] mb-1.5">備註（選填）</label>
            <textarea value={description} onChange={e => setDescription(e.target.value)}
              rows={2}
              placeholder="細節、地點、與會者..."
              className="w-full px-3 py-2 bg-surface2 border border-border rounded-lg text-fg text-[12.5px] outline-none focus:border-borderStrong resize-none"/>
          </div>
        </div>

        <div className="flex items-center gap-2 mt-5">
          {editing && (
            <button onClick={() => onDelete(initialData.id)}
              className="rounded-lg px-3 h-9 text-[12.5px] border border-border bg-surface text-red-400 hover:bg-red-500/10 transition-colors">
              刪除
            </button>
          )}
          <div className="flex-1" />
          <button onClick={onClose}
            className="rounded-lg px-4 h-9 text-[12.5px] border border-border bg-surface text-fg2 hover:text-fg hover:border-borderStrong transition-colors">
            取消
          </button>
          <button onClick={submit}
            disabled={!date || !title.trim()}
            className="rounded-lg px-4 h-9 text-[12.5px] bg-fg text-bg font-semibold hover:opacity-90 disabled:opacity-40 disabled:cursor-not-allowed transition-opacity">
            {editing ? "儲存" : "新增"}
          </button>
        </div>
      </div>
    </div>
  );
}

function pad2(n) { return String(n).padStart(2, "0"); }
function fmtDate(d) { return `${d.getFullYear()}-${pad2(d.getMonth()+1)}-${pad2(d.getDate())}`; }
function sameMonth(a, b) { return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth(); }

function ScheduleCalendar({ pmMode, userId, canEdit = false }) {
  const today = useMemo(() => new Date(), []);
  const [viewDate, setViewDate] = useState(() => new Date(today.getFullYear(), today.getMonth(), 1));
  const [selectedDate, setSelectedDate] = useState(() => fmtDate(today));
  const [expandedReleases, setExpandedReleases] = useState(() => new Set());

  const toggleReleaseExpand = (key) => {
    setExpandedReleases(prev => {
      const next = new Set(prev);
      if (next.has(key)) next.delete(key);
      else next.add(key);
      return next;
    });
  };

  const [customEvents, setCustomEvents] = useState([]);
  const [eventsLoaded, setEventsLoaded] = useState(false);
  const [modalOpen, setModalOpen] = useState(false);
  const [editingEvent, setEditingEvent] = useState(null);

  // Fetch from Supabase on mount
  useEffect(() => {
    fetchCustomEvents().then(rows => {
      setCustomEvents(rows);
      setEventsLoaded(true);
    });
  }, []);

  const events = useMemo(() => buildScheduleEvents(customEvents), [customEvents]);

  const [detailTab, setDetailTab] = useState("all");
  // Reset detail tab when selected date changes
  useEffect(() => { setDetailTab("all"); }, [selectedDate]);

  const openAdd = (presetDate) => {
    setEditingEvent({ date: presetDate || selectedDate || fmtDate(today) });
    setModalOpen(true);
  };
  const openEdit = (ev) => {
    setEditingEvent(ev);
    setModalOpen(true);
  };
  const handleSave = async (eventData) => {
    const { data, error } = await upsertCustomEvent(eventData, userId);
    if (error) {
      alert("儲存失敗：" + error.message);
      return;
    }
    if (data) {
      setCustomEvents(prev => {
        const exists = prev.some(e => e.id === data.id);
        return exists ? prev.map(e => e.id === data.id ? data : e) : [...prev, data];
      });
      logAudit(eventData.id ? "update_event" : "create_event", "custom_event", data.id, { title: data.title, date: data.date });
    }
    setModalOpen(false);
    setEditingEvent(null);
    setSelectedDate(eventData.date);
  };

  const handleDelete = async (id) => {
    const { error } = await deleteCustomEvent(id);
    if (error) {
      alert("刪除失敗：" + error.message);
      return;
    }
    setCustomEvents(prev => prev.filter(e => e.id !== id));
    logAudit("delete_event", "custom_event", id);
    setModalOpen(false);
    setEditingEvent(null);
  };

  const importMilestones = async () => {
    const suggestions = generateMilestonesFromCycles();
    const existingKeys = new Set(customEvents.map(e => `${e.date}|${e.title}`));
    const toAdd = suggestions.filter(s => !existingKeys.has(`${s.date}|${s.title}`));
    if (toAdd.length === 0) {
      alert("這些里程碑已經匯入過了。");
      return;
    }
    const { data, error } = await batchInsertCustomEvents(toAdd, userId);
    if (error) {
      alert("匯入失敗：" + error.message);
      return;
    }
    if (data) {
      setCustomEvents(prev => [...prev, ...data]);
      logAudit("import_milestones", "custom_event", null, { count: data.length });
    }
  };

  const year = viewDate.getFullYear();
  const month = viewDate.getMonth();
  const firstDay = new Date(year, month, 1);
  const lastDay = new Date(year, month + 1, 0);
  const startWeekday = firstDay.getDay();
  const daysInMonth = lastDay.getDate();

  // Build 6-row grid (42 cells) including leading days from prev month and trailing from next
  const cells = [];
  for (let i = startWeekday - 1; i >= 0; i--) {
    const d = new Date(year, month, -i);
    cells.push({ date: d, inMonth: false });
  }
  for (let d = 1; d <= daysInMonth; d++) {
    cells.push({ date: new Date(year, month, d), inMonth: true });
  }
  while (cells.length < 42) {
    const last = cells[cells.length - 1].date;
    cells.push({ date: new Date(last.getFullYear(), last.getMonth(), last.getDate() + 1), inMonth: false });
  }

  const todayStr = fmtDate(today);
  const monthName = `${year} 年 ${month + 1} 月`;
  const selEvents = (selectedDate && events[selectedDate]) || [];

  const goPrev = () => setViewDate(new Date(year, month - 1, 1));
  const goNext = () => setViewDate(new Date(year, month + 1, 1));
  const goToday = () => {
    setViewDate(new Date(today.getFullYear(), today.getMonth(), 1));
    setSelectedDate(todayStr);
  };

  // Stats for header
  const monthEvents = Object.entries(events).filter(([k]) => {
    const d = new Date(k + "T00:00:00");
    return d.getFullYear() === year && d.getMonth() === month;
  });
  const monthEventCount = monthEvents.reduce((s, [, arr]) => s + arr.length, 0);

  return (
    <section className="mt-5 mb-4">
      {/* Calendar header */}
      <div className="flex items-center justify-between mb-4 flex-wrap gap-2">
        <div className="flex items-center gap-2">
          <button onClick={goPrev}
            className="w-8 h-8 rounded-lg border border-border bg-surface text-fg2 hover:text-fg hover:border-borderStrong transition-colors flex items-center justify-center"
            aria-label="上個月">‹</button>
          <h3 className="text-fg text-[16px] font-semibold tracking-tight min-w-[110px] text-center">{monthName}</h3>
          <button onClick={goNext}
            className="w-8 h-8 rounded-lg border border-border bg-surface text-fg2 hover:text-fg hover:border-borderStrong transition-colors flex items-center justify-center"
            aria-label="下個月">›</button>
        </div>
        <div className="flex items-center gap-2 text-[12px]">
          <span className="text-fg3 mono">本月 {monthEventCount} 個事件</span>
          <button onClick={goToday}
            className="rounded-md border border-border bg-surface px-2.5 h-7 text-fg2 hover:text-fg hover:border-borderStrong transition-colors text-[11.5px]">
            今天
          </button>
          {canEdit && (
            <>
              <button onClick={importMilestones}
                className="rounded-md border border-border bg-surface px-2.5 h-7 text-fg2 hover:text-fg hover:border-borderStrong transition-colors text-[11.5px] inline-flex items-center gap-1"
                title="從 NEXT_RELEASES 自動產生 freeze 日 + 發版日里程碑">
                🚩 匯入發版里程碑
              </button>
              <button onClick={() => openAdd()}
                className="rounded-md bg-fg text-bg px-2.5 h-7 hover:opacity-90 transition-opacity text-[11.5px] font-semibold inline-flex items-center gap-1">
                ＋ 新增事件
              </button>
            </>
          )}
        </div>
      </div>

      {/* Legend */}
      <div className="flex items-center gap-x-4 gap-y-1 mb-3 text-[11.5px] text-fg3 flex-wrap">
        <span className="flex items-center gap-1.5"><span className="inline-block w-2 h-2 rounded-full" style={{background:"rgb(var(--vf-rgb))"}} />PredictGo</span>
        <span className="flex items-center gap-1.5"><span className="inline-block w-2 h-2 rounded-full" style={{background:"rgb(var(--pg-rgb))"}} />Pridict Gaming</span>
        <span className="flex items-center gap-1.5"><span className="inline-block w-2 h-2 rounded-full" style={{background:"rgb(var(--hotfix-rgb))"}} />Hotfix</span>
        <span className="flex items-center gap-1.5"><span className="inline-block w-2 h-2 rounded-sm border" style={{borderColor:"rgb(var(--fg3-rgb))",background:"transparent"}} />未來規劃</span>
        <span className="text-fg3">·</span>
        <span className="flex items-center gap-1.5"><span className="inline-block w-2 h-2 rotate-45" style={{background:"rgb(var(--fg2-rgb))"}} />Linear due</span>
        <span className="text-fg3">·</span>
        {Object.entries(EVENT_CATEGORIES).map(([k, meta]) => (
          <span key={k} className="flex items-center gap-1">
            <span style={{color: meta.color}}>{meta.emoji}</span>
            {meta.label}
          </span>
        ))}
      </div>

      {/* Weekday headers */}
      <div className="grid grid-cols-7 gap-1 mb-1">
        {["日","一","二","三","四","五","六"].map((d, i) => (
          <div key={d} className={`text-center text-[11px] mono py-1.5 ${i === 0 || i === 6 ? "text-fg3" : "text-fg2"}`}>{d}</div>
        ))}
      </div>

      {/* Day grid */}
      <div className="grid grid-cols-7 gap-1">
        {cells.map((cell, i) => {
          const dStr = fmtDate(cell.date);
          const isToday = dStr === todayStr;
          const isSelected = dStr === selectedDate;
          const dayEvents = events[dStr] || [];
          const dimClass = !cell.inMonth ? "opacity-30" : "";

          // Split: milestone events show as full-width title bars; everything else as dots
          const milestoneEvents = dayEvents.filter(e => e.kind === "custom" && e.category === "milestone");
          const otherEvents = dayEvents.filter(e => !(e.kind === "custom" && e.category === "milestone"));

          return (
            <button
              key={i}
              onClick={() => setSelectedDate(dStr)}
              className={`relative min-h-[92px] p-1.5 rounded-lg border text-left transition-colors flex flex-col gap-1
                ${dimClass}
                ${isSelected ? "bg-surface2 border-borderStrong" : "bg-surface border-border hover:bg-surface2"}
                ${isToday ? "ring-2 ring-vf/40" : ""}`}
            >
              <div className={`text-[12.5px] mono ${isToday ? "text-vf font-semibold" : cell.inMonth ? "text-fg" : "text-fg3"}`}>
                {cell.date.getDate()}
              </div>

              {/* Milestone title bars — visible directly on the cell */}
              {milestoneEvents.length > 0 && (
                <div className="flex flex-col gap-0.5">
                  {milestoneEvents.slice(0, 2).map((e, mi) => (
                    <div key={mi}
                      className="rounded px-1 py-0.5 text-[9.5px] font-medium leading-tight truncate"
                      style={{
                        background: "rgba(234,179,8,0.15)",
                        color: "rgb(234,179,8)",
                        border: "1px solid rgba(234,179,8,0.35)",
                      }}
                      title={e.title}
                    >
                      🚩 {e.title}
                    </div>
                  ))}
                  {milestoneEvents.length > 2 && (
                    <div className="text-[9px] text-fg3 mono">+{milestoneEvents.length - 2} 個里程碑</div>
                  )}
                </div>
              )}

              {/* Non-milestone events as compact dots */}
              {otherEvents.length > 0 && (
                <div className="flex flex-wrap gap-1 mt-auto">
                  {otherEvents.slice(0, 6).map((e, ei) => {
                    if (e.kind === "custom") {
                      const meta = EVENT_CATEGORIES[e.category] || EVENT_CATEGORIES.other;
                      return (
                        <span key={ei} className="text-[10px] leading-none" title={e.title}>
                          {meta.emoji}
                        </span>
                      );
                    }
                    if (e.kind === "linear") {
                      const c = e.product === "VF" ? "--vf-rgb" : "--pg-rgb";
                      return (
                        <span key={ei}
                          className="w-1.5 h-1.5 rotate-45"
                          style={{background: `rgb(var(${c}))`}}
                          title={`${e.id} ${e.title}`} />
                      );
                    }
                    const isHot = e.hotfix;
                    const colorVar = isHot ? "--hotfix-rgb" : (e.product === "VF" ? "--vf-rgb" : "--pg-rgb");
                    return (
                      <span key={ei}
                        className="w-1.5 h-1.5 rounded-full"
                        style={{
                          background: e.isFuture ? "transparent" : `rgb(var(${colorVar}))`,
                          boxShadow: e.isFuture ? `inset 0 0 0 1px rgb(var(${colorVar}))` : undefined,
                        }}
                        title={`${e.product} ${e.version}`} />
                    );
                  })}
                  {otherEvents.length > 6 && (
                    <span className="text-[9px] mono text-fg3">+{otherEvents.length - 6}</span>
                  )}
                </div>
              )}
            </button>
          );
        })}
      </div>

      {/* Selected date detail panel */}
      <div className="mt-5 rounded-xl border border-border bg-surface p-4">
        <div className="flex items-center justify-between mb-3 gap-2 flex-wrap">
          <div className="flex items-center gap-2">
            <span className="text-fg text-[14px] font-semibold mono">
              {selectedDate || "—"}
            </span>
            {selectedDate === todayStr && (
              <span className="tint-fix mono text-[10px] px-1.5 py-0.5 rounded font-semibold uppercase">今天</span>
            )}
          </div>
          <div className="flex items-center gap-2">
            <span className="text-fg3 text-[11.5px] mono">{selEvents.length} 個事件</span>
            {canEdit && (
              <button onClick={() => openAdd(selectedDate)}
                className="rounded-md border border-border bg-surface px-2 h-6 text-fg2 hover:text-fg hover:border-borderStrong transition-colors text-[11px]">
                ＋ 加事件
              </button>
            )}
          </div>
        </div>

        {/* Detail tabs (filter events by source) */}
        {selEvents.length > 0 && (() => {
          const counts = {
            all: selEvents.length,
            release: selEvents.filter(e => e.kind === "release" || e.kind === "upcoming").length,
            linear: selEvents.filter(e => e.kind === "linear").length,
            milestone: selEvents.filter(e => e.kind === "custom" && e.category === "milestone").length,
            custom: selEvents.filter(e => e.kind === "custom" && e.category !== "milestone").length,
          };
          const tabs = [
            { key: "all",       label: "全部" },
            { key: "milestone", label: "🚩 重要時程",  hidden: counts.milestone === 0 },
            { key: "release",   label: "發版",       hidden: counts.release === 0 },
            { key: "linear",    label: "Linear due", hidden: counts.linear === 0 },
            { key: "custom",    label: "其他",       hidden: counts.custom === 0 },
          ].filter(t => !t.hidden);
          return (
            <div className="flex items-center gap-1 mb-3 border-b border-border overflow-x-auto">
              {tabs.map(t => {
                const isActive = detailTab === t.key;
                return (
                  <button key={t.key}
                    onClick={() => setDetailTab(t.key)}
                    className={`relative flex items-center gap-1.5 px-3 py-2 text-[12px] whitespace-nowrap transition-colors
                      ${isActive ? "text-fg" : "text-fg3 hover:text-fg2"}`}>
                    <span>{t.label}</span>
                    <span className="mono text-[10.5px] text-fg3">{counts[t.key]}</span>
                    {isActive && <span className="absolute left-0 right-0 -bottom-px h-0.5 bg-fg" />}
                  </button>
                );
              })}
            </div>
          );
        })()}

        {/* Events list — filter by detailTab */}
        {(() => {
          const filtered = selEvents.filter(e => {
            if (detailTab === "all") return true;
            if (detailTab === "release") return e.kind === "release" || e.kind === "upcoming";
            if (detailTab === "linear") return e.kind === "linear";
            if (detailTab === "milestone") return e.kind === "custom" && e.category === "milestone";
            if (detailTab === "custom") return e.kind === "custom" && e.category !== "milestone";
            return true;
          });
          if (filtered.length === 0) {
            return <div className="text-fg3 text-[12.5px] py-2">{selEvents.length === 0 ? "這天沒有排定事件" : "此分類無事件"}</div>;
          }
          return (
          <ul className="flex flex-col gap-2">
            {filtered.map((e, i) => {
              if (e.kind === "custom") {
                const meta = EVENT_CATEGORIES[e.category] || EVENT_CATEGORIES.other;
                return (
                  <li key={`c-${e.id}`} className="flex items-start gap-3 py-1.5 group">
                    <span className="text-[14px] leading-tight pt-0.5 shrink-0">{meta.emoji}</span>
                    <div className="min-w-0 flex-1">
                      <div className="flex items-center gap-2 flex-wrap">
                        <span className="text-[13.5px] text-fg font-medium">{e.title}</span>
                        <span className="rounded px-1.5 py-0.5 text-[10px] font-semibold mono uppercase border border-border text-fg3">
                          {meta.label}
                        </span>
                      </div>
                      {e.description && (
                        <div className="text-fg2 text-[12px] mt-0.5 leading-snug">{e.description}</div>
                      )}
                    </div>
                    {canEdit && (
                      <button onClick={() => openEdit(e)}
                        className="opacity-0 group-hover:opacity-100 transition-opacity text-fg3 hover:text-fg text-[11px] px-2 h-6 rounded border border-border">
                        編輯
                      </button>
                    )}
                  </li>
                );
              }
              if (e.kind === "linear") {
                const tint = e.product === "VF" ? "tint-vf" : "tint-pg";
                const priorityLabel = ["", "Urgent", "High", "Medium", "Low"][e.priority] || "";
                const isOverdue = selectedDate < todayStr;
                return (
                  <li key={`l-${e.id}`} className="flex items-start gap-3 py-1.5">
                    <span className={`${tint} mono rounded px-1.5 py-0.5 text-[10.5px] font-semibold whitespace-nowrap shrink-0`}>
                      {e.id}
                    </span>
                    <div className="min-w-0 flex-1">
                      <div className="flex items-center gap-2 flex-wrap">
                        <a href={e.url} target="_blank" rel="noopener noreferrer"
                          className="text-[13px] text-fg font-medium hover:underline">
                          {e.title}
                        </a>
                        {isOverdue && <span className="rounded px-1.5 py-0.5 text-[10px] font-semibold mono uppercase" style={{background:"rgba(239,68,68,0.12)", color:"rgb(239,68,68)", border:"1px solid rgba(239,68,68,0.3)"}}>逾期</span>}
                        {e.status && <span className="tint-fix rounded px-1.5 py-0.5 text-[10px] font-semibold mono uppercase">{e.status}</span>}
                        {priorityLabel && <span className="text-fg3 mono text-[10px] uppercase">{priorityLabel}</span>}
                      </div>
                      <div className="text-fg3 text-[11.5px] mt-0.5">
                        Linear due · {e.assignee || "未指派"}
                      </div>
                    </div>
                  </li>
                );
              }
              // release / upcoming — expandable to show issues
              const tint = e.product === "VF" ? "tint-vf" : "tint-pg";
              const expandKey = `${e.product}-${e.version}`;
              const isExpanded = expandedReleases.has(expandKey);

              // For upcoming: pull issues from NEXT_RELEASES
              // For past: collect tickets from RELEASE_DATA whatsNew + bugFixes
              let issues = [];
              if (e.kind === "upcoming") {
                const cycle = (typeof window !== "undefined" && window.NEXT_RELEASES || [])
                  .find(c => c.product === e.product && c.plannedVersion === e.version);
                issues = (cycle?.issues || []).map(it => ({ ...it, kind: "live" }));
              } else if (e.kind === "release") {
                const r = (typeof window !== "undefined" && window.RELEASE_DATA || [])
                  .find(x => x.product === e.product && x.version === e.version);
                if (r) {
                  const items = [
                    ...(r.whatsNew || []).map(it => ({ ...it, section: "What's New" })),
                    ...(r.bugFixes || []).map(it => ({ ...it, section: "Bug Fixes" })),
                  ];
                  // flatten to ticket-level entries
                  items.forEach(it => {
                    (it.tickets || []).forEach(tid => {
                      issues.push({
                        id: tid,
                        title: it.name,
                        section: it.section,
                        url: `https://linear.app/${typeof window !== "undefined" ? (window.LINEAR_WORKSPACE || "singdm") : "singdm"}/issue/${tid}`,
                        kind: "historical",
                      });
                    });
                  });
                }
              }
              const hasIssues = issues.length > 0;

              return (
                <li key={`r-${i}`} className="flex flex-col py-1.5">
                  <button
                    onClick={() => hasIssues && toggleReleaseExpand(expandKey)}
                    disabled={!hasIssues}
                    className={`flex items-start gap-3 ${hasIssues ? "cursor-pointer hover:opacity-80" : "cursor-default"}`}
                  >
                    <span className={`${tint} mono rounded px-1.5 py-0.5 text-[10.5px] font-semibold whitespace-nowrap shrink-0`}>
                      {PRODUCT_SHORT[e.product]}
                    </span>
                    <div className="min-w-0 flex-1 text-left">
                      <div className="flex items-center gap-2 flex-wrap">
                        <span className="mono text-[13px] text-fg font-semibold">{e.version}</span>
                        {e.hotfix && <span className="tint-hot rounded px-1.5 py-0.5 text-[10px] font-semibold mono uppercase">Hotfix</span>}
                        {e.isFuture && <span className="rounded px-1.5 py-0.5 text-[10px] font-semibold mono uppercase border border-border text-fg3">未來規劃</span>}
                        {e.isCurrent && <span className="tint-fix rounded px-1.5 py-0.5 text-[10px] font-semibold mono uppercase">進行中</span>}
                        {hasIssues && (
                          <span className="text-fg3 mono text-[10px]">
                            {issues.length} 張票
                          </span>
                        )}
                      </div>
                      <div className="text-fg2 text-[12.5px] mt-0.5 flex items-center gap-1.5">
                        {e.title}
                        {hasIssues && (
                          <span className={`chev text-fg3 ${isExpanded ? "open" : ""}`}>
                            <Icon.Chev className="w-3 h-3" />
                          </span>
                        )}
                      </div>
                    </div>
                  </button>

                  {isExpanded && hasIssues && (
                    <ul className="mt-2 ml-0 pl-9 flex flex-col gap-1 border-t border-border pt-2 max-h-[360px] overflow-y-auto">
                      {issues.map(it => {
                        const idEl = pmMode
                          ? <a href={it.url} target="_blank" rel="noopener noreferrer"
                              className={`ticket-chip ${e.product.toLowerCase()}`}
                              onClick={ev => ev.stopPropagation()}>{it.id}</a>
                          : <span className="mono text-[10.5px] text-fg3">{it.id}</span>;

                        if (it.kind === "live") {
                          // upcoming — full Linear info
                          const statusTintCls = it.statusType === "completed" ? "tint-new"
                            : it.statusType === "started" ? "tint-fix"
                            : "tint-none";
                          return (
                            <li key={it.id} className="flex items-start gap-2 py-0.5">
                              <span className={`${statusTintCls} mono rounded px-1.5 py-0.5 text-[10px] font-semibold whitespace-nowrap shrink-0`}>{it.status}</span>
                              <div className="min-w-0 flex-1">
                                <div className="text-[12px] text-fg leading-snug">{it.title}</div>
                                <div className="flex items-center gap-2 mt-0.5 text-[10.5px] text-fg3">
                                  {idEl}
                                  {it.assignee && <span>· {it.assignee}</span>}
                                </div>
                              </div>
                            </li>
                          );
                        }
                        // historical — section + title
                        return (
                          <li key={`${it.id}-${it.section}`} className="flex items-start gap-2 py-0.5">
                            <span className="mono rounded px-1.5 py-0.5 text-[10px] font-semibold whitespace-nowrap shrink-0 border border-border text-fg3">{it.section}</span>
                            <div className="min-w-0 flex-1">
                              <div className="text-[12px] text-fg leading-snug">{it.title}</div>
                              <div className="mt-0.5 text-[10.5px]">
                                {idEl}
                              </div>
                            </div>
                          </li>
                        );
                      })}
                    </ul>
                  )}
                </li>
              );
            })}
          </ul>
          );
        })()}
      </div>

      {/* Modal for add/edit custom event */}
      {modalOpen && (
        <CustomEventModal
          initialData={editingEvent}
          onSave={handleSave}
          onClose={() => { setModalOpen(false); setEditingEvent(null); }}
          onDelete={handleDelete}
        />
      )}
    </section>
  );
}

// ---------- Upcoming Releases (Linear cycles) ----------
// Read window.NEXT_RELEASES freshly each render so it reflects post-fetch updates.
const getNextData = () => (typeof window !== "undefined" && window.NEXT_RELEASES) || [];

// Group statusType into 3 buckets for compact stats
function bucketStatus(statusType) {
  if (statusType === "completed") return "done";
  if (statusType === "started") return "wip";
  return "todo"; // unstarted, triage, backlog
}

const STATUS_BUCKET_META = {
  done: { label: "已完成", short: "✓", color: "rgb(var(--success-rgb))" },
  wip:  { label: "進行中", short: "⏵", color: "rgb(var(--vf-rgb))" },
  todo: { label: "待處理", short: "○", color: "rgb(var(--fg3-rgb))" },
};

// Detailed status name → tint class
function statusTint(statusType) {
  if (statusType === "completed") return "tint-new";
  if (statusType === "started") return "tint-fix";
  return "tint-none";
}

function daysUntil(dateStr) {
  if (!dateStr) return null;
  const target = new Date(dateStr + "T00:00:00");
  const now = new Date();
  now.setHours(0, 0, 0, 0);
  const diff = Math.round((target - now) / (1000 * 60 * 60 * 24));
  return diff;
}

function buildCycleCopyText(cycle) {
  const productName = PRODUCT_LABEL[cycle.product] || cycle.product;
  const issues = cycle.issues || [];
  const days = daysUntil(cycle.releaseDate);
  const dayHint = days == null
    ? ""
    : days < 0
      ? ` (已逾期 ${-days} 天)`
      : days === 0
        ? " (今天發版)"
        : days === 1
          ? " (明天發版)"
          : ` (${days} 天後發版)`;

  const issueMap = new Map(issues.map(i => [i.id, i]));
  const childrenOf = new Map();
  const topLevel = [];
  issues.forEach(it => {
    if (it.parentTicket && issueMap.has(it.parentTicket)) {
      if (!childrenOf.has(it.parentTicket)) childrenOf.set(it.parentTicket, []);
      childrenOf.get(it.parentTicket).push(it);
    } else {
      topLevel.push(it);
    }
  });

  const lines = [
    `📅 ${productName} ${cycle.plannedVersion} · 預計 ${cycle.releaseDate}${dayHint}`,
  ];
  if (topLevel.length === 0) {
    lines.push("", "（尚未排票）");
    return lines.join("\n");
  }
  lines.push("");
  topLevel.forEach((parent, idx) => {
    lines.push(`• ${parent.title}`);
    const kids = childrenOf.get(parent.id) || [];
    kids.forEach(k => lines.push(`  ◦ ${k.title}`));
    if (idx < topLevel.length - 1) lines.push("");
  });
  return lines.join("\n");
}

function CycleCard({ cycle, pmMode, defaultOpen, isAdmin }) {
  const [open, setOpen] = useState(!!defaultOpen);
  const [copied, setCopied] = useState(false);
  const issues = cycle.issues || [];
  const buckets = { done: 0, wip: 0, todo: 0 };
  issues.forEach(i => { buckets[bucketStatus(i.statusType)]++; });

  const days = daysUntil(cycle.releaseDate);
  const dayHint = days == null
    ? ""
    : days < 0
      ? `已逾期 ${-days} 天`
      : days === 0
        ? "今天發版"
        : days === 1
          ? "明天發版"
          : `${days} 天後發版`;

  const tint = cycle.product === "VF" ? "tint-vf" : "tint-pg";
  const empty = issues.length === 0;

  const handleCopy = async (e) => {
    e.stopPropagation();
    try {
      await navigator.clipboard.writeText(buildCycleCopyText(cycle));
      setCopied(true);
      setTimeout(() => setCopied(false), 1800);
    } catch {
      setCopied(false);
    }
  };

  return (
    <article className={`rounded-xl border bg-surface overflow-hidden transition-colors
      ${cycle.isCurrent ? "border-borderStrong" : "border-border"}`}>
      <button
        onClick={() => !empty && setOpen(o => !o)}
        disabled={empty}
        className={`w-full text-left px-4 py-3 flex items-center gap-3 ${empty ? "cursor-default" : "trace-hover cursor-pointer"} transition-colors`}
      >
        <div className="flex items-center gap-2.5 min-w-0 flex-1 flex-wrap">
          <span className={`${tint} rounded-md px-1.5 py-0.5 text-[10.5px] font-semibold mono tracking-wide`}>
            {PRODUCT_SHORT[cycle.product]}
          </span>
          <span className="mono text-[13px] text-fg font-semibold">{cycle.plannedVersion}</span>
          {cycle.isCurrent && (
            <span className="tint-fix rounded px-1.5 py-0.5 text-[10px] font-semibold mono uppercase tracking-wider">進行中</span>
          )}
        </div>

        <div className="shrink-0 flex items-center gap-2 text-[11.5px]">
          {empty ? (
            <span className="text-fg3 mono">尚未排票</span>
          ) : (
            <>
              <span className="text-fg3 mono">{issues.length} 票</span>
              {pmMode && (
                <>
                  <span className="text-fg3">·</span>
                  <span className="flex items-center gap-1.5 mono">
                    <span style={{color: STATUS_BUCKET_META.done.color}}>✓{buckets.done}</span>
                    <span style={{color: STATUS_BUCKET_META.wip.color}}>⏵{buckets.wip}</span>
                    <span style={{color: STATUS_BUCKET_META.todo.color}}>○{buckets.todo}</span>
                  </span>
                </>
              )}
            </>
          )}
        </div>

        {!empty && (
          <span className={`chev shrink-0 ${open ? "open" : ""} text-fg3`}>
            <Icon.Chev className="w-3.5 h-3.5" />
          </span>
        )}
      </button>

      {/* Date + days */}
      <div className="px-4 pb-2.5 flex items-center justify-between gap-2 text-[11.5px]">
        <span className="text-fg3 mono shrink-0">📅 預計 {cycle.releaseDate}</span>
        <div className="flex items-center gap-2">
          {dayHint && (
            <span className={`mono ${days != null && days <= 1 ? "text-hotfix" : "text-fg2"}`}>
              {dayHint}
            </span>
          )}
          {pmMode && cycle.notionUrl && (
            <a
              href={cycle.notionUrl}
              target="_blank"
              rel="noopener noreferrer"
              onClick={(e) => e.stopPropagation()}
              className="chip rounded-md px-2 h-6 text-[10.5px] border bg-surface text-fg3 border-border hover:text-fg hover:border-borderStrong inline-flex items-center gap-1"
              title="在 Notion 編輯預告內容"
            >
              📝 Notion
            </a>
          )}
          {isAdmin && !empty && (
            <button
              onClick={handleCopy}
              onMouseDown={(e) => e.stopPropagation()}
              className={`chip rounded-md px-2 h-6 text-[10.5px] border inline-flex items-center gap-1 transition-colors
                ${copied
                  ? "bg-success/10 text-success border-success/30"
                  : "bg-surface text-fg3 border-border hover:text-fg hover:border-borderStrong"}`}
              title="複製此版本內容（純文字、無票號）"
            >
              {copied ? "✓ 已複製" : "📋 複製"}
            </button>
          )}
        </div>
      </div>

      {/* Expand: issue list (parent → children, 2-level tree) */}
      {open && !empty && (() => {
        // Build parent → children map; orphans go to top-level
        const issueMap = new Map(issues.map(i => [i.id, i]));
        const childrenOf = new Map();
        const topLevel = [];
        issues.forEach(it => {
          if (it.parentTicket && issueMap.has(it.parentTicket)) {
            if (!childrenOf.has(it.parentTicket)) childrenOf.set(it.parentTicket, []);
            childrenOf.get(it.parentTicket).push(it);
          } else {
            topLevel.push(it);
          }
        });

        const IssueRow = ({ it, isChild }) => {
          const bullet = isChild ? "◦" : "•";
          if (!pmMode) {
            return (
              <div className="py-1 flex items-start gap-2">
                <span className="text-fg3 shrink-0 leading-snug select-none w-3 text-center">{bullet}</span>
                <div className="text-[12.5px] text-fg leading-snug min-w-0 flex-1">{it.title}</div>
              </div>
            );
          }
          const tt = statusTint(it.statusType);
          return (
            <div className="flex items-start gap-2 py-1">
              <span className="text-fg3 shrink-0 leading-snug select-none w-3 text-center mt-0.5">{bullet}</span>
              <span className={`${tt} mono rounded px-1.5 py-0.5 text-[10px] font-semibold whitespace-nowrap shrink-0`}>{it.status}</span>
              <div className="min-w-0 flex-1">
                <div className="text-[12.5px] text-fg leading-snug">{it.title}</div>
                <div className="flex items-center gap-2 mt-0.5 text-[11px] text-fg3">
                  <a href={it.url} target="_blank" rel="noopener noreferrer"
                     className={`ticket-chip ${cycle.product.toLowerCase()}`}
                     onClick={e=>e.stopPropagation()}>{it.id}</a>
                  {it.assignee && <span>· {it.assignee}</span>}
                </div>
              </div>
            </div>
          );
        };

        return (
          <div className="px-4 pb-4 border-t border-border pt-3 max-h-[420px] overflow-y-auto">
            <ul className="flex flex-col gap-1">
              {topLevel.map(parent => {
                const kids = childrenOf.get(parent.id) || [];
                return (
                  <li key={parent.id} className="list-none">
                    <IssueRow it={parent} isChild={false} />
                    {kids.length > 0 && (
                      <ul className="pl-5 mt-0.5 flex flex-col gap-0.5">
                        {kids.map(k => (
                          <li key={k.id} className="list-none">
                            <IssueRow it={k} isChild={true} />
                          </li>
                        ))}
                      </ul>
                    )}
                  </li>
                );
              })}
            </ul>
          </div>
        );
      })()}
    </article>
  );
}

function filterCycleIssues(cycle, q) {
  if (!q) return cycle;
  const lower = q.toLowerCase();
  const filtered = (cycle.issues || []).filter(it => {
    const hay = [it.id, it.title, it.status, it.assignee, ...(it.labels || [])]
      .filter(Boolean).join(" ").toLowerCase();
    return hay.includes(lower);
  });
  return { ...cycle, issues: filtered };
}

function UpcomingReleases({ pmMode, query, isAdmin }) {
  const [mobileTab, setMobileTab] = useState("VF"); // mobile-only product tab
  const NEXT_DATA = getNextData();
  if (!NEXT_DATA.length) return null;
  const cycles = query
    ? NEXT_DATA.map(c => filterCycleIssues(c, query))
    : NEXT_DATA;

  const vfCycles = cycles.filter(c => c.product === "VF");
  const pgCycles = cycles.filter(c => c.product === "PG");

  const totalIssues = cycles.reduce((s, c) => s + (c.issues?.length || 0), 0);
  const vfIssues = vfCycles.reduce((s, c) => s + (c.issues?.length || 0), 0);
  const pgIssues = pgCycles.reduce((s, c) => s + (c.issues?.length || 0), 0);

  const Column = ({ cycles, label, accent, hideHeaderOnMobile }) => (
    <div className="flex-1 min-w-0">
      <div className={`${hideHeaderOnMobile ? "hidden lg:flex" : "flex"} items-center gap-2 mb-3`}>
        <span className="inline-block w-1.5 h-1.5 rounded-full" style={{background: accent}} />
        <h3 className="text-fg text-[13px] font-semibold tracking-wide">{label}</h3>
      </div>
      <div className="flex flex-col gap-2.5">
        {cycles.map(c => (
          <CycleCard key={c.cycleId} cycle={c} pmMode={pmMode} defaultOpen={c.isCurrent || !!query} isAdmin={isAdmin} />
        ))}
      </div>
    </div>
  );

  return (
    <section className="mt-5 mb-4">
      {query && (
        <div className="text-fg3 text-[12.5px] mb-4">
          搜尋「<span className="text-fg">{query}</span>」共找到 <span className="mono text-fg">{totalIssues}</span> 張票
        </div>
      )}

      {/* Mobile-only product tabs */}
      <div className="lg:hidden flex items-center gap-1 mb-4 border-b border-border">
        {[
          { key: "VF", label: "PredictGo",      accent: "rgb(var(--vf-rgb))", count: vfIssues },
          { key: "PG", label: "Pridict Gaming", accent: "rgb(var(--pg-rgb))", count: pgIssues },
        ].map(t => {
          const isActive = mobileTab === t.key;
          return (
            <button key={t.key}
              onClick={() => setMobileTab(t.key)}
              className={`relative flex items-center gap-2 px-3 py-2 text-[13px] font-medium transition-colors
                ${isActive ? "text-fg" : "text-fg3 hover:text-fg2"}`}>
              <span className="inline-block w-1.5 h-1.5 rounded-full" style={{background: t.accent}} />
              <span>{t.label}</span>
              <span className={`mono text-[10.5px] ${isActive ? "text-fg2" : "text-fg3"}`}>{t.count}</span>
              {isActive && <span className="absolute left-0 right-0 -bottom-px h-0.5 bg-fg" />}
            </button>
          );
        })}
      </div>

      {/* Layout: side-by-side on desktop, mobile shows only active tab */}
      <div className="flex flex-col lg:flex-row gap-5">
        <div className={`${mobileTab === "VF" ? "block" : "hidden"} lg:block flex-1 min-w-0`}>
          <Column cycles={vfCycles} label="PredictGo (VF)" accent="rgb(var(--vf-rgb))" hideHeaderOnMobile />
        </div>
        <div className={`${mobileTab === "PG" ? "block" : "hidden"} lg:block flex-1 min-w-0`}>
          <Column cycles={pgCycles} label="Pridict Gaming (PG)" accent="rgb(var(--pg-rgb))" hideHeaderOnMobile />
        </div>
      </div>
    </section>
  );
}


function Section({ color, title, count, items, query, compact, product, pmMode }) {
  return (
    <div className={compact ? "mt-5" : ""}>
      <div className="flex items-center gap-2 mb-3">
        <Icon.Dot className="w-2 h-2" style={{ color }} />
        <h4 className="text-[13px] font-semibold text-fg tracking-wide">{title}</h4>
        <span className="mono text-[11px] text-fg3">{String(count).padStart(2, "0")}</span>
      </div>
      <ol className="space-y-2.5 pl-0">
        {items.map((it, idx) => (
          <li key={idx} className="flex gap-3">
            <span className="mono text-[11.5px] text-fg3 pt-0.5 w-5 shrink-0 text-right">
              {String(idx + 1).padStart(2, "0")}
            </span>
            <div className="min-w-0 flex-1">
              <div className="flex items-center gap-1.5 flex-wrap">
                <div className="text-[13.5px] text-fg font-medium leading-snug">
                  <Highlight text={it.name} query={query} />
                </div>
                {pmMode && (it.tickets || []).map(t => (
                  <TicketChip key={t} id={t} product={product} />
                ))}
              </div>
              <div className="text-[12.5px] text-fg2 leading-relaxed mt-0.5">
                <Highlight text={it.desc} query={query} />
              </div>
            </div>
          </li>
        ))}
      </ol>
    </div>
  );
}

// ---------- PM mode banner ----------
function PmBanner({ onClose }) {
  return (
    <div className="mb-4 rounded-xl border border-vf/30 px-4 py-2.5 flex items-center gap-3 text-[12.5px]"
         style={{background: "linear-gradient(90deg, rgba(59,130,246,0.12), rgba(249,115,22,0.08))"}}>
      <span className="mono text-vf font-semibold tracking-wide">PM MODE</span>
      <span className="text-fg2">已顯示 Linear 票號連結，外部使用者看不到此資訊</span>
      <div className="flex-1" />
      <button onClick={onClose} className="text-fg3 hover:text-fg">
        <Icon.X className="w-3.5 h-3.5" />
      </button>
    </div>
  );
}

// ─── Login Gate ───
// Two ways in:
//   1. Guest password (default visible) — type today's date (YYYY-MM-DD) → viewer-only access via Worker
//   2. Magic Link (collapsed) — Supabase email login, full role-based access

function LoginGate({ theme, onToggleTheme, onGuestSuccess }) {
  // guest password state
  const [guestPwd, setGuestPwd] = useState("");
  const [guestSubmitting, setGuestSubmitting] = useState(false);
  const [guestErr, setGuestErr] = useState(null);
  const guestInputRef = useRef(null);

  // magic link state (collapsed by default)
  const [showMagicLink, setShowMagicLink] = useState(false);
  const [email, setEmail] = useState("");
  const [sending, setSending] = useState(false);
  const [sent, setSent] = useState(false);
  const [error, setError] = useState(null);
  const emailInputRef = useRef(null);

  useEffect(() => { guestInputRef.current?.focus(); }, []);
  useEffect(() => { if (showMagicLink) emailInputRef.current?.focus(); }, [showMagicLink]);

  const submitGuest = async () => {
    const pwd = guestPwd.trim();
    if (!pwd || guestSubmitting) return;
    setGuestSubmitting(true);
    setGuestErr(null);
    try {
      const res = await fetch(`${WORKER_URL}/releases?password=${encodeURIComponent(pwd)}`);
      const data = await res.json();
      if (!data.ok) {
        setGuestErr(data.error === "Invalid password" ? "密碼錯誤" : (data.error || "登入失敗"));
        return;
      }
      sessionStorage.setItem(GUEST_PWD_KEY, pwd);
      sessionStorage.setItem(GUEST_DATA_KEY, JSON.stringify(data.releases));
      onGuestSuccess(data.releases);
    } catch (e) {
      setGuestErr(`網路錯誤：${e.message || e}`);
    } finally {
      setGuestSubmitting(false);
    }
  };

  const sendLink = async () => {
    if (!email.trim() || sending) return;
    setSending(true);
    setError(null);
    const { error } = await supabase.auth.signInWithOtp({
      email: email.trim().toLowerCase(),
      options: {
        emailRedirectTo: window.location.origin,
        shouldCreateUser: true,
      },
    });
    setSending(false);
    if (error) {
      setError(error.message || "送出失敗，請稍後再試");
    } else {
      setSent(true);
    }
  };

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-bg px-5">
      <div className="absolute top-5 right-5">
        <ThemeToggle theme={theme} onToggle={onToggleTheme} />
      </div>

      <div className="w-full max-w-[400px]">
        {/* brand mark */}
        <div className="flex flex-col items-center mb-8">
          <div className="logo-grad rounded-xl border border-border p-5 mb-5 w-[72px] h-[72px] flex items-center justify-center">
            <div className="w-9 h-9 rounded-md" style={{background: "linear-gradient(135deg, rgb(var(--vf-rgb)) 0%, rgb(var(--pg-rgb)) 100%)"}} />
          </div>
          <div className="text-[11px] mono uppercase tracking-[0.18em] text-fg3 mb-1.5">Internal Tool</div>
          <h1 className="text-fg text-[20px] font-semibold">Release Notes</h1>
          <p className="text-fg2 text-[13px] mt-1">PredictGo <span className="text-fg3">×</span> Pridict Gaming</p>
        </div>

        {/* guest password — default visible */}
        <div className="bg-surface border border-border rounded-2xl p-6">
          <label className="block text-fg text-[13.5px] font-medium mb-2.5">輸入密碼</label>
          <input
            ref={guestInputRef}
            type="password"
            value={guestPwd}
            onChange={(e) => { setGuestPwd(e.target.value); setGuestErr(null); }}
            onKeyDown={(e) => e.key === "Enter" && submitGuest()}
            placeholder="••••"
            inputMode="numeric"
            autoComplete="off"
            disabled={guestSubmitting}
            className={`w-full h-11 px-3.5 bg-surface2 border rounded-lg text-fg text-[14px] mono outline-none transition-colors
              ${guestErr ? "border-red-500/60" : "border-border focus:border-borderStrong"}`}
          />
          <div className="mt-2 h-5 text-[12px]">
            {guestErr
              ? <span className="text-red-400">{guestErr}</span>
              : <span className="text-fg3">&nbsp;</span>}
          </div>
          <button
            onClick={submitGuest}
            disabled={!guestPwd.trim() || guestSubmitting}
            className="mt-4 w-full h-11 rounded-lg bg-fg text-bg text-[14px] font-semibold hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed transition-opacity"
          >
            {guestSubmitting ? "驗證中..." : "進入"}
          </button>
        </div>

        {/* magic link — collapsed by default */}
        <div className="mt-4 bg-surface border border-border rounded-2xl overflow-hidden">
          <button
            onClick={() => setShowMagicLink(v => !v)}
            className="w-full px-6 py-3.5 flex items-center justify-between text-fg2 hover:text-fg text-[13px] transition-colors"
          >
            <span>用 Email Magic Link 登入</span>
            <span className={`chev text-fg3 ${showMagicLink ? "open" : ""}`}>▾</span>
          </button>

          {showMagicLink && (
            <div className="px-6 pb-6 pt-1 border-t border-border">
              {!sent ? (
                <>
                  <input
                    ref={emailInputRef}
                    type="email"
                    value={email}
                    onChange={(e) => { setEmail(e.target.value); setError(null); }}
                    onKeyDown={(e) => e.key === "Enter" && sendLink()}
                    placeholder="you@example.com"
                    autoComplete="email"
                    disabled={sending}
                    className={`mt-4 w-full h-11 px-3.5 bg-surface2 border rounded-lg text-fg text-[14px] outline-none transition-colors
                      ${error ? "border-red-500/60" : "border-border focus:border-borderStrong"}`}
                  />
                  <div className="mt-2 h-5 text-[12px]">
                    {error
                      ? <span className="text-red-400">{error}</span>
                      : <span className="text-fg3">寄一封登入連結到你的信箱（限團隊成員）</span>}
                  </div>
                  <button
                    onClick={sendLink}
                    disabled={!email.trim() || sending}
                    className="mt-4 w-full h-11 rounded-lg border border-borderStrong bg-surface2 text-fg text-[14px] font-medium hover:border-fg3 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
                  >
                    {sending ? "發送中..." : "寄送登入連結"}
                  </button>
                </>
              ) : (
                <div className="text-center py-4">
                  <div className="text-[36px] mb-2">📬</div>
                  <div className="text-fg text-[14px] font-semibold mb-2">登入連結已寄出</div>
                  <div className="text-fg2 text-[12.5px] mb-3">
                    請到 <span className="text-fg mono">{email}</span> 點擊登入連結
                    <br />（連結 60 分鐘內有效）
                  </div>
                  <button
                    onClick={() => { setSent(false); setEmail(""); }}
                    className="text-fg3 hover:text-fg text-[12px] underline">
                    換一個信箱
                  </button>
                </div>
              )}
            </div>
          )}
        </div>

        <div className="mt-5 text-center text-[11px] mono text-fg3">
          © Singdm Internal · {new Date().getFullYear()}
        </div>
      </div>
    </div>
  );
}

// ---------- empty state ----------
function EmptyState({ query }) {
  return (
    <div className="rounded-xl border border-dashed border-border bg-surface/40 py-16 flex flex-col items-center justify-center">
      <div className="w-12 h-12 rounded-full border border-border flex items-center justify-center text-fg3 mb-4">
        <Icon.Search className="w-5 h-5" />
      </div>
      <div className="text-fg font-medium text-[14px] mb-1">找不到符合條件的版本</div>
      <div className="text-fg3 text-[12.5px] mono">
        {query ? <>no match for "<span className="text-fg2">{query}</span>"</> : "try clearing filters"}
      </div>
    </div>
  );
}

// ---------- mobile sidebar (collapsed to filter row) ----------
function MobileFilters({ counts, filters, setFilters }) {
  const Row = ({ label, items, value, onChange }) => (
    <div>
      <div className="text-[10.5px] font-medium uppercase tracking-[0.12em] text-fg3 mb-1.5">{label}</div>
      <div className="flex gap-1.5 flex-wrap">
        {items.map(it => {
          const active = value === it.key;
          return (
            <button
              key={it.key}
              onClick={() => onChange(it.key)}
              className={`chip rounded-full px-2.5 h-7 text-[12px] border flex items-center gap-1.5
                ${active ? "bg-fg text-bg border-fg" : "bg-surface text-fg2 border-border"}`}
            >
              {it.dot && <span className="inline-block w-1.5 h-1.5 rounded-full" style={{background: it.dot}} />}
              {it.label}
              {it.count != null && <span className="mono opacity-70">{it.count}</span>}
            </button>
          );
        })}
      </div>
    </div>
  );

  return (
    <div className="lg:hidden space-y-3 mb-4">
      <Row
        label="產品"
        value={filters.product}
        onChange={v => setFilters(f => ({ ...f, product: v }))}
        items={[
          { key: "all", label: "全部", count: counts.product.all },
          { key: "VF",  label: "PredictGo", dot: "#3b82f6", count: counts.product.VF },
          { key: "PG",  label: "Pridict Gaming", dot: "#f97316", count: counts.product.PG },
        ]}
      />
      <Row
        label="年份"
        value={filters.year}
        onChange={v => setFilters(f => ({ ...f, year: v }))}
        items={[
          { key: "all", label: "全部", count: counts.year.all },
          { key: "2026", label: "2026", count: counts.year["2026"] || 0 },
          { key: "2025", label: "2025", count: counts.year["2025"] || 0 },
        ]}
      />
      <Row
        label="類型"
        value={filters.type}
        onChange={v => setFilters(f => ({ ...f, type: v }))}
        items={[
          { key: "all", label: "全部", count: counts.type.all },
          { key: "new", label: "新功能", dot: "#10b981", count: counts.type.new },
          { key: "fix", label: "修復", dot: "#3b82f6", count: counts.type.fix },
          { key: "hotfix", label: "Hotfix", dot: "#f59e0b", count: counts.type.hotfix },
        ]}
      />
    </div>
  );
}

// ---------- main app ----------
function App() {
  const data = (typeof window !== "undefined" && window.RELEASE_DATA) || [];

  const [query, setQuery] = useState("");
  const [filters, setFilters] = useState({ product: "all", year: "all", type: "all" });
  const [chip, setChip] = useState("all");
  const [sort, setSort] = useState("desc");
  const [openIds, setOpenIds] = useState(() => new Set());
  const [flashId, setFlashId] = useState(null);
  const [activeTab, setActiveTab] = useState(() => {
    return localStorage.getItem("rn-active-tab") || "history";
  });
  useEffect(() => { localStorage.setItem("rn-active-tab", activeTab); }, [activeTab]);
  // (fallback useEffect for tab restrictions is declared further below, after `isAdmin` is defined)

  // ---------- Theme ----------
  const [theme, setTheme] = useState(getInitialTheme);
  useEffect(() => {
    applyTheme(theme);
    localStorage.setItem(THEME_KEY, theme);
  }, [theme]);
  // live follow system changes only if user hasn't manually set
  useEffect(() => {
    const mq = window.matchMedia?.("(prefers-color-scheme: light)");
    if (!mq) return;
    const onChange = (e) => {
      if (!localStorage.getItem(THEME_KEY + "-manual")) {
        setTheme(e.matches ? "light" : "dark");
      }
    };
    mq.addEventListener?.("change", onChange);
    return () => mq.removeEventListener?.("change", onChange);
  }, []);
  const toggleTheme = useCallback(() => {
    setTheme(prev => {
      const next = prev === "dark" ? "light" : "dark";
      localStorage.setItem(THEME_KEY + "-manual", "1");
      return next;
    });
  }, []);

  // ---------- Auth & PM mode (Supabase) ----------
  // session: Supabase auth session (null = not logged in, or loading)
  // profile: row from public.profiles (has role, is_active, etc.)
  // authReady: have we attempted initial session check?
  // guestMode: logged in via daily date password, no Supabase session, viewer-only
  // guestReleases: release rows fetched from Worker on guest login
  const [session, setSession] = useState(null);
  const [profile, setProfile] = useState(null);
  const [authReady, setAuthReady] = useState(false);
  const [guestMode, setGuestMode] = useState(false);
  const [guestReleases, setGuestReleases] = useState(null);

  // Synthetic profile for guest — viewer role, no real id/email
  const effectiveProfile = guestMode
    ? { id: null, email: "guest", role: "viewer", is_active: true }
    : profile;

  const role = effectiveProfile?.role || null;   // 'viewer' | 'pm' | 'editor' | 'admin' | null
  const isAdmin = role === "admin";
  const pmUnlocked = role === "pm" || role === "editor" || role === "admin";  // can see Linear/Notion/Schedule
  const canEditEvents = role === "pm" || role === "editor" || role === "admin"; // can write to custom_events
  const isInactive = effectiveProfile && effectiveProfile.is_active === false;

  // PM display toggle — editor/admin can hide PM features (e.g. screen sharing)
  // The toggle is just a preference; effective pmMode requires role unlock.
  const [pmModeToggle, setPmModeToggle] = useState(() => {
    return localStorage.getItem(PM_DISPLAY_KEY) !== "0"; // default ON
  });
  const pmMode = pmUnlocked && pmModeToggle;  // effective: viewer always false even if toggle is on
  const setPmMode = setPmModeToggle;          // backward-compat shim for any old setter usage

  // schedule tab requires PM/Editor/Admin role; admin tab requires admin role
  useEffect(() => {
    if (activeTab === "schedule" && !pmUnlocked) setActiveTab("history");
    if (activeTab === "admin" && !isAdmin) setActiveTab("history");
  }, [pmUnlocked, isAdmin, activeTab]);

  // ─── Subscribe to Supabase auth changes ───
  useEffect(() => {
    let mounted = true;
    supabase.auth.getSession().then(({ data }) => {
      if (!mounted) return;
      setSession(data.session);
      // Restore guest session if today's date stored AND no Supabase session
      if (!data.session) {
        const storedPwd = sessionStorage.getItem(GUEST_PWD_KEY);
        const storedData = sessionStorage.getItem(GUEST_DATA_KEY);
        if (storedPwd && storedData && storedPwd === todayPasswordTPE()) {
          try {
            setGuestReleases(JSON.parse(storedData));
            setGuestMode(true);
          } catch {
            sessionStorage.removeItem(GUEST_PWD_KEY);
            sessionStorage.removeItem(GUEST_DATA_KEY);
          }
        } else if (storedPwd) {
          // stale (different day) — clear
          sessionStorage.removeItem(GUEST_PWD_KEY);
          sessionStorage.removeItem(GUEST_DATA_KEY);
        }
      }
      setAuthReady(true);
    });
    const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, sess) => {
      setSession(sess);
      // If a Supabase login happens, drop any guest state
      if (sess) {
        setGuestMode(false);
        setGuestReleases(null);
        sessionStorage.removeItem(GUEST_PWD_KEY);
        sessionStorage.removeItem(GUEST_DATA_KEY);
      }
    });
    return () => { mounted = false; subscription?.unsubscribe(); };
  }, []);

  const handleGuestSuccess = useCallback((releases) => {
    setGuestReleases(releases);
    setGuestMode(true);
  }, []);

  // ─── When session changes, fetch the profile row ───
  useEffect(() => {
    if (!session?.user) {
      setProfile(null);
      return;
    }
    let cancelled = false;
    supabase.from("profiles").select("*").eq("id", session.user.id).maybeSingle()
      .then(({ data, error }) => {
        if (cancelled) return;
        if (error) {
          console.error("fetch profile error:", error.message);
          return;
        }
        setProfile(data);
        if (data) {
          // Log login event (only once per session, on first profile load)
          logAudit("login", "user", data.id, { email: data.email });
        }
      });
    return () => { cancelled = true; };
  }, [session?.user?.id]);

  const handleLogout = useCallback(async () => {
    if (guestMode) {
      sessionStorage.removeItem(GUEST_PWD_KEY);
      sessionStorage.removeItem(GUEST_DATA_KEY);
      setGuestMode(false);
      setGuestReleases(null);
      return;
    }
    if (profile) logAudit("logout", "user", profile.id, { email: profile.email });
    await supabase.auth.signOut();
    setSession(null);
    setProfile(null);
  }, [profile, guestMode]);

  // ─── Fetch app data from Supabase (releases / cycles / linear due dates) ───
  // bumped after fetch to trigger components reading window.* via getNextData() etc.
  const [dataVersion, setDataVersion] = useState(0);
  useEffect(() => {
    // Guest mode: data already came from Worker — just populate window.RELEASE_DATA
    if (guestMode) {
      const rels = guestReleases || [];
      window.RELEASE_DATA = rels.map(r => ({
        id: r.id,
        product: r.product,
        version: r.version,
        date: r.date,
        title: r.title,
        hotfix: r.hotfix,
        parentTicket: r.parent_ticket,
        whatsNew: r.whats_new || [],
        bugFixes: r.bug_fixes || [],
      }));
      window.NEXT_RELEASES = [];
      window.LINEAR_DUE_DATES = [];
      setDataVersion(v => v + 1);
      return;
    }
    if (!profile || isInactive) return;
    let cancelled = false;
    Promise.all([
      supabase.from("releases").select("*").order("date", { ascending: false }),
      supabase.from("cycles").select("*").order("release_date", { ascending: true }),
      supabase.from("linear_due_dates").select("*").order("due_date", { ascending: true }),
    ]).then(([rels, cycs, dds]) => {
      if (cancelled) return;
      if (rels.data) {
        window.RELEASE_DATA = rels.data.map(r => ({
          id: r.id,
          product: r.product,
          version: r.version,
          date: r.date,
          title: r.title,
          hotfix: r.hotfix,
          parentTicket: r.parent_ticket,
          whatsNew: r.whats_new || [],
          bugFixes: r.bug_fixes || [],
        }));
      } else if (rels.error) {
        console.error("releases fetch:", rels.error.message);
      }
      if (cycs.data) {
        window.NEXT_RELEASES = cycs.data.map(c => ({
          cycleId: c.id,
          product: c.product,
          cycleNumber: c.cycle_number,
          cycleTitle: c.cycle_title,
          plannedVersion: c.planned_version,
          notionUrl: c.notion_url,
          releaseDate: c.release_date,
          isCurrent: c.is_current,
          issues: c.issues || [],
        }));
      } else if (cycs.error) {
        console.error("cycles fetch:", cycs.error.message);
      }
      if (dds.data) {
        window.LINEAR_DUE_DATES = dds.data.map(d => ({
          id: d.id,
          product: d.product,
          title: d.title,
          dueDate: d.due_date,
          status: d.status,
          statusType: d.status_type,
          priority: d.priority,
          assignee: d.assignee,
          url: d.url,
          labels: d.labels || [],
        }));
      } else if (dds.error) {
        console.error("linear_due_dates fetch:", dds.error.message);
      }
      setDataVersion(v => v + 1);
    });
    return () => { cancelled = true; };
  }, [profile?.id, isInactive, guestMode, guestReleases]);

  const togglePm = useCallback(() => {
    if (!pmUnlocked) return;
    setPmMode(prev => {
      const next = !prev;
      localStorage.setItem(PM_DISPLAY_KEY, next ? "1" : "0");
      return next;
    });
  }, [pmUnlocked]);

  const inputRef = useRef(null);
  const cardRefs = useRef({});

  // keyboard shortcut ⌘K / Ctrl+K + ⌘⇧P for PM toggle
  useEffect(() => {
    const onKey = (e) => {
      if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "p") {
        e.preventDefault();
        if (pmUnlocked) togglePm(); // 只有 PM 可以切換顯示
        return;
      }
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
        e.preventDefault();
        inputRef.current?.focus();
        inputRef.current?.select();
      } else if (e.key === "Escape" && document.activeElement === inputRef.current) {
        setQuery("");
        inputRef.current?.blur();
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [pmUnlocked, togglePm]);

  // ---------- counts (unfiltered reference) ----------
  const counts = useMemo(() => {
    const c = {
      product: { all: data.length, VF: 0, PG: 0 },
      year: { all: data.length },
      type: { all: data.length, new: 0, fix: 0, hotfix: 0 },
      lastUpdate: [...data].sort((a,b) => b.date.localeCompare(a.date))[0]?.date,
    };
    for (const r of data) {
      c.product[r.product] = (c.product[r.product] || 0) + 1;
      const y = yearOf(r.date); c.year[y] = (c.year[y] || 0) + 1;
      const tags = getTypeTags(r);
      if (tags.includes("new")) c.type.new++;
      if (tags.includes("fix")) c.type.fix++;
      if (tags.includes("hotfix")) c.type.hotfix++;
    }
    return c;
  }, [data]);

  // ---------- keyword hits (used for summary even before chip filters) ----------
  const keywordHits = useMemo(() => {
    if (!query) return [];
    return data
      .filter(r => matchesQuery(r, query))
      .sort((a,b) => b.date.localeCompare(a.date));
  }, [data, query]);

  // ---------- filtered + sorted list ----------
  const filtered = useMemo(() => {
    let list = data.filter(r => {
      if (filters.product !== "all" && r.product !== filters.product) return false;
      if (filters.year !== "all" && yearOf(r.date) !== filters.year) return false;
      const tags = getTypeTags(r);
      if (filters.type !== "all" && !tags.includes(filters.type)) return false;

      if (chip === "hotfix" && !r.hotfix) return false;
      if (chip === "new" && (r.whatsNew?.length ?? 0) === 0) return false;

      if (!matchesQuery(r, query)) return false;
      return true;
    });

    list.sort((a,b) => sort === "desc" ? b.date.localeCompare(a.date) : a.date.localeCompare(b.date));

    if (chip === "latest") {
      // keep only the most-recent per product
      const seen = new Set();
      list = list.filter(r => (seen.has(r.product) ? false : (seen.add(r.product), true)));
    }
    return list;
  }, [data, filters, chip, query, sort]);

  const toggle = useCallback((id) => {
    setOpenIds(prev => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id); else next.add(id);
      return next;
    });
  }, []);

  const jumpTo = useCallback((id) => {
    setOpenIds(prev => new Set(prev).add(id));
    setFlashId(id);
    setTimeout(() => {
      const el = cardRefs.current[id];
      if (el) {
        const y = el.getBoundingClientRect().top + window.scrollY - 80;
        window.scrollTo({ top: y, behavior: "smooth" });
      }
    }, 40);
    setTimeout(() => setFlashId(null), 1500);
  }, []);

  // auto-expand matched cards when searching
  useEffect(() => {
    if (query) {
      setOpenIds(new Set(keywordHits.map(r => r.id)));
    }
  }, [query, keywordHits]);

  // ─── Auth gate ───
  // 1. Auth not ready yet → show splash (avoids login flash)
  // 2. No session → show login form
  // 3. Logged in but profile not loaded yet → splash
  // 4. Profile is_active = false → show locked screen
  if (!authReady) {
    return <div className="fixed inset-0 flex items-center justify-center bg-bg text-fg3 text-[13px]">載入中...</div>;
  }
  if (!session && !guestMode) {
    return <LoginGate theme={theme} onToggleTheme={toggleTheme} onGuestSuccess={handleGuestSuccess} />;
  }
  if (session && !profile) {
    return <div className="fixed inset-0 flex items-center justify-center bg-bg text-fg3 text-[13px]">取得使用者資料中...</div>;
  }
  if (isInactive) {
    return (
      <div className="fixed inset-0 flex items-center justify-center bg-bg px-5">
        <div className="max-w-[400px] text-center">
          <div className="text-[48px] mb-3">🚫</div>
          <h1 className="text-fg text-[18px] font-semibold mb-2">帳號已停用</h1>
          <p className="text-fg2 text-[13.5px] mb-6">你的帳號已被停用，請聯繫管理員 ({profile?.email})</p>
          <button onClick={handleLogout}
            className="rounded-lg px-4 h-9 text-[13px] border border-border bg-surface text-fg2 hover:text-fg hover:border-borderStrong transition-colors">
            登出
          </button>
        </div>
      </div>
    );
  }

  return (
    <div className="min-h-screen flex">
      <Sidebar counts={counts} filters={filters} setFilters={setFilters} />

      <main className="flex-1 min-w-0">
        <div className="max-w-[920px] mx-auto px-5 lg:px-10 py-8 lg:py-12">
          {/* header / kicker */}
          <header className="mb-6 flex items-start justify-between gap-4">
            <div className="min-w-0">
              <div className="flex items-center gap-2 text-fg3 text-[11.5px] mono uppercase tracking-[0.18em] mb-3">
                <span>Internal</span>
                <span>·</span>
                <span>Partners</span>
                <span>·</span>
                <span className="text-fg2">Weekly Cadence</span>
              </div>
              <h1 className="text-[28px] lg:text-[32px] font-semibold tracking-tight text-fg leading-tight">
                版本發行紀錄
              </h1>
              <p className="text-fg2 text-[14px] mt-1.5 leading-relaxed">
                PredictGo 與 Pridict Gaming 的完整發版歷程 — 搜尋、篩選、追蹤每一個功能落地的版本。
              </p>
            </div>
            <div className="shrink-0 pt-1 flex items-center gap-2">
              {/* role badge */}
              {(() => {
                const meta = {
                  admin:  { label: "ADMIN",  cls: "tint-hot" },
                  editor: { label: "EDITOR", cls: "tint-vf" },
                  pm:     { label: "PM",     cls: "tint-new" },
                  viewer: { label: "VIEWER", cls: "bg-surface border border-border text-fg3" },
                }[role] || { label: "—", cls: "bg-surface border border-border text-fg3" };
                return (
                  <span className={`${meta.cls} mono text-[10.5px] font-semibold uppercase tracking-wider px-2 py-1 rounded`}
                        title={profile?.email}>
                    {meta.label}
                  </span>
                );
              })()}
              <ThemeToggle theme={theme} onToggle={toggleTheme} />
              <button
                onClick={handleLogout}
                title={`登出 ${profile?.email || ""}`.trim()}
                aria-label="登出"
                className="flex items-center justify-center w-9 h-9 rounded-lg border border-border bg-surface hover:border-borderStrong text-fg2 hover:text-fg transition-colors"
              >
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
                  <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
                  <polyline points="16 17 21 12 16 7"/>
                  <line x1="21" y1="12" x2="9" y2="12"/>
                </svg>
              </button>
            </div>
          </header>

          {/* tab bar */}
          <div className="mb-5">
            <TabBar
              active={activeTab}
              onChange={setActiveTab}
              upcomingCount={getNextData().length}
              historyCount={counts.product.all}
              pmUnlocked={pmUnlocked}
              isAdmin={isAdmin}
            />
          </div>

          {/* search bar — shared by both tabs */}
          <SearchBar query={query} setQuery={setQuery} inputRef={inputRef} />
          {activeTab === "history" && (
            <KeywordSummary query={query} hits={keywordHits} onJump={jumpTo} />
          )}

          {/* PM banner — visible on both tabs */}
          {pmMode && <div className="mt-5"><PmBanner onClose={togglePm} /></div>}

          {activeTab === "upcoming" && (
            <UpcomingReleases pmMode={pmMode} query={query} isAdmin={isAdmin} />
          )}

          {activeTab === "schedule" && pmUnlocked && (
            <ScheduleCalendar pmMode={pmMode} userId={profile?.id} canEdit={canEditEvents} />
          )}

          {activeTab === "admin" && isAdmin && (
            <AdminPanel currentUserId={profile?.id} />
          )}

          {activeTab === "history" && (
            <>
              {/* mobile filters */}
              <div className="mt-5">
                <MobileFilters counts={counts} filters={filters} setFilters={setFilters} />
              </div>

              {/* chips */}
              <div className="mt-5">
                <QuickChips
                  value={chip}
                  onChange={setChip}
                  sort={sort}
                  setSort={setSort}
                  visibleCount={filtered.length}
                />
              </div>

              {/* cards */}
              <div className="mt-5 flex flex-col gap-3">
                {filtered.length === 0 ? (
                  <EmptyState query={query} />
                ) : filtered.map(r => (
                  <VersionCard
                    key={r.id}
                    r={r}
                    query={query}
                    open={openIds.has(r.id)}
                    onToggle={() => toggle(r.id)}
                    flash={flashId === r.id}
                    cardRef={(el) => { cardRefs.current[r.id] = el; }}
                    pmMode={pmMode}
                  />
                ))}
              </div>
            </>
          )}

          {/* footer */}
          <footer className="mt-16 pt-6 hair text-[11.5px] text-fg3 mono flex flex-wrap items-center gap-x-3 gap-y-1">
            <span>last sync <span className="text-fg2">{counts.lastUpdate}</span></span>
            <span>·</span>
            <span>source: internal release tracker</span>
            <span>·</span>
            <span>press <span className="kbd" style={{padding:"2px 5px"}}>⌘K</span> to search</span>
          </footer>
        </div>
      </main>

    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
