// Announcements — 活動消息發布（前台訊息頁「活動」tab 的資料來源）

const ANN_KIND = {
  activity: { label: "活動消息", desc: "顯示在前台訊息頁「活動」分頁" },
  system:   { label: "系統通知", desc: "顯示在前台訊息頁「系統通知」（跟個人事件通知混合顯示）" },
};

const ANN_AUDIENCE = {
  all:     { label: "全站",     tone: "info" },
  artist:  { label: "創作者",   tone: "accent" },
  general: { label: "一般用戶", tone: "default" },
  users:   { label: "指定名單", tone: "warn" },
};
const ANN_STATUS = {
  published: { label: "發布中", tone: "success" },
  hidden:    { label: "已隱藏", tone: "warn" },
};

// 有效狀態：把排程時間窗一起算進去（排程是前台讀取時即時判斷、不回寫 status，
// 所以列表要自己把「未開始／已結束」算出來，才不會跟前台實際顯示對不上）。
function effectiveStatus(a) {
  if (a.status === "hidden") return { label: "已隱藏", tone: "warn" };
  const now = Date.now();
  if (a.endAt && new Date(a.endAt).getTime() <= now) return { label: "已結束（自動下架）", tone: "default" };
  if (a.startAt && new Date(a.startAt).getTime() > now) return { label: "排程中（未開始）", tone: "info" };
  return { label: "發布中", tone: "success" };
}

// ── 時區工具：後台輸入的 datetime-local 一律當作 Asia/Taipei ──────────────────
function taipeiLocalToIso(local) {
  if (!local) return "";
  // local = "YYYY-MM-DDTHH:MM" → 明確標成 +08:00 的絕對時間
  return `${local}:00+08:00`;
}
function isoToTaipeiLocal(iso) {
  if (!iso) return "";
  const d = new Date(iso);
  if (isNaN(d.getTime())) return "";
  const p = new Intl.DateTimeFormat("en-CA", {
    timeZone: "Asia/Taipei", year: "numeric", month: "2-digit", day: "2-digit",
    hour: "2-digit", minute: "2-digit", hour12: false,
  }).formatToParts(d).reduce((a, x) => { a[x.type] = x.value; return a; }, {});
  const hour = p.hour === "24" ? "00" : p.hour;
  return `${p.year}-${p.month}-${p.day}T${hour}:${p.minute}`;
}
function fmtTaipei(iso) {
  if (!iso) return "—";
  const d = new Date(iso);
  if (isNaN(d.getTime())) return "—";
  return d.toLocaleString("zh-TW", {
    timeZone: "Asia/Taipei", year: "numeric", month: "2-digit", day: "2-digit",
    hour: "2-digit", minute: "2-digit", hour12: false,
  });
}
function scheduleText(item) {
  const s = item.startAt ? fmtTaipei(item.startAt) : "即時";
  const e = item.endAt ? fmtTaipei(item.endAt) : "不限";
  return `${s} ～ ${e}`;
}

const dtInputStyle = {
  width: "100%", padding: "8px 12px", fontSize: 14,
  border: "1px solid var(--border-strong)", borderRadius: 8,
  fontFamily: "var(--font-sans)", outline: 0, background: "var(--surface)",
};

// ══ 站內排版預覽 ═══════════════════════════════════════════════════════════════
// 忠實重現前台三種載具下「活動」tab 卡片與文章內頁的排版，讓後台編輯時能先看實際樣子。
// 前台排版關鍵規則（app/globals.css / AppShell / BottomNav）：
//   • <768px（手機）：內容欄滿版、底部橫向導覽列
//   • ≥768px（iPad／桌機）：左側讓出 220px 直向側欄，內容欄 = 視窗寬 − 220
// 因此 iPad／桌機的卡片會比手機寬很多；此預覽就是要把這個差異照實呈現。
const PREVIEW_DEVICES = [
  { key: "mobile",  label: "手機",  vw: 390,  vh: 800,  chrome: "bottom" },
  { key: "ipad",    label: "iPad",  vw: 834,  vh: 1040, chrome: "side"   },
  { key: "desktop", label: "桌機",  vw: 1280, vh: 820,  chrome: "side"   },
];

const PREVIEW_DOT_BG = {
  backgroundColor: "#FBF7F0",
  backgroundImage: "radial-gradient(rgba(221, 235, 153, 0.8) 2px, transparent 2px)",
  backgroundSize: "20px 20px",
};

// 前台「活動」tab 的列表卡片（對齊 app/messages/page.tsx 的排版）
const PreviewCard = ({ data }) => {
  const isArticle = data.layout === "article";
  const cta = isArticle ? "閱讀全文" : (data.linkUrl ? (data.linkLabel || "查看詳情") : null);
  const clamp = isArticle ? { display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical", overflow: "hidden" } : {};
  return (
    <div style={{ background: "#fff", borderRadius: 16, boxShadow: "0 1px 2px rgba(0,0,0,0.06)", overflow: "hidden" }}>
      {data.coverImage && <img src={data.coverImage} alt="" style={{ display: "block", width: "100%", aspectRatio: "16 / 9", objectFit: "cover" }} onError={e => { e.target.style.display = "none"; }}/>}
      <div style={{ padding: 16 }}>
        <p style={{ margin: 0, fontWeight: 600, fontSize: 14, lineHeight: 1.4, color: data.title ? "#111827" : "#B0A79F" }}>{data.title || "（未填標題）"}</p>
        <p style={{ margin: "4px 0 0", fontSize: 14, lineHeight: 1.625, whiteSpace: "pre-wrap", color: data.content ? "#4B5563" : "#B0A79F", ...clamp }}>{data.content || "（未填內文）"}</p>
        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, marginTop: 8 }}>
          <p style={{ margin: 0, fontSize: 12, color: "#9CA3AF" }}>剛剛</p>
          {cta && <span style={{ fontSize: 12, fontWeight: 500, color: "#7C3AED", flexShrink: 0 }}>{cta} ›</span>}
        </div>
      </div>
    </div>
  );
};

// 訊息頁「活動」tab（頭部 tab 列 + 卡片）
const PreviewFeed = ({ data }) => (
  <div style={{ minHeight: "100%" }}>
    <div style={{ position: "sticky", top: 0, zIndex: 5, ...PREVIEW_DOT_BG, padding: "16px 16px 0" }}>
      <div style={{ fontSize: 20, fontWeight: 700, color: "#1C0700" }}>訊息</div>
      <div style={{ display: "flex", gap: 20, marginTop: 12, borderBottom: "1px solid rgba(28,7,0,0.08)" }}>
        {["私訊", "活動"].map(t => {
          const on = t === "活動";
          return <div key={t} style={{ paddingBottom: 8, fontSize: 14, fontWeight: on ? 600 : 400, color: on ? "#1C0700" : "#9C8A82", borderBottom: on ? "2px solid #A7664B" : "2px solid transparent" }}>{t}</div>;
        })}
      </div>
    </div>
    <div style={{ padding: 16, display: "flex", flexDirection: "column", gap: 12 }}>
      <PreviewCard data={data} />
    </div>
  </div>
);

// ── 區塊式長內文模型 ───────────────────────────────────────────────────────────
// article 形式的 body 欄位改存「區塊陣列 JSON」；每個區塊是一段可獨立排版的內容
// （段落／標題／圖片／分隔線／按鈕）。舊資料（純文字 body）向後相容：解析失敗就
// 當成單一段落區塊。前台 app/announcement/[id]/page.tsx 有一份對應的 renderer，
// 改這裡的區塊結構要同步改那邊。
const ANN_ALIGNS = [
  { v: "left", label: "靠左" },
  { v: "center", label: "置中" },
  { v: "right", label: "靠右" },
];
const ANN_IMG_WIDTHS = [
  { v: "full", label: "滿版" },
  { v: "large", label: "大" },
  { v: "medium", label: "中" },
];
const BLOCK_LABEL = { paragraph: "段落", heading: "標題", image: "圖片", divider: "分隔線", button: "按鈕" };

// 給編輯器用的穩定 key（不寫入 DB，序列化時剝掉）
let _blockKeySeq = 0;
const withBlockKey = (b) => ({ ...b, _k: ++_blockKeySeq });

function parseBlocks(raw) {
  if (!raw) return [];
  const s = String(raw).trim();
  if (s.startsWith("[")) {
    try {
      const arr = JSON.parse(s);
      if (Array.isArray(arr)) return arr.filter(b => b && typeof b === "object").map(withBlockKey);
    } catch { /* 落到下面當純文字 */ }
  }
  return [withBlockKey({ type: "paragraph", text: s, align: "left" })]; // 舊純文字 → 單一段落
}

function blockIsEmpty(b) {
  if (b.type === "paragraph" || b.type === "heading") return !String(b.text || "").trim();
  if (b.type === "image") return !b.url;
  if (b.type === "button") return !String(b.label || "").trim() || !String(b.url || "").trim();
  return false; // divider 永遠有效
}

function serializeBlocks(blocks) {
  const clean = blocks.filter(b => !blockIsEmpty(b)).map(({ _k, ...rest }) => rest);
  return JSON.stringify(clean);
}

// **粗體** 內嵌語法；換行由 white-space: pre-wrap 保留
function renderInline(text) {
  return String(text).split(/(\*\*[^*]+\*\*)/g).map((p, i) =>
    /^\*\*[^*]+\*\*$/.test(p) ? <strong key={i}>{p.slice(2, -2)}</strong> : p
  );
}

// 預覽用的區塊渲染（inline style 對齊前台 app/announcement/[id]/page.tsx，內容 padding 20px）
const PreviewBlocks = ({ blocks }) => (
  <>
    {blocks.map((b, i) => {
      const key = b._k ?? i;
      const align = b.align || "left";
      if (b.type === "heading")
        return <h2 key={key} style={{ margin: "22px 0 0", fontSize: 17, fontWeight: 700, lineHeight: 1.4, color: "#1C0700", textAlign: align }}>{renderInline(b.text)}</h2>;
      if (b.type === "paragraph")
        return <p key={key} style={{ margin: "14px 0 0", fontSize: 15, lineHeight: 1.85, whiteSpace: "pre-wrap", color: "#1C0700", textAlign: align }}>{renderInline(b.text)}</p>;
      if (b.type === "divider")
        return <hr key={key} style={{ margin: "24px 0 0", border: 0, borderTop: "1px solid rgba(28,7,0,0.12)" }} />;
      if (b.type === "button")
        return <div key={key} style={{ marginTop: 20, textAlign: "center", borderRadius: 999, padding: 12, fontSize: 14, fontWeight: 600, color: "#fff", background: "#A7664B" }}>{b.label || "按鈕"}</div>;
      if (b.type === "image") {
        if (!b.url) return null;
        const width = b.width || "large";
        const full = width === "full";
        const pct = width === "medium" ? "60%" : "100%";
        const items = align === "center" ? "center" : align === "right" ? "flex-end" : "flex-start";
        return (
          <figure key={key} style={{ margin: full ? "20px -20px 0" : "20px 0 0", display: "flex", flexDirection: "column", alignItems: full ? "stretch" : items }}>
            <img src={b.url} alt="" style={{ display: "block", width: full ? "100%" : pct, borderRadius: full ? 0 : 10 }} onError={e => { e.target.style.display = "none"; }}/>
            {b.caption && <figcaption style={{ margin: "6px 0 0", fontSize: 12, color: "#9C8A82", textAlign: "center", width: full ? "100%" : pct }}>{b.caption}</figcaption>}
          </figure>
        );
      }
      return null;
    })}
  </>
);

// 活動消息文章內頁（對齊 app/announcement/[id]/page.tsx 的排版）
const PreviewArticle = ({ data }) => (
  <div style={{ minHeight: "100%", background: "#FBF7F0" }}>
    <div style={{ position: "sticky", top: 0, zIndex: 5, background: "#FBF7F0", borderBottom: "1px solid rgba(28,7,0,0.08)", display: "flex", alignItems: "center", gap: 12, padding: 16 }}>
      <Icon name="chevron-left" size={22} style={{ color: "#1C0700" }} />
      <span style={{ fontWeight: 600, color: "#1C0700" }}>活動消息</span>
    </div>
    <article>
      {data.coverImage && <img src={data.coverImage} alt="" style={{ display: "block", width: "100%", aspectRatio: "16 / 9", objectFit: "cover" }} onError={e => { e.target.style.display = "none"; }}/>}
      <div style={{ padding: "20px 20px 40px" }}>
        <h1 style={{ margin: 0, fontSize: 20, fontWeight: 700, lineHeight: 1.375, color: data.title ? "#1C0700" : "#B0A79F" }}>{data.title || "（未填標題）"}</h1>
        <p style={{ margin: "8px 0 0", fontSize: 12, color: "#9C8A82" }}>剛剛</p>
        {data.content && <p style={{ margin: "16px 0 0", fontSize: 14, lineHeight: 1.625, whiteSpace: "pre-wrap", color: "#5A4640" }}>{data.content}</p>}
        {(data.blocks && data.blocks.length)
          ? <PreviewBlocks blocks={data.blocks} />
          : <div style={{ margin: "16px 0 0", fontSize: 15, lineHeight: 1.85, color: "#B0A79F" }}>（未填文章全文）</div>}
        {data.linkUrl && (
          <div style={{ marginTop: 28, textAlign: "center", borderRadius: 999, padding: 12, fontSize: 14, fontWeight: 600, color: "#fff", background: "#A7664B" }}>{data.linkLabel || "查看詳情"}</div>
        )}
      </div>
    </article>
  </div>
);

// 側欄（iPad／桌機 md+）與底部列（手機）——簡化但比例忠實的導覽 chrome
const PreviewSideNav = () => (
  <aside style={{ width: 220, flexShrink: 0, ...PREVIEW_DOT_BG, borderRight: "1px solid rgba(0,0,0,0.05)", padding: "20px 12px", display: "flex", flexDirection: "column", gap: 4 }}>
    <img src="/logo-color.png" alt="堆堆" style={{ height: 32, width: "auto", objectFit: "contain", alignSelf: "flex-start", margin: "0 4px 12px" }} onError={e => { e.target.style.display = "none"; }}/>
    {[["首頁", false], ["動態", false], ["訊息", true], ["我的", false]].map(([label, on]) => (
      <div key={label} style={{ display: "flex", alignItems: "center", gap: 12, padding: "12px 16px", borderRadius: 16, background: on ? "#fff" : "transparent", boxShadow: on ? "0 1px 2px rgba(0,0,0,0.06)" : "none" }}>
        <div style={{ width: 22, height: 22, borderRadius: 6, background: on ? "#94B8F7" : "#D9CFC7", flexShrink: 0 }} />
        <span style={{ fontSize: 14, fontWeight: 500, color: on ? "#94B8F7" : "#5A4640" }}>{label}</span>
      </div>
    ))}
    <div style={{ marginTop: 8, display: "flex", alignItems: "center", justifyContent: "center", gap: 8, padding: "12px 16px", borderRadius: 16, background: "#A7664B", color: "#fff", fontWeight: 600, fontSize: 14 }}>
      <Icon name="plus" size={18} /> 新增
    </div>
  </aside>
);

const PreviewBottomBar = () => (
  <div style={{ flexShrink: 0, ...PREVIEW_DOT_BG, display: "flex", alignItems: "center", justifyContent: "space-around", height: 64, borderTop: "1px solid rgba(0,0,0,0.04)" }}>
    {["首頁", "動態", null, "訊息", "我的"].map((label, i) => label === null ? (
      <div key={i} style={{ width: 56, height: 56, borderRadius: "50%", background: "#A7664B", color: "#fff", display: "flex", alignItems: "center", justifyContent: "center", transform: "translateY(-12px)", flexShrink: 0 }}><Icon name="plus" size={26} /></div>
    ) : (
      <div key={i} style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 4 }}>
        <div style={{ width: 22, height: 22, borderRadius: 6, background: label === "訊息" ? "#94B8F7" : "#D9CFC7" }} />
        <span style={{ fontSize: 10, fontWeight: 500, color: label === "訊息" ? "#94B8F7" : "#9C8A82" }}>{label}</span>
      </div>
    ))}
  </div>
);

const PreviewChrome = ({ device, children }) => {
  if (device.chrome === "side") {
    return (
      <div style={{ display: "flex", width: "100%", height: "100%", ...PREVIEW_DOT_BG }}>
        <PreviewSideNav />
        <div style={{ flex: 1, minWidth: 0, height: "100%", overflowY: "auto" }}>{children}</div>
      </div>
    );
  }
  return (
    <div style={{ display: "flex", flexDirection: "column", width: "100%", height: "100%", ...PREVIEW_DOT_BG }}>
      <div style={{ flex: 1, minHeight: 0, overflowY: "auto" }}>{children}</div>
      <PreviewBottomBar />
    </div>
  );
};

// 全螢幕預覽覆蓋層：載具切換（手機／iPad／桌機）＋（長內文時）列表卡片／文章內頁切換＋直接儲存
const AnnouncementPreview = ({ data, onClose, onSave, saving }) => {
  const isArticle = data.layout === "article";
  const [deviceKey, setDeviceKey] = useState("mobile");
  const [view, setView] = useState(isArticle ? "article" : "feed");
  const stageRef = useRef(null);
  const [stage, setStage] = useState({ w: 900, h: 600 });
  const device = PREVIEW_DEVICES.find(d => d.key === deviceKey) || PREVIEW_DEVICES[0];

  useEffect(() => {
    const el = stageRef.current;
    if (!el) return;
    const update = () => setStage({ w: el.clientWidth, h: el.clientHeight });
    update();
    window.addEventListener("resize", update);
    return () => window.removeEventListener("resize", update);
  }, []);

  useEffect(() => {
    const onKey = e => { if (e.key === "Escape") onClose(); };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [onClose]);

  const PAD = 24;
  const scale = Math.min(1, (stage.w - PAD * 2) / device.vw, (stage.h - PAD * 2) / device.vh);
  const showArticle = isArticle && view === "article";
  const content = showArticle ? <PreviewArticle data={data} /> : <PreviewFeed data={data} />;

  const segStyle = { display: "flex", background: "var(--surface-2)", borderRadius: 10, padding: 3, gap: 2 };
  const segBtn = on => ({ padding: "6px 14px", borderRadius: 8, fontSize: 13, fontWeight: on ? 600 : 500, background: on ? "var(--surface)" : "transparent", color: on ? "var(--ink)" : "var(--ink-3)", boxShadow: on ? "var(--shadow-sm)" : "none" });

  return (
    <div style={{ position: "fixed", inset: 0, zIndex: 120, background: "rgba(26,24,20,0.55)", backdropFilter: "blur(3px)", display: "flex", flexDirection: "column", animation: "overlayIn .18s ease-out" }}>
      <div style={{ flexShrink: 0, display: "flex", alignItems: "center", gap: 14, flexWrap: "wrap", padding: "12px 20px", background: "var(--surface)", borderBottom: "1px solid var(--border)" }}>
        <div style={{ fontSize: 15, fontWeight: 600 }}>站內排版預覽</div>
        <div style={segStyle}>
          {PREVIEW_DEVICES.map(d => (
            <button key={d.key} onClick={() => setDeviceKey(d.key)} style={segBtn(d.key === deviceKey)}>{d.label}</button>
          ))}
        </div>
        {isArticle && (
          <div style={segStyle}>
            {[["feed", "列表卡片"], ["article", "文章內頁"]].map(([v, label]) => (
              <button key={v} onClick={() => setView(v)} style={segBtn(v === view)}>{label}</button>
            ))}
          </div>
        )}
        <div style={{ flex: 1 }} />
        <span className="mono" style={{ fontSize: 12, color: "var(--ink-4)" }}>{device.vw}×{device.vh}・{Math.round(scale * 100)}%</span>
        <Button variant="secondary" onClick={onClose} disabled={saving}>關閉</Button>
        <Button variant="primary" icon="check" disabled={saving} onClick={onSave}>{saving ? "儲存中…" : "儲存"}</Button>
      </div>

      <div ref={stageRef} style={{ flex: 1, minHeight: 0, overflow: "hidden", display: "flex", alignItems: "center", justifyContent: "center", padding: PAD }}>
        <div style={{ width: device.vw * scale, height: device.vh * scale, flexShrink: 0 }}>
          <div style={{ width: device.vw, height: device.vh, transform: `scale(${scale})`, transformOrigin: "top left", borderRadius: 18, overflow: "hidden", boxShadow: "0 12px 48px rgba(0,0,0,0.4)", border: "1px solid rgba(255,255,255,0.15)" }}>
            <PreviewChrome device={device} key={device.key + (showArticle ? "-a" : "-f")}>{content}</PreviewChrome>
          </div>
        </div>
      </div>
    </div>
  );
};

// ── 區塊編輯器（article 形式的文章全文）───────────────────────────────────────
const SegControl = ({ value, options, onChange }) => (
  <div style={{ display: "inline-flex", background: "var(--surface-2)", borderRadius: 8, padding: 2, gap: 2 }}>
    {options.map(o => {
      const on = o.v === value;
      return (
        <button key={o.v} onClick={() => onChange(o.v)} style={{
          padding: "5px 12px", borderRadius: 6, fontSize: 12.5, fontWeight: on ? 600 : 500,
          background: on ? "var(--surface)" : "transparent", color: on ? "var(--ink)" : "var(--ink-3)",
          boxShadow: on ? "var(--shadow-sm)" : "none",
        }}>{o.label}</button>
      );
    })}
  </div>
);

const BlockIconBtn = ({ name, onClick, disabled, danger }) => (
  <button onClick={onClick} disabled={disabled} style={{
    width: 28, height: 28, borderRadius: 7, display: "flex", alignItems: "center", justifyContent: "center",
    color: disabled ? "var(--ink-4)" : danger ? "var(--danger)" : "var(--ink-3)",
    opacity: disabled ? 0.45 : 1, cursor: disabled ? "default" : "pointer",
  }}><Icon name={name} size={15} /></button>
);

const BLOCK_ADD = [
  { type: "paragraph", label: "段落",  icon: "scroll",   make: () => ({ type: "paragraph", text: "", align: "left" }) },
  { type: "heading",   label: "標題",  icon: "sparkle",  make: () => ({ type: "heading", text: "", align: "left" }) },
  { type: "image",     label: "圖片",  icon: "image",    make: () => ({ type: "image", url: "", caption: "", width: "large", align: "center" }) },
  { type: "divider",   label: "分隔線", icon: "minus",    make: () => ({ type: "divider" }) },
  { type: "button",    label: "按鈕",  icon: "external", make: () => ({ type: "button", label: "", url: "" }) },
];
const blockAddBtnStyle = {
  display: "inline-flex", alignItems: "center", gap: 6, padding: "8px 12px",
  borderRadius: 8, border: "1px dashed var(--border-strong)", background: "var(--surface)",
  fontSize: 13, fontWeight: 500, color: "var(--ink-2)",
};

const BlockEditor = ({ blocks, onChange }) => {
  const toast = useToast();
  const [uploadingKey, setUploadingKey] = useState(null);
  const fileRefs = useRef({});

  const update = (k, patch) => onChange(blocks.map(b => (b._k === k ? { ...b, ...patch } : b)));
  const remove = (k) => onChange(blocks.filter(b => b._k !== k));
  const move = (k, dir) => {
    const i = blocks.findIndex(b => b._k === k);
    const j = i + dir;
    if (i < 0 || j < 0 || j >= blocks.length) return;
    const next = blocks.slice();
    [next[i], next[j]] = [next[j], next[i]];
    onChange(next);
  };
  const add = (make) => onChange([...blocks, withBlockKey(make())]);

  const uploadImage = async (k, e) => {
    const file = e.target.files?.[0];
    if (!file) return;
    e.target.value = "";
    setUploadingKey(k);
    try {
      const fd = new FormData();
      fd.append("file", file);
      const res = await fetch("/api/admin/announcements/upload", { method: "POST", body: fd });
      const data = await res.json();
      if (res.ok && data.url) update(k, { url: data.url });
      else toast({ title: "上傳失敗", message: data.error || "請重試", icon: "x", tone: "danger" });
    } catch { toast({ title: "上傳失敗", message: "網路錯誤", icon: "x", tone: "danger" }); }
    setUploadingKey(null);
  };

  return (
    <div>
      {blocks.length === 0 && (
        <div style={{ padding: 20, textAlign: "center", color: "var(--ink-4)", fontSize: 12.5, border: "1px dashed var(--border-strong)", borderRadius: 10 }}>
          還沒有內容區塊，從下方新增段落、圖片等開始排版。
        </div>
      )}
      <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
        {blocks.map((b, i) => (
          <div key={b._k} style={{ border: "1px solid var(--border)", borderRadius: 10, background: "var(--surface)" }}>
            <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "7px 8px 7px 12px", borderBottom: "1px solid var(--border)" }}>
              <span style={{ fontSize: 12, fontWeight: 600, color: "var(--ink-2)" }}>{BLOCK_LABEL[b.type] || b.type}</span>
              <div style={{ flex: 1 }} />
              <BlockIconBtn name="arrow-up" disabled={i === 0} onClick={() => move(b._k, -1)} />
              <BlockIconBtn name="arrow-down" disabled={i === blocks.length - 1} onClick={() => move(b._k, +1)} />
              <BlockIconBtn name="trash" danger onClick={() => remove(b._k)} />
            </div>
            <div style={{ padding: 10 }}>
              {(b.type === "paragraph" || b.type === "heading") && (
                <>
                  <textarea value={b.text || ""} onChange={e => update(b._k, { text: e.target.value })} rows={b.type === "heading" ? 1 : 3}
                    placeholder={b.type === "heading" ? "小標題…" : "段落內容…（用 **兩個星號** 包住文字＝粗體）"}
                    style={{ ...dtInputStyle, resize: "vertical", lineHeight: 1.7, fontWeight: b.type === "heading" ? 700 : 400 }}/>
                  <div style={{ marginTop: 8 }}>
                    <SegControl value={b.align || "left"} options={ANN_ALIGNS} onChange={v => update(b._k, { align: v })} />
                  </div>
                </>
              )}
              {b.type === "image" && (
                <>
                  <input ref={el => { fileRefs.current[b._k] = el; }} type="file" accept="image/*" style={{ display: "none" }} onChange={e => uploadImage(b._k, e)} />
                  {b.url ? (
                    <div style={{ position: "relative", borderRadius: 8, overflow: "hidden", background: "#000" }}>
                      <img src={b.url} style={{ display: "block", width: "100%", maxHeight: 220, objectFit: "contain" }}/>
                      <button onClick={() => update(b._k, { url: "" })} style={{
                        position: "absolute", top: 6, right: 6, width: 24, height: 24, borderRadius: "50%",
                        background: "rgba(0,0,0,0.6)", color: "#fff", display: "flex", alignItems: "center", justifyContent: "center",
                      }}><Icon name="x" size={13}/></button>
                    </div>
                  ) : (
                    <Button variant="secondary" icon="image" disabled={uploadingKey === b._k} onClick={() => fileRefs.current[b._k]?.click()}>
                      {uploadingKey === b._k ? "上傳中…" : "上傳圖片"}
                    </Button>
                  )}
                  <input value={b.caption || ""} onChange={e => update(b._k, { caption: e.target.value })} placeholder="圖說（選填）"
                    style={{ ...dtInputStyle, marginTop: 8, fontSize: 13 }}/>
                  <div style={{ display: "flex", gap: 10, marginTop: 8, flexWrap: "wrap", alignItems: "center" }}>
                    <SegControl value={b.width || "large"} options={ANN_IMG_WIDTHS} onChange={v => update(b._k, { width: v })} />
                    {(b.width || "large") !== "full" && (
                      <SegControl value={b.align || "center"} options={ANN_ALIGNS} onChange={v => update(b._k, { align: v })} />
                    )}
                  </div>
                </>
              )}
              {b.type === "button" && (
                <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
                  <input value={b.label || ""} onChange={e => update(b._k, { label: e.target.value })} placeholder="按鈕文字" style={dtInputStyle}/>
                  <input value={b.url || ""} onChange={e => update(b._k, { url: e.target.value })} placeholder="https://… 或 /event/xxx" style={dtInputStyle}/>
                </div>
              )}
              {b.type === "divider" && <div style={{ borderTop: "1px solid var(--border-strong)" }} />}
            </div>
          </div>
        ))}
      </div>
      <div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginTop: 12 }}>
        {BLOCK_ADD.map(x => (
          <button key={x.type} onClick={() => add(x.make)} style={blockAddBtnStyle}>
            <Icon name={x.icon} size={15} /> {x.label}
          </button>
        ))}
      </div>
    </div>
  );
};

// ── 編輯抽屜（新增 / 編輯共用）────────────────────────────────────────────────
const AnnouncementEditor = ({ item, onClose, onSaved, onDeleted }) => {
  const toast = useToast();
  const isNew = !item?.id;
  const [kind, setKind] = useState(item?.kind || "activity");
  const [title, setTitle] = useState(item?.title || "");
  const [layout, setLayout] = useState(item?.layout || "card");
  const [content, setContent] = useState(item?.content || "");
  const [blocks, setBlocks] = useState(() => parseBlocks(item?.body));
  const [coverImage, setCoverImage] = useState(item?.coverImage || "");
  const [linkUrl, setLinkUrl] = useState(item?.linkUrl || "");
  const [linkLabel, setLinkLabel] = useState(item?.linkLabel || "");
  const [audience, setAudience] = useState(item?.audience || "all");
  const [targetEmails, setTargetEmails] = useState("");
  const [status, setStatus] = useState(item?.status || "published");
  const [startLocal, setStartLocal] = useState(isoToTaipeiLocal(item?.startAt));
  const [endLocal, setEndLocal] = useState(isoToTaipeiLocal(item?.endAt));
  const [uploading, setUploading] = useState(false);
  const [saving, setSaving] = useState(false);
  const [confirmDelete, setConfirmDelete] = useState(false);
  const [previewOpen, setPreviewOpen] = useState(false);
  const fileRef = useRef(null);

  // 餵給預覽的即時資料快照（跟著編輯欄位走）
  const previewData = {
    title, layout, content,
    blocks: layout === "article" ? blocks.filter(b => !blockIsEmpty(b)) : [],
    coverImage, linkUrl, linkLabel,
  };

  const uploadCover = async (e) => {
    const file = e.target.files?.[0];
    if (!file) return;
    e.target.value = "";
    setUploading(true);
    try {
      const fd = new FormData();
      fd.append("file", file);
      const res = await fetch("/api/admin/announcements/upload", { method: "POST", body: fd });
      const data = await res.json();
      if (res.ok && data.url) setCoverImage(data.url);
      else toast({ title: "上傳失敗", message: data.error || "請重試", icon: "x", tone: "danger" });
    } catch { toast({ title: "上傳失敗", message: "網路錯誤", icon: "x", tone: "danger" }); }
    setUploading(false);
  };

  const save = async () => {
    if (!title.trim()) { toast({ title: "請填寫標題", message: "", icon: "alert", tone: "warn" }); return; }
    if (!content.trim()) { toast({ title: layout === "article" ? "請填寫卡片摘要" : "請填寫內文", message: "", icon: "alert", tone: "warn" }); return; }
    if (layout === "article" && !blocks.some(b => !blockIsEmpty(b))) { toast({ title: "請填寫文章全文", message: "長內文形式至少要有一個內容區塊", icon: "alert", tone: "warn" }); return; }
    setSaving(true);
    const body = {
      kind,
      title: title.trim(), content: content.trim(),
      layout, body: layout === "article" ? serializeBlocks(blocks) : null,
      coverImage: coverImage || null,
      linkUrl: linkUrl.trim() || null, linkLabel: linkLabel.trim() || null,
      audience, status,
      startAt: taipeiLocalToIso(startLocal) || null,
      endAt: taipeiLocalToIso(endLocal) || null,
    };
    if (audience === "users") {
      body.targetEmails = targetEmails.split(/[\n,;]+/).map(s => s.trim()).filter(Boolean);
    }
    try {
      const res = await fetch(
        isNew ? "/api/admin/announcements" : `/api/admin/announcements/${item.id}`,
        { method: isNew ? "POST" : "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }
      );
      const data = await res.json();
      if (!res.ok) {
        const miss = (data.missing || []).length ? `：找不到 ${data.missing.join("、")}` : "";
        toast({ title: "儲存失敗", message: (data.error || "請重試") + miss, icon: "x", tone: "danger" });
        setSaving(false);
        return;
      }
      const miss = (data._missingEmails || []).length;
      const kindLabel = ANN_KIND[kind]?.label || "活動消息";
      toast({
        title: isNew ? `已發布${kindLabel}` : "已更新",
        message: miss ? `注意：${miss} 個 email 找不到對應用戶，已略過` : "前台立即生效",
        icon: "check",
      });
      onSaved?.();
      onClose();
    } catch {
      toast({ title: "儲存失敗", message: "網路錯誤", icon: "x", tone: "danger" });
      setSaving(false);
    }
  };

  const doDelete = async () => {
    setSaving(true);
    try {
      const res = await fetch(`/api/admin/announcements/${item.id}`, { method: "DELETE" });
      if (!res.ok) throw new Error(await res.text());
      toast({ title: "已刪除", message: "活動消息已移除", icon: "check" });
      onDeleted?.(item.id);
      onClose();
    } catch (err) {
      toast({ title: "刪除失敗", message: String(err), icon: "x", tone: "danger" });
      setSaving(false);
    }
  };

  return (
    <Drawer open={true} onClose={onClose} width={560}>
      <div style={{ padding: "18px 24px", borderBottom: "1px solid var(--border)", display: "flex", alignItems: "center", gap: 12 }}>
        <button onClick={onClose} style={{ padding: 4, color: "var(--ink-3)" }}><Icon name="x" size={18}/></button>
        <div style={{ fontSize: 15, fontWeight: 600 }}>{isNew ? `新增${ANN_KIND[kind]?.label || "活動消息"}` : `編輯${ANN_KIND[kind]?.label || "活動消息"}`}</div>
      </div>

      <div style={{ flex: 1, overflow: "auto", padding: "22px 24px 120px", display: "flex", flexDirection: "column", gap: 16 }}>
        <div>
          <label style={labelStyle}>發布類型</label>
          <div style={{ display: "flex", gap: 8 }}>
            {Object.entries(ANN_KIND).map(([v, opt]) => {
              const active = kind === v;
              return (
                <button key={v} onClick={() => setKind(v)} style={{
                  flex: 1, textAlign: "left", padding: "10px 12px", borderRadius: 10,
                  border: "1.5px solid " + (active ? "var(--ink)" : "var(--border-strong)"),
                  background: active ? "var(--surface-2)" : "var(--surface)",
                }}>
                  <div style={{ fontSize: 13.5, fontWeight: 600, color: "var(--ink)" }}>{opt.label}</div>
                  <div style={{ fontSize: 11.5, color: "var(--ink-3)", marginTop: 2 }}>{opt.desc}</div>
                </button>
              );
            })}
          </div>
        </div>

        <div>
          <label style={labelStyle}>標題 <span style={{ color: "var(--danger)" }}>*</span></label>
          <Input value={title} onChange={e => setTitle(e.target.value)} placeholder="例如：週年慶活動開跑"/>
        </div>

        <div>
          <label style={labelStyle}>形式</label>
          <div style={{ display: "flex", gap: 8 }}>
            {[
              { v: "card", label: "卡片", desc: "短內容直接顯示" },
              { v: "article", label: "長內文", desc: "點卡片看整篇文章" },
            ].map(opt => {
              const active = layout === opt.v;
              return (
                <button key={opt.v} onClick={() => setLayout(opt.v)} style={{
                  flex: 1, textAlign: "left", padding: "10px 12px", borderRadius: 10,
                  border: "1.5px solid " + (active ? "var(--ink)" : "var(--border-strong)"),
                  background: active ? "var(--surface-2)" : "var(--surface)",
                }}>
                  <div style={{ fontSize: 13.5, fontWeight: 600, color: "var(--ink)" }}>{opt.label}</div>
                  <div style={{ fontSize: 11.5, color: "var(--ink-3)", marginTop: 2 }}>{opt.desc}</div>
                </button>
              );
            })}
          </div>
        </div>

        <div>
          <label style={labelStyle}>{layout === "article" ? "卡片摘要（列表短描述）" : "內文"} <span style={{ color: "var(--danger)" }}>*</span></label>
          <textarea value={content} onChange={e => setContent(e.target.value)} rows={layout === "article" ? 2 : 4}
            placeholder={layout === "article" ? "顯示在活動列表卡片上的一兩句摘要…" : "活動說明…"}
            style={{ ...dtInputStyle, resize: "vertical", lineHeight: 1.6 }}/>
        </div>

        {layout === "article" && (
          <div>
            <label style={labelStyle}>文章全文（點卡片後顯示）<span style={{ color: "var(--danger)" }}>*</span></label>
            <div style={{ fontSize: 11.5, color: "var(--ink-3)", margin: "-2px 0 8px" }}>
              用區塊自由排版：加入段落、標題、圖片、分隔線或按鈕，可上下移動調整順序。上方「預覽」可看實際樣子。
            </div>
            <BlockEditor blocks={blocks} onChange={setBlocks} />
          </div>
        )}

        <div>
          <label style={labelStyle}>封面圖片</label>
          <input ref={fileRef} type="file" accept="image/*" style={{ display: "none" }} onChange={uploadCover}/>
          {coverImage ? (
            <div style={{ position: "relative", borderRadius: 10, overflow: "hidden", aspectRatio: "16/9", background: "#000" }}>
              <img src={coverImage} style={{ width: "100%", height: "100%", objectFit: "cover" }}/>
              <button onClick={() => setCoverImage("")} style={{
                position: "absolute", top: 8, right: 8, width: 26, height: 26, borderRadius: "50%",
                background: "rgba(0,0,0,0.6)", color: "#fff", display: "flex", alignItems: "center", justifyContent: "center",
              }}><Icon name="x" size={14}/></button>
            </div>
          ) : (
            <Button variant="secondary" icon="image" disabled={uploading} onClick={() => fileRef.current?.click()}>
              {uploading ? "上傳中…" : "上傳封面圖"}
            </Button>
          )}
        </div>

        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
          <div>
            <label style={labelStyle}>連結網址（選填）</label>
            <Input value={linkUrl} onChange={e => setLinkUrl(e.target.value)} placeholder="https://… 或 /event/xxx"/>
          </div>
          <div>
            <label style={labelStyle}>按鈕文字</label>
            <Input value={linkLabel} onChange={e => setLinkLabel(e.target.value)} placeholder="查看詳情"/>
          </div>
        </div>

        <div>
          <label style={labelStyle}>發送對象</label>
          <Select value={audience} onChange={setAudience} options={[
            { value: "all", label: "全站（所有使用者）" },
            { value: "artist", label: "創作者（有創作者檔案）" },
            { value: "general", label: "一般用戶（非創作者）" },
            { value: "users", label: "指定名單（依 email）" },
          ]} style={{ width: "100%" }}/>
        </div>

        {audience === "users" && (
          <div>
            <label style={labelStyle}>指定 email 名單 <span style={{ color: "var(--ink-3)", fontWeight: 400 }}>（用逗號或換行分隔）</span></label>
            <textarea value={targetEmails} onChange={e => setTargetEmails(e.target.value)} rows={3}
              placeholder={"a@example.com\nb@example.com"} style={{ ...dtInputStyle, resize: "vertical", fontFamily: "var(--font-mono)", fontSize: 13 }}/>
            {!isNew && item.audience === "users" && (
              <div style={{ fontSize: 12, color: "var(--ink-3)", marginTop: 6 }}>
                目前已鎖定 {(item.targetUserIds || []).length} 位；留空重填才會覆寫名單。
              </div>
            )}
          </div>
        )}

        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
          <div>
            <label style={labelStyle}>開始時間（台灣）</label>
            <input type="datetime-local" value={startLocal} onChange={e => setStartLocal(e.target.value)} style={dtInputStyle}/>
            <div style={{ fontSize: 11.5, color: "var(--ink-4)", marginTop: 4 }}>留空＝立即生效</div>
          </div>
          <div>
            <label style={labelStyle}>結束時間（台灣）</label>
            <input type="datetime-local" value={endLocal} onChange={e => setEndLocal(e.target.value)} style={dtInputStyle}/>
            <div style={{ fontSize: 11.5, color: "var(--ink-4)", marginTop: 4 }}>留空＝不自動下架</div>
          </div>
        </div>

        <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
          <Switch checked={status === "published"} onChange={v => setStatus(v ? "published" : "hidden")}/>
          <span style={{ fontSize: 13.5 }}>{status === "published" ? "發布中（前台可見）" : "已隱藏（前台不顯示）"}</span>
        </div>
        {status === "published" && (() => {
          // 此開關只是「手動發布/隱藏」；實際是否顯示還要看排程時間窗，這裡即時提示。
          const eff = (() => {
            const now = Date.now();
            const s = taipeiLocalToIso(startLocal), e = taipeiLocalToIso(endLocal);
            if (e && new Date(e).getTime() <= now) return "⚠ 目前已超過結束時間，前台不會顯示（已自動下架）";
            if (s && new Date(s).getTime() > now) return "⚠ 尚未到開始時間，前台目前不會顯示（排程中）";
            return "";
          })();
          return eff ? <div style={{ fontSize: 12, color: "var(--warn)", marginTop: -6 }}>{eff}</div> : null;
        })()}
      </div>

      {/* Footer */}
      <div style={{ position: "absolute", left: 0, right: 0, bottom: 0, padding: "14px 24px 16px", borderTop: "1px solid var(--border)", background: "var(--surface)", display: "flex", gap: 8, alignItems: "center" }}>
        {!isNew && (
          confirmDelete ? (
            <Button variant="danger" icon="trash" disabled={saving} onClick={doDelete}>{saving ? "處理中…" : "確認刪除？"}</Button>
          ) : (
            <Button variant="secondary" icon="trash" style={{ color: "var(--danger)" }} disabled={saving} onClick={() => setConfirmDelete(true)}>刪除</Button>
          )
        )}
        <div style={{ flex: 1 }}/>
        <Button variant="secondary" icon="eye" onClick={() => setPreviewOpen(true)} disabled={saving || uploading}>預覽</Button>
        <Button variant="secondary" onClick={onClose} disabled={saving}>取消</Button>
        <Button variant="primary" icon="check" disabled={saving || uploading} onClick={save}>
          {saving ? "儲存中…" : isNew ? "發布" : "儲存變更"}
        </Button>
      </div>

      {previewOpen && (
        <AnnouncementPreview
          data={previewData}
          saving={saving}
          onClose={() => setPreviewOpen(false)}
          onSave={async () => { await save(); }}
        />
      )}
    </Drawer>
  );
};

// ── 主頁 ──────────────────────────────────────────────────────────────────────
const AnnouncementsPage = () => {
  const [kindTab, setKindTab] = useState("activity");
  const [items, setItems] = useState([]);
  const [loading, setLoading] = useState(true);
  const [editing, setEditing] = useState(null); // null | {} (new) | item

  const load = (kind = kindTab) => {
    setLoading(true);
    fetch(`/api/admin/announcements?kind=${kind}`)
      .then(r => r.ok ? r.json() : [])
      .then(d => { if (Array.isArray(d)) setItems(d); })
      .catch(() => {})
      .finally(() => setLoading(false));
  };
  useEffect(() => { load(kindTab); }, [kindTab]);

  const handleDeleted = (id) => setItems(prev => prev.filter(x => x.id !== id));
  const kindLabel = ANN_KIND[kindTab]?.label || "活動消息";

  return (
    <div className="fade-in">
      <PageHeader
        title="活動消息／系統通知"
        subtitle={ANN_KIND[kindTab]?.desc || ""}
        actions={<>
          <Button variant="secondary" icon="refresh" onClick={() => load()}>重新整理</Button>
          <Button variant="accent" icon="plus" onClick={() => setEditing({ kind: kindTab })}>新增{kindLabel}</Button>
        </>}
      />

      <div style={{ marginBottom: 16 }}>
        <Tabs active={kindTab} onChange={setKindTab} tabs={[
          { value: "activity", label: "活動消息" },
          { value: "system", label: "系統通知" },
        ]}/>
      </div>

      {loading ? (
        <div style={{ padding: 40, textAlign: "center", color: "var(--ink-3)" }}>載入中…</div>
      ) : items.length === 0 ? (
        <div style={{ padding: 60, textAlign: "center", color: "var(--ink-4)", fontSize: 13 }}>
          尚未發布任何{kindLabel}，點右上角「新增{kindLabel}」開始。
        </div>
      ) : (
        <Table
          onRowClick={setEditing}
          columns={[
            { title: "標題", render: a => (
              <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
                {a.coverImage
                  ? <img src={a.coverImage} style={{ width: 44, height: 30, borderRadius: 6, objectFit: "cover", flexShrink: 0 }} onError={e => { e.target.style.display = "none"; }}/>
                  : <div style={{ width: 44, height: 30, borderRadius: 6, flexShrink: 0, background: "var(--surface-2)", display: "flex", alignItems: "center", justifyContent: "center" }}><Icon name="bell" size={14} style={{ color: "var(--ink-4)" }}/></div>}
                <div style={{ minWidth: 0 }}>
                  <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
                    <span style={{ fontSize: 13.5, fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", maxWidth: 220 }}>{a.title}</span>
                    {a.layout === "article" && <Badge tone="info" size="sm">長內文</Badge>}
                  </div>
                  <div style={{ fontSize: 11.5, color: "var(--ink-3)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", maxWidth: 260, marginTop: 2 }}>{a.content}</div>
                </div>
              </div>
            )},
            { title: "對象", render: a => {
              const m = ANN_AUDIENCE[a.audience] || ANN_AUDIENCE.all;
              return <Badge tone={m.tone} size="sm">{m.label}{a.audience === "users" ? `（${(a.targetUserIds || []).length}）` : ""}</Badge>;
            }},
            { title: "排程（台灣時間）", render: a => <span style={{ fontSize: 12, color: "var(--ink-3)" }}>{scheduleText(a)}</span> },
            { title: "狀態", render: a => {
              const m = effectiveStatus(a);
              return <Badge tone={m.tone} size="sm" dot>{m.label}</Badge>;
            }},
            { title: "建立時間", render: a => <span className="mono" style={{ fontSize: 12, color: "var(--ink-3)" }}>{fmtTaipei(a.createdAt)}</span> },
          ]}
          rows={items}
        />
      )}

      {editing && (
        <AnnouncementEditor
          item={editing}
          onClose={() => setEditing(null)}
          onSaved={load}
          onDeleted={handleDeleted}
        />
      )}
    </div>
  );
};

window.AnnouncementsPage = AnnouncementsPage;
