'use client';

// Mira 河道 — 社群貼文式設計（FB / X 風格）
// SDD: polaris/mira/.agents/specs/mira-app/design.md §5.3

import { useEffect, useState, useCallback, useRef, useMemo } from 'react';
import { MarkdownView } from '../_shared/markdown';
import '../mira.css';

const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? 'https://cypher.arcrun.dev';
const KBDB_BASE = 'https://kbdb.finally.click';
const KBDB_INGEST = 'https://kbdb-ingest.arcrun.dev';
const KBDB_PATCH = 'https://kbdb-patch-block.arcrun.dev';
const KBDB_CREATE = 'https://kbdb-create-block.arcrun.dev';
const CLAUDE_API = 'https://claude-api.arcrun.dev';

const PREVIEW_LINES = 3;            // 卡片預覽顯示行數
const PREVIEW_CHARS = 200;          // 卡片預覽顯示字數上限

type Document = {
  page_name: string;
  user_id: string;
  block_count: number;
  created_at: number | string;
  updated_at: number | string;
};

type KBDBBlock = {
  id: string;
  content: string | null;
  type: string;
  source: string | null;
  user_id: string | null;
  parent_id: string | null;
  page_name: string | null;
  tags_json: string;
  refs_json: string;
  sort_order?: number;
  created_at: number | string;
  updated_at: number | string;
};

type Me = { email: string; api_key: string; display_name: string; avatar_url?: string };

const MIRA_POST_USER_ID = 'inkstone_mira_post';

export default function MiraPage() {
  const [me, setMe] = useState<Me | null>(null);
  const [docs, setDocs] = useState<Document[] | null>(null);
  const [error, setError] = useState<string | null>(null);
  // page_name → "thinking" / null：哪些 doc 在等 AI 回覆
  const [aiThinking, setAiThinking] = useState<Set<string>>(new Set());

  const meRef = useRef<Me | null>(null);
  meRef.current = me;

  const reload = useCallback(async () => {
    const m = meRef.current;
    if (!m) return;
    try {
      const docsRes = await fetch(`${KBDB_BASE}/blocks/documents?limit=50&_t=${Date.now()}`, {
        headers: { Authorization: `Bearer ${m.api_key}` },
        cache: 'no-store',
      });
      if (docsRes.ok) {
        const data = (await docsRes.json()) as { documents?: Document[] };
        setDocs(data.documents ?? []);
      }
    } catch (e) {
      console.error('[mira reload]', e);
    }
  }, []);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const meRes = await fetch(`${API_BASE}/me`, { credentials: 'include' });
        if (!meRes.ok) {
          window.location.href = '/login?redirect=/mira/feed';
          return;
        }
        const meData = (await meRes.json()) as Me;
        if (cancelled) return;
        setMe(meData);

        const docsRes = await fetch(`${KBDB_BASE}/blocks/documents?limit=50`, {
          headers: { Authorization: `Bearer ${meData.api_key}` },
        });
        if (!docsRes.ok) {
          setError(`KBDB 讀取失敗：${docsRes.status}`);
          return;
        }
        const data = (await docsRes.json()) as { documents?: Document[] };
        if (cancelled) return;
        setDocs(data.documents ?? []);
      } catch (e) {
        if (!cancelled) setError(e instanceof Error ? e.message : String(e));
      }
    })();
    return () => { cancelled = true; };
  }, []);

  return (
    <div className="mira-app">
      <div className="mira-content">
        {me && (
          <PostComposer
            me={me}
            onPosted={reload}
            onAiTriggered={(pageName) => {
              setAiThinking(prev => new Set(prev).add(pageName));
              // 90 秒後 hard timeout（Mira 思考 + KBDB write 通常 10-45 秒，給寬限）
              // 卡片內 useEffect 偵測 AI reply 真的進來會主動呼叫 onAiResponded 清掉
              setTimeout(() => {
                setAiThinking(prev => {
                  if (!prev.has(pageName)) return prev;
                  const n = new Set(prev);
                  n.delete(pageName);
                  return n;
                });
              }, 90000);
            }}
          />
        )}

        {error && (
          <div className="mira-card">
            <div className="mira-error">{error}</div>
          </div>
        )}

        {!error && docs === null && <div className="empty-state">載入中…</div>}

        {!error && docs !== null && docs.length === 0 && me && (
          <EmptyRiver email={me.email} />
        )}

        {!error && docs && docs.length > 0 && me && (
          <DocsList
            docs={docs}
            me={me}
            aiThinking={aiThinking}
            onAiResponded={(pageName) => {
              setAiThinking(prev => {
                if (!prev.has(pageName)) return prev;
                const n = new Set(prev);
                n.delete(pageName);
                return n;
              });
            }}
          />
        )}
      </div>
    </div>
  );
}

// ─── AI 回覆觸發器（fire-and-forget）──────────────────────

async function triggerWikiSynthesis(opts: { apiKey: string; rawBlockId: string }) {
  // 觸發 arcrun wiki_synthesis workflow（arcrun-native public trigger endpoint）
  // 不等結果（workflow 60-90s 含 2 次 claude_api pause/resume）
  try {
    const res = await fetch(`${API_BASE}/webhooks/named/wiki_synthesis/trigger`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Arcrun-API-Key': opts.apiKey,
      },
      body: JSON.stringify({ api_key: opts.apiKey, raw_block_id: opts.rawBlockId }),
    });
    if (!res.ok) {
      console.warn('[wiki_synthesis trigger] non-ok:', res.status);
      return;
    }
    const data = await res.json().catch(() => ({}));
    console.log('[wiki_synthesis trigger] response:', data);
  } catch (e) {
    console.warn('[wiki_synthesis trigger] error:', e);
  }
}

async function triggerAiReply(opts: {
  apiKey: string;
  postContent: string;
  parentBlockId: string;
  pageName: string;
}) {
  const prompt =
    `用戶 leo 在 mira 河道發了這則貼文：\n\n` +
    `「${opts.postContent}」\n\n` +
    `請以 Mira 副駕 AI 的身份留言回應。\n` +
    `規則：\n` +
    `- 繁體中文（台灣用語）\n` +
    `- 簡短 1-3 段，務實，不客套\n` +
    `- 可以發問、補充、提建議、或反問釐清\n` +
    `- 不替老闆下決策，但可以給判斷依據`;

  try {
    const aiRes = await fetch(CLAUDE_API, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ prompt, timeout_ms: 25000 }),
    });
    const aiData = (await aiRes.json()) as {
      success?: boolean;
      pending?: boolean;
      data?: { text?: string };
      error?: string;
    };
    if (!aiRes.ok || !aiData.success) {
      console.error('[mira ai reply] claude_api failed:', aiData.error);
      return;
    }
    if (aiData.pending) {
      // daemon 切到 async，目前簡化版不處理 polling
      console.warn('[mira ai reply] daemon went async, skipping');
      return;
    }
    const text = aiData.data?.text;
    if (!text) {
      console.warn('[mira ai reply] no text in response');
      return;
    }

    // 寫成 chat block (parent_id = 原貼文 block, source=ai-mira)
    await fetch(KBDB_CREATE, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        api_key: opts.apiKey,
        content: text,
        type: 'chat',
        parent_id: opts.parentBlockId,
        user_id: MIRA_POST_USER_ID,
        source: 'ai-mira',
        page_name: opts.pageName,
      }),
    });
  } catch (e) {
    console.error('[mira ai reply] exception:', e);
  }
}

// ─── 寫貼文 composer ──────────────────────────────────────

function PostComposer({
  me,
  onPosted,
  onAiTriggered,
}: {
  me: Me;
  onPosted: () => Promise<void> | void;
  onAiTriggered: (pageName: string) => void;
}) {
  const [text, setText] = useState('');
  const [submitting, setSubmitting] = useState(false);
  const [err, setErr] = useState<string | null>(null);
  const taRef = useRef<HTMLTextAreaElement>(null);

  const submit = useCallback(async () => {
    const trimmed = text.trim();
    if (!trimmed || submitting) return;
    setSubmitting(true);
    setErr(null);
    try {
      const stamp = new Date().toISOString();
      const pageName = `post-${stamp}-${Math.random().toString(36).slice(2, 8)}`;

      const res = await fetch(KBDB_CREATE, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          api_key: me.api_key,
          content: trimmed,
          type: 'note',
          user_id: MIRA_POST_USER_ID,
          source: 'km-writer-direct',
          page_name: pageName,
        }),
      });
      const data = (await res.json()) as { success?: boolean; error?: string; data?: { id?: string } };
      if (!res.ok || !data.success || !data.data?.id) {
        setErr(data.error || `寫入失敗：${res.status}`);
        return;
      }
      const postBlockId = data.data.id;
      setText('');

      // fire-and-forget 觸發 Mira AI 回覆（不擋用戶）
      void triggerAiReply({
        apiKey: me.api_key,
        postContent: trimmed,
        parentBlockId: postBlockId,
        pageName,
      });
      // 7B.3h：fire-and-forget 觸發 wiki_synthesis（browser → cypher.arcrun.dev，arcrun-native）
      // 不走 watcher 是因為 cypher-executor 自己 fetch 自己 workers.dev URL 被 CF 1042 擋
      // watcher 仍作為 cron-driven backup（漏掉的 raws 5 分鐘後補跑），但需先解 self-fetch 問題
      void triggerWikiSynthesis({ apiKey: me.api_key, rawBlockId: postBlockId });
      onAiTriggered(pageName);

      // 給 D1 GROUP BY 查詢看到新資料的時間
      await new Promise(r => setTimeout(r, 1500));
      await onPosted();
    } catch (e) {
      setErr(e instanceof Error ? e.message : String(e));
    } finally {
      setSubmitting(false);
    }
  }, [text, submitting, me, onPosted, onAiTriggered]);

  return (
    <div className="mira-card mira-composer-card">
      <div className="mira-composer-row">
        <Avatar me={me} />
        <textarea
          ref={taRef}
          value={text}
          onChange={e => setText(e.target.value)}
          onKeyDown={e => {
            if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
              e.preventDefault();
              submit();
              return;
            }
            handleTabIndent(e, text, setText);
          }}
          placeholder="現在想分享什麼？"
          rows={2}
          disabled={submitting}
          className="mira-composer-textarea"
        />
      </div>
      <div className="mira-composer-actions">
        <MarkdownToolbar textareaRef={taRef} value={text} setValue={setText} compact />
        {err && <span className="mira-msg-error">{err}</span>}
        <span style={{ flex: 1 }} />
        <span className="mira-kbd">⌘+Enter</span>
        <button
          type="button"
          disabled={submitting || !text.trim()}
          onClick={submit}
          className={`mira-btn-primary${!text.trim() ? ' disabled' : ''}`}
        >
          {submitting ? '送出中⋯' : '貼文'}
        </button>
      </div>
    </div>
  );
}

function Avatar({ me, size = 40 }: { me: Me; size?: number }) {
  if (me.avatar_url) {
    return (
      // eslint-disable-next-line @next/next/no-img-element
      <img
        src={me.avatar_url}
        alt={me.display_name}
        width={size}
        height={size}
        className="mira-avatar"
        style={{ width: size, height: size }}
      />
    );
  }
  const initial = (me.display_name || me.email || '?').slice(0, 1).toUpperCase();
  return (
    <div className="mira-avatar mira-avatar-fallback" style={{ width: size, height: size, fontSize: size * 0.4 }}>
      {initial}
    </div>
  );
}

// ─── ⋮ 選單 ──────────────────────────────────────────────

type MenuItem = { label: string; onClick: () => void; danger?: boolean };

function MoreMenu({ items }: { items: MenuItem[] }) {
  const [open, setOpen] = useState(false);
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!open) return;
    const onClickOutside = (e: MouseEvent | TouchEvent) => {
      if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
    };
    document.addEventListener('mousedown', onClickOutside);
    document.addEventListener('touchstart', onClickOutside);
    return () => {
      document.removeEventListener('mousedown', onClickOutside);
      document.removeEventListener('touchstart', onClickOutside);
    };
  }, [open]);

  return (
    <div ref={ref} className="mira-menu-wrap">
      <button
        type="button"
        className="mira-icon-btn mira-icon-btn-vertical"
        aria-label="更多"
        onClick={(e) => { e.stopPropagation(); setOpen(o => !o); }}
      >
        ⋮
      </button>
      {open && (
        <div className="mira-menu" onClick={(e) => e.stopPropagation()} role="menu">
          {items.map((item, i) => (
            <button
              key={i}
              type="button"
              className={`mira-menu-item${item.danger ? ' danger' : ''}`}
              onClick={() => { item.onClick(); setOpen(false); }}
              role="menuitem"
            >
              {item.label}
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

// ─── 列表 ──────────────────────────────────────────────────

function EmptyRiver({ email }: { email: string }) {
  return (
    <div className="empty-state">
      <div style={{ fontSize: 56, marginBottom: 14 }}>🌊</div>
      <p style={{ color: 'var(--mira-text-2)', fontSize: 14, marginBottom: 6 }}>
        河裡還沒有東西。
      </p>
      <p style={{ fontSize: 12, color: 'var(--mira-text-3)' }}>
        登入身份：<span className="mira-en">{email}</span>
        <br />
        在上方寫第一則貼文。
      </p>
    </div>
  );
}

function DocsList({
  docs,
  me,
  aiThinking,
  onAiResponded,
}: {
  docs: Document[];
  me: Me;
  aiThinking: Set<string>;
  onAiResponded: (pageName: string) => void;
}) {
  return (
    <>
      {docs.map(d => (
        <DocCard
          key={`${d.page_name}::${d.user_id}`}
          doc={d}
          me={me}
          aiThinking={aiThinking.has(d.page_name)}
          onAiResponded={() => onAiResponded(d.page_name)}
        />
      ))}
    </>
  );
}

// ─── 卡片（社群貼文風格）────────────────────────────────────

function DocCard({
  doc,
  me,
  aiThinking,
  onAiResponded,
}: {
  doc: Document;
  me: Me;
  aiThinking: boolean;
  onAiResponded: () => void;
}) {
  const [blocks, setBlocks] = useState<KBDBBlock[] | null>(null);
  const [loading, setLoading] = useState(false);
  const [expandedFull, setExpandedFull] = useState(false);
  const [showReply, setShowReply] = useState(false);
  const [showAllReplies, setShowAllReplies] = useState(false);
  const [editingPost, setEditingPost] = useState(false);
  const cardRef = useRef<HTMLElement>(null);
  const fetchedRef = useRef(false);

  // Lazy fetch：進入視窗才抓 sub-blocks（IntersectionObserver）
  useEffect(() => {
    if (fetchedRef.current) return;
    const node = cardRef.current;
    if (!node) return;

    const trigger = async () => {
      if (fetchedRef.current) return;
      fetchedRef.current = true;
      setLoading(true);
      try {
        const res = await fetch(
          `${KBDB_BASE}/blocks?page_name=${encodeURIComponent(doc.page_name)}&limit=200`,
          { headers: { Authorization: `Bearer ${me.api_key}` } },
        );
        if (res.ok) {
          const data = (await res.json()) as { blocks?: KBDBBlock[] };
          setBlocks(data.blocks ?? []);
        } else {
          setBlocks([]);
        }
      } catch {
        setBlocks([]);
      } finally {
        setLoading(false);
      }
    };

    if (typeof IntersectionObserver === 'undefined') {
      // 不支援 IntersectionObserver 的環境（極舊瀏覽器）：直接 fetch
      void trigger();
      return;
    }

    const io = new IntersectionObserver((entries) => {
      for (const entry of entries) {
        if (entry.isIntersecting) {
          void trigger();
          io.disconnect();
          break;
        }
      }
    }, {
      rootMargin: '200px 0px',  // 提前 200px 觸發，不等卡片完全進入視窗
      threshold: 0.01,
    });
    io.observe(node);
    return () => io.disconnect();
  }, [doc.page_name, me.api_key]);

  // 強制重抓（外部呼叫 / aiThinking polling）
  const refetch = useCallback(async (): Promise<KBDBBlock[] | null> => {
    try {
      const res = await fetch(
        `${KBDB_BASE}/blocks?page_name=${encodeURIComponent(doc.page_name)}&limit=200&_t=${Date.now()}`,
        { headers: { Authorization: `Bearer ${me.api_key}` }, cache: 'no-store' },
      );
      if (res.ok) {
        const data = (await res.json()) as { blocks?: KBDBBlock[] };
        const fresh = data.blocks ?? [];
        setBlocks(fresh);
        return fresh;
      }
    } catch {}
    return null;
  }, [doc.page_name, me.api_key]);

  // aiThinking 期間每 5 秒 refetch；偵測 AI reply 出現就清 thinking
  useEffect(() => {
    if (!aiThinking) return;
    const id = setInterval(async () => {
      const fresh = await refetch();
      if (fresh && fresh.some(b => b.source?.startsWith('ai-'))) {
        onAiResponded();
      }
    }, 5000);
    return () => clearInterval(id);
  }, [aiThinking, refetch, onAiResponded]);

  // 主內容（type != chat 的 sorted blocks），合成單一文字流
  // 留言（type=chat）拍平按時間排，不論 parent_id 都集中到卡片底部
  const { mainContent, mainBlocksList, allReplies } = useMemo(() => {
    if (!blocks) return {
      mainContent: '',
      mainBlocksList: [] as KBDBBlock[],
      allReplies: [] as KBDBBlock[],
    };

    const sorted = [...blocks].sort((a, b) => {
      const sa = a.sort_order ?? 0;
      const sb = b.sort_order ?? 0;
      if (sa !== sb) return sa - sb;
      return toDate(a.created_at).getTime() - toDate(b.created_at).getTime();
    });

    const main = sorted.filter(b => b.type !== 'chat');
    const chats = sorted.filter(b => b.type === 'chat');
    const repliesSorted = [...chats].sort(
      (a, b) => toDate(a.created_at).getTime() - toDate(b.created_at).getTime(),
    );

    const text = main.map(b => b.content || '').filter(t => t.trim()).join('\n\n');
    return { mainContent: text, mainBlocksList: main, allReplies: repliesSorted };
  }, [blocks]);

  // 預覽截斷
  const { previewText, isLong } = useMemo(() => {
    const text = mainContent;
    const lines = text.split('\n');
    const tooManyLines = lines.length > PREVIEW_LINES;
    const tooLong = text.length > PREVIEW_CHARS;
    if (!tooManyLines && !tooLong) return { previewText: text, isLong: false };
    let preview = lines.slice(0, PREVIEW_LINES).join('\n');
    if (preview.length > PREVIEW_CHARS) preview = preview.slice(0, PREVIEW_CHARS);
    return { previewText: preview, isLong: true };
  }, [mainContent]);

  const deletePost = useCallback(async () => {
    if (!blocks || blocks.length === 0) return;
    const ok = window.confirm(`確定刪除這篇貼文？（共 ${blocks.length} 個 block + 留言將一併刪除，無法復原）`);
    if (!ok) return;
    await Promise.all(
      blocks.map(b =>
        fetch(`${KBDB_BASE}/blocks/${b.id}`, {
          method: 'DELETE',
          headers: { Authorization: `Bearer ${me.api_key}` },
        }).catch(() => null),
      ),
    );
    window.location.reload();
  }, [blocks, me.api_key]);

  const cardMenu: MenuItem[] = [
    ...(isLong || expandedFull ? [{
      label: expandedFull ? '收起' : '展開全文',
      onClick: () => setExpandedFull(v => !v),
    }] : []),
    { label: '編輯', onClick: () => setEditingPost(true) },
    { label: '留言', onClick: () => setShowReply(true) },
    { label: '複製連結', onClick: () => {
      const url = `${window.location.origin}/mira/feed#page=${encodeURIComponent(doc.page_name)}`;
      void navigator.clipboard?.writeText(url);
    }},
    { label: '刪除', onClick: deletePost, danger: true },
  ];

  const totalReplies = allReplies.length;

  // 樂觀添加新 reply
  const addReply = useCallback((b: KBDBBlock) => {
    setBlocks(curr => (curr ? [...curr, b] : [b]));
  }, []);

  const updateBlock = useCallback((id: string, newContent: string) => {
    setBlocks(curr => curr ? curr.map(b => b.id === id ? { ...b, content: newContent } : b) : null);
  }, []);

  return (
    <article className="mira-card mira-post" ref={cardRef}>
      {/* 頭部：作者區塊 */}
      <header className="mira-post-header">
        <Avatar me={me} size={40} />
        <div className="mira-post-author">
          <div className="mira-post-name">{me.display_name || me.email}</div>
          <div className="mira-post-time">
            <SourceBadge source={inferSource(blocks, doc.page_name)} />
            <span>·</span>
            <RelTime when={doc.updated_at} />
          </div>
        </div>
        <MoreMenu items={cardMenu} />
      </header>

      {/* 內容主體 */}
      <div className="mira-post-body">
        {loading && <div className="mira-skel" />}
        {!loading && !mainContent && !editingPost && (
          <em style={{ color: 'var(--mira-text-3)' }}>（無內容）</em>
        )}
        {!loading && editingPost && (
          <PostEditor
            initial={mainContent}
            mainBlocks={mainBlocksList}
            apiKey={me.api_key}
            onCancel={() => setEditingPost(false)}
            onSaved={(newContent) => {
              setEditingPost(false);
              // 樂觀更新：第一個 block 改新內容、其餘清空
              setBlocks(curr => {
                if (!curr) return curr;
                const main = curr.filter(b => b.type !== 'chat');
                const chats = curr.filter(b => b.type === 'chat');
                if (main.length === 0) return curr;
                const updated: KBDBBlock[] = [
                  { ...main[0], content: newContent },
                  ...chats,
                ];
                return updated;
              });
            }}
          />
        )}
        {!loading && !editingPost && mainContent && (
          expandedFull ? (
            <>
              <FullContent blocks={mainBlocksList}
                apiKey={me.api_key} docPageName={doc.page_name}
                addReply={addReply} updateBlock={updateBlock} />
              <button
                type="button"
                className="mira-show-more"
                onClick={() => setExpandedFull(false)}
              >
                ▴ 收起
              </button>
            </>
          ) : (
            <>
              <PreviewContent text={previewText} />
              {isLong && (
                <button
                  type="button"
                  className="mira-show-more"
                  onClick={() => setExpandedFull(true)}
                >
                  ⋯查看更多
                </button>
              )}
            </>
          )
        )}
      </div>

      {/* 互動列 */}
      <footer className="mira-post-footer">
        <button type="button" className="mira-action-btn" onClick={() => setShowReply(s => !s)}>
          💬 留言{totalReplies > 0 && <span className="mira-action-count">{totalReplies}</span>}
        </button>
      </footer>

      {/* 留言區（所有 chat 拍平按時間排，預設顯示後 2 則，點開看全部）*/}
      {(allReplies.length > 0 || aiThinking) && (
        <div className="mira-post-replies">
          {!showAllReplies && allReplies.length > 2 && (
            <button
              type="button"
              className="mira-show-replies"
              onClick={() => setShowAllReplies(true)}
            >
              查看全部 {allReplies.length} 則留言
            </button>
          )}
          {(showAllReplies ? allReplies : allReplies.slice(-2)).map(r => (
            <ReplyLine
              key={r.id}
              reply={r}
              apiKey={me.api_key}
              docPageName={doc.page_name}
              onUpdated={updateBlock}
              onReplyAdded={addReply}
              repliesByParent={new Map()}
              depth={0}
            />
          ))}
          {aiThinking && (
            <div className="mira-reply-line is-ai mira-thinking">
              <span className="mira-reply-icon">🤖</span>
              <div className="mira-reply-content">
                <span className="mira-thinking-dots">Mira 思考中</span>
              </div>
            </div>
          )}
          {showAllReplies && allReplies.length > 2 && (
            <button
              type="button"
              className="mira-show-replies"
              onClick={() => setShowAllReplies(false)}
            >
              收起留言
            </button>
          )}
        </div>
      )}

      {/* 留言 composer */}
      {showReply && (
        <div className="mira-reply-composer">
          <Avatar me={me} size={32} />
          <PageReplyComposer
            apiKey={me.api_key}
            docPageName={doc.page_name}
            onSubmitted={(b) => { addReply(b); setShowReply(false); }}
            onCancel={() => setShowReply(false)}
          />
        </div>
      )}
    </article>
  );
}

// 預覽內容（純文字，不顯示 sub-block 結構）
function PreviewContent({ text }: { text: string }) {
  return <div className="mira-post-content"><MarkdownView text={text} /></div>;
}

// 整篇貼文編輯器：把所有 main blocks 合併成一段編輯
// 存回時：第一個 block 改成新內容，其餘 main blocks 刪除
function PostEditor({
  initial,
  mainBlocks,
  apiKey,
  onCancel,
  onSaved,
}: {
  initial: string;
  mainBlocks: KBDBBlock[];
  apiKey: string;
  onCancel: () => void;
  onSaved: (newContent: string) => void;
}) {
  const [draft, setDraft] = useState(initial);
  const [saving, setSaving] = useState(false);
  const [err, setErr] = useState<string | null>(null);

  const save = async () => {
    if (saving) return;
    const trimmed = draft.trim();
    if (!trimmed) {
      setErr('內容不能空');
      return;
    }
    if (trimmed === initial.trim()) {
      onCancel();
      return;
    }
    if (mainBlocks.length === 0) {
      setErr('找不到可編輯的 block');
      return;
    }
    setSaving(true);
    setErr(null);
    try {
      // 1. 改第一個 block 內容
      const firstId = mainBlocks[0].id;
      const patchRes = await fetch(KBDB_PATCH, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ api_key: apiKey, block_id: firstId, content: trimmed }),
      });
      const patchData = (await patchRes.json()) as { success?: boolean; error?: string };
      if (!patchRes.ok || !patchData.success) {
        setErr(patchData.error || `儲存失敗：${patchRes.status}`);
        return;
      }

      // 2. 刪其餘 main blocks（如有）
      if (mainBlocks.length > 1) {
        await Promise.all(
          mainBlocks.slice(1).map(b =>
            fetch(`${KBDB_BASE}/blocks/${b.id}`, {
              method: 'DELETE',
              headers: { Authorization: `Bearer ${apiKey}` },
            }).catch(() => null),
          ),
        );
      }

      onSaved(trimmed);
    } catch (e) {
      setErr(e instanceof Error ? e.message : String(e));
    } finally {
      setSaving(false);
    }
  };

  return (
    <EditingArea
      draft={draft}
      setDraft={setDraft}
      onSubmit={save}
      onCancel={onCancel}
      saving={saving}
      err={err}
      submitLabel="儲存"
      placeholder="編輯內容⋯"
    />
  );
}

// 完整內容（每個 sub-block 一行，可編輯，可留言）
function FullContent({
  blocks,
  apiKey,
  docPageName,
  addReply,
  updateBlock,
}: {
  blocks: KBDBBlock[];
  apiKey: string;
  docPageName: string;
  addReply: (b: KBDBBlock) => void;
  updateBlock: (id: string, c: string) => void;
}) {
  // 只顯示 main blocks（type != chat），所有 chat 全部集中到卡片底部 replies 區
  return (
    <div className="mira-post-content mira-post-content-full">
      {blocks.slice(0, 100).map(b => (
        <BlockLine
          key={b.id}
          block={b}
          apiKey={apiKey}
          docPageName={docPageName}
          onUpdated={updateBlock}
          onReplyAdded={addReply}
        />
      ))}
      {blocks.length > 100 && (
        <div style={{ color: 'var(--mira-text-2)', fontSize: 12, marginTop: 12 }}>
          …還有 {blocks.length - 100} 個 block 未顯示
        </div>
      )}
    </div>
  );
}

function BlockLine({
  block,
  apiKey,
  docPageName,
  onUpdated,
  onReplyAdded,
}: {
  block: KBDBBlock;
  apiKey: string;
  docPageName: string;
  onUpdated: (id: string, c: string) => void;
  onReplyAdded: (b: KBDBBlock) => void;
}) {
  const [mode, setMode] = useState<'view' | 'edit' | 'reply'>('view');
  const [draft, setDraft] = useState(block.content ?? '');
  const [saving, setSaving] = useState(false);
  const [err, setErr] = useState<string | null>(null);

  const startEdit = () => { setDraft(block.content ?? ''); setMode('edit'); setErr(null); };
  const startReply = () => { setDraft(''); setMode('reply'); setErr(null); };
  const cancel = () => { setMode('view'); setDraft(block.content ?? ''); setErr(null); };

  const saveEdit = async () => {
    if (saving) return;
    if (draft === (block.content ?? '')) { setMode('view'); return; }
    setSaving(true); setErr(null);
    try {
      const res = await fetch(KBDB_PATCH, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ api_key: apiKey, block_id: block.id, content: draft }),
      });
      const data = (await res.json()) as { success?: boolean; error?: string };
      if (!res.ok || !data.success) { setErr(data.error || `儲存失敗：${res.status}`); return; }
      onUpdated(block.id, draft);
      setMode('view');
    } catch (e) { setErr(e instanceof Error ? e.message : String(e)); }
    finally { setSaving(false); }
  };

  const submitReply = async () => {
    if (saving) return;
    const trimmed = draft.trim();
    if (!trimmed) { cancel(); return; }
    setSaving(true); setErr(null);
    try {
      const res = await fetch(KBDB_CREATE, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          api_key: apiKey,
          content: trimmed, type: 'chat', parent_id: block.id,
          user_id: MIRA_POST_USER_ID, source: 'mira-reply', page_name: docPageName,
        }),
      });
      const data = (await res.json()) as { success?: boolean; data?: { id?: string }; error?: string };
      if (!res.ok || !data.success || !data.data?.id) {
        setErr(data.error || `留言失敗：${res.status}`); return;
      }
      onReplyAdded({
        id: data.data.id, content: trimmed, type: 'chat', source: 'mira-reply',
        user_id: MIRA_POST_USER_ID, parent_id: block.id, page_name: docPageName,
        tags_json: '[]', refs_json: '[]', sort_order: 0,
        created_at: Math.floor(Date.now() / 1000), updated_at: Math.floor(Date.now() / 1000),
      });
      setMode('view'); setDraft('');
    } catch (e) { setErr(e instanceof Error ? e.message : String(e)); }
    finally { setSaving(false); }
  };

  const indent = block.parent_id ? 16 : 0;
  const borderLeft = block.parent_id ? '2px solid var(--mira-line-soft)' : 'none';

  const deleteBlock = async () => {
    const ok = window.confirm('確定刪除這個 block？（不可復原）');
    if (!ok) return;
    await fetch(`${KBDB_BASE}/blocks/${block.id}`, {
      method: 'DELETE',
      headers: { Authorization: `Bearer ${apiKey}` },
    }).catch(() => null);
    // 設為空內容讓父層感知（樂觀更新；refetch 才會真正消失）
    onUpdated(block.id, '【已刪除】');
  };

  const menu: MenuItem[] = [
    { label: '編輯', onClick: startEdit },
    { label: '回覆', onClick: startReply },
    { label: '刪除', onClick: deleteBlock, danger: true },
  ];

  return (
    <div className="mira-block-line"
      style={{ marginBottom: 8, paddingLeft: indent, borderLeft, position: 'relative', display: 'flex', alignItems: 'flex-start', gap: 8 }}
    >
      <div style={{ flex: 1, minWidth: 0 }}>
        {mode === 'view' && (
          block.content ? <MarkdownView text={block.content} /> :
            <em style={{ color: 'var(--mira-text-3)' }}>（空）</em>
        )}
        {mode === 'edit' && (
          <EditingArea draft={draft} setDraft={setDraft} onSubmit={saveEdit} onCancel={cancel} saving={saving} err={err} submitLabel="儲存" />
        )}
        {mode === 'reply' && (
          <EditingArea draft={draft} setDraft={setDraft} onSubmit={submitReply} onCancel={cancel} saving={saving} err={err} submitLabel="回覆" placeholder="回覆⋯⋯" />
        )}
      </div>
      {mode === 'view' && <MoreMenu items={menu} />}
    </div>
  );
}

function ReplyLine({
  reply,
  apiKey,
  docPageName,
  onUpdated,
  onReplyAdded,
  repliesByParent,
  depth,
}: {
  reply: KBDBBlock;
  apiKey: string;
  docPageName: string;
  onUpdated: (id: string, c: string) => void;
  onReplyAdded: (b: KBDBBlock) => void;
  repliesByParent: Map<string, KBDBBlock[]>;
  depth: number;
}) {
  const [mode, setMode] = useState<'view' | 'edit' | 'reply'>('view');
  const [draft, setDraft] = useState(reply.content ?? '');
  const [saving, setSaving] = useState(false);
  const [err, setErr] = useState<string | null>(null);
  const isAI = reply.source?.startsWith('ai-') ?? false;

  const startEdit = () => { setDraft(reply.content ?? ''); setMode('edit'); setErr(null); };
  const startReply = () => { setDraft(''); setMode('reply'); setErr(null); };
  const cancel = () => { setMode('view'); setDraft(reply.content ?? ''); setErr(null); };

  const save = async () => {
    if (saving || draft === (reply.content ?? '')) { setMode('view'); return; }
    setSaving(true); setErr(null);
    try {
      const res = await fetch(KBDB_PATCH, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ api_key: apiKey, block_id: reply.id, content: draft }),
      });
      const data = (await res.json()) as { success?: boolean; error?: string };
      if (!res.ok || !data.success) { setErr(data.error || `儲存失敗：${res.status}`); return; }
      onUpdated(reply.id, draft);
      setMode('view');
    } catch (e) { setErr(e instanceof Error ? e.message : String(e)); }
    finally { setSaving(false); }
  };

  const submitReply = async () => {
    if (saving) return;
    const trimmed = draft.trim();
    if (!trimmed) { cancel(); return; }
    setSaving(true); setErr(null);
    try {
      const res = await fetch(KBDB_CREATE, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          api_key: apiKey,
          content: trimmed, type: 'chat', parent_id: reply.id,
          user_id: MIRA_POST_USER_ID, source: 'mira-reply', page_name: docPageName,
        }),
      });
      const data = (await res.json()) as { success?: boolean; data?: { id?: string }; error?: string };
      if (!res.ok || !data.success || !data.data?.id) {
        setErr(data.error || `留言失敗：${res.status}`); return;
      }
      onReplyAdded({
        id: data.data.id, content: trimmed, type: 'chat', source: 'mira-reply',
        user_id: MIRA_POST_USER_ID, parent_id: reply.id, page_name: docPageName,
        tags_json: '[]', refs_json: '[]', sort_order: 0,
        created_at: Math.floor(Date.now() / 1000), updated_at: Math.floor(Date.now() / 1000),
      });
      setMode('view'); setDraft('');
    } catch (e) { setErr(e instanceof Error ? e.message : String(e)); }
    finally { setSaving(false); }
  };

  const deleteReply = async () => {
    const ok = window.confirm('確定刪除這則留言？（不可復原）');
    if (!ok) return;
    await fetch(`${KBDB_BASE}/blocks/${reply.id}`, {
      method: 'DELETE',
      headers: { Authorization: `Bearer ${apiKey}` },
    }).catch(() => null);
    onUpdated(reply.id, '【已刪除】');
  };

  const menu: MenuItem[] = [
    { label: '回覆', onClick: startReply },
    { label: '編輯', onClick: startEdit },
    { label: '刪除', onClick: deleteReply, danger: true },
  ];

  return (
    <div className={`mira-reply-line${isAI ? ' is-ai' : ''}`}>
      <span className="mira-reply-icon">{isAI ? '🤖' : '💬'}</span>
      <div style={{ flex: 1, minWidth: 0 }}>
        {mode === 'view' && (
          <div className="mira-reply-content">
            {reply.content ? <MarkdownView text={reply.content} /> :
              <em style={{ color: 'var(--mira-text-3)' }}>（空）</em>}
          </div>
        )}
        {mode === 'edit' && (
          <EditingArea draft={draft} setDraft={setDraft} onSubmit={save} onCancel={cancel} saving={saving} err={err} submitLabel="儲存" />
        )}
        {mode === 'reply' && (
          <EditingArea draft={draft} setDraft={setDraft} onSubmit={submitReply} onCancel={cancel} saving={saving} err={err} submitLabel="回覆" placeholder="回覆這則留言⋯⋯" />
        )}
        {/* 巢狀留言 */}
        {depth < 3 && (
          <ReplyTree
            replies={repliesByParent.get(reply.id) ?? []}
            allReplies={repliesByParent}
            apiKey={apiKey}
            docPageName={docPageName}
            onUpdated={onUpdated}
            onReplyAdded={onReplyAdded}
            depth={depth + 1}
          />
        )}
      </div>
      {mode === 'view' && <MoreMenu items={menu} />}
    </div>
  );
}

function ReplyTree({
  replies,
  allReplies,
  apiKey,
  docPageName,
  onUpdated,
  onReplyAdded,
  depth,
}: {
  replies: KBDBBlock[];
  allReplies: Map<string, KBDBBlock[]>;
  apiKey: string;
  docPageName: string;
  onUpdated: (id: string, c: string) => void;
  onReplyAdded: (b: KBDBBlock) => void;
  depth: number;
}) {
  if (replies.length === 0) return null;
  const sorted = [...replies].sort((a, b) => toDate(a.created_at).getTime() - toDate(b.created_at).getTime());
  return (
    <div className="mira-reply-nested">
      {sorted.map(r => (
        <ReplyLine
          key={r.id}
          reply={r}
          apiKey={apiKey}
          docPageName={docPageName}
          onUpdated={onUpdated}
          onReplyAdded={onReplyAdded}
          repliesByParent={allReplies}
          depth={depth}
        />
      ))}
    </div>
  );
}

function PageReplyComposer({
  apiKey,
  docPageName,
  onSubmitted,
  onCancel,
}: {
  apiKey: string;
  docPageName: string;
  onSubmitted: (b: KBDBBlock) => void;
  onCancel: () => void;
}) {
  const [draft, setDraft] = useState('');
  const [saving, setSaving] = useState(false);
  const [err, setErr] = useState<string | null>(null);

  const submit = async () => {
    if (saving) return;
    const trimmed = draft.trim();
    if (!trimmed) { onCancel(); return; }
    setSaving(true); setErr(null);
    try {
      const res = await fetch(KBDB_CREATE, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          api_key: apiKey,
          content: trimmed, type: 'chat',
          user_id: MIRA_POST_USER_ID, source: 'mira-reply', page_name: docPageName,
        }),
      });
      const data = (await res.json()) as { success?: boolean; data?: { id?: string }; error?: string };
      if (!res.ok || !data.success || !data.data?.id) {
        setErr(data.error || `留言失敗：${res.status}`); return;
      }
      onSubmitted({
        id: data.data.id, content: trimmed, type: 'chat', source: 'mira-reply',
        user_id: MIRA_POST_USER_ID, parent_id: null, page_name: docPageName,
        tags_json: '[]', refs_json: '[]', sort_order: 0,
        created_at: Math.floor(Date.now() / 1000), updated_at: Math.floor(Date.now() / 1000),
      });
      setDraft('');
    } catch (e) { setErr(e instanceof Error ? e.message : String(e)); }
    finally { setSaving(false); }
  };

  return (
    <div style={{ flex: 1, minWidth: 0 }}>
      <EditingArea draft={draft} setDraft={setDraft} onSubmit={submit} onCancel={onCancel} saving={saving} err={err} submitLabel="留言" placeholder="寫留言⋯⋯" />
    </div>
  );
}

function EditingArea({
  draft, setDraft, onSubmit, onCancel, saving, err, submitLabel, placeholder,
}: {
  draft: string;
  setDraft: (s: string) => void;
  onSubmit: () => void;
  onCancel: () => void;
  saving: boolean;
  err: string | null;
  submitLabel: string;
  placeholder?: string;
}) {
  const taRef = useRef<HTMLTextAreaElement>(null);
  return (
    <>
      <MarkdownToolbar textareaRef={taRef} value={draft} setValue={setDraft} />
      <textarea
        ref={taRef}
        value={draft}
        onChange={e => setDraft(e.target.value)}
        onKeyDown={e => {
          if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); onSubmit(); return; }
          if (e.key === 'Escape') { e.preventDefault(); onCancel(); return; }
          handleTabIndent(e, draft, setDraft);
        }}
        autoFocus
        rows={Math.max(2, draft.split('\n').length)}
        disabled={saving}
        placeholder={placeholder}
        className="mira-edit-textarea"
      />
      <div className="mira-edit-actions">
        {err && <span className="mira-msg-error">{err}</span>}
        <span style={{ flex: 1 }} />
        <span className="mira-kbd">⌘+Enter · Esc</span>
        <button type="button" onClick={onCancel} disabled={saving} className="mira-btn-ghost">取消</button>
        <button type="button" onClick={onSubmit} disabled={saving} className="mira-btn-primary">
          {saving ? '處理中⋯' : submitLabel}
        </button>
      </div>
    </>
  );
}

// Tab / Shift-Tab 處理：textarea 內按 Tab 縮排（不離開 textarea）
// - 無選取：游標位置插 2 空格（行內）/ 行首插 2 空格（行首）
// - 有選取：選取範圍內每行行首加/減 2 空格
// - Shift-Tab：減縮排
function handleTabIndent(
  e: React.KeyboardEvent<HTMLTextAreaElement>,
  value: string,
  setValue: (v: string) => void,
): boolean {
  if (e.key !== 'Tab') return false;
  e.preventDefault();
  const ta = e.currentTarget;
  const start = ta.selectionStart;
  const end = ta.selectionEnd;
  const indent = '  '; // 2 空格
  const isShift = e.shiftKey;

  // 找該 selection 涉及的所有行
  const lineStart = value.lastIndexOf('\n', start - 1) + 1;
  const hasMultiLine = value.slice(start, end).includes('\n');

  if (start === end && !isShift) {
    // 無選取 + Tab：游標位置插 indent
    const next = value.slice(0, start) + indent + value.slice(start);
    setValue(next);
    requestAnimationFrame(() => {
      ta.selectionStart = ta.selectionEnd = start + indent.length;
    });
    return true;
  }

  // 有選取 或 Shift-Tab：對涉及的所有行操作
  const beforeBlock = value.slice(0, lineStart);
  const block = value.slice(lineStart, end);
  const afterBlock = value.slice(end);
  const lines = block.split('\n');

  let delta = 0;
  const newLines = lines.map((line, i) => {
    if (isShift) {
      // 減縮排：若行首是 indent 或 1 個 tab/space 開頭，去掉
      if (line.startsWith(indent)) {
        if (i === 0) delta -= indent.length;
        else delta -= indent.length;
        return line.slice(indent.length);
      }
      if (line.startsWith('\t')) {
        if (i === 0) delta -= 1;
        else delta -= 1;
        return line.slice(1);
      }
      if (line.startsWith(' ')) {
        if (i === 0) delta -= 1;
        else delta -= 1;
        return line.slice(1);
      }
      return line;
    } else {
      // 加縮排：行首加 indent
      if (i === 0) delta += indent.length;
      else delta += indent.length;
      return indent + line;
    }
  });

  const next = beforeBlock + newLines.join('\n') + afterBlock;
  setValue(next);
  requestAnimationFrame(() => {
    if (hasMultiLine) {
      ta.selectionStart = lineStart;
      ta.selectionEnd = end + delta;
    } else {
      ta.selectionStart = ta.selectionEnd = end + delta;
    }
  });
  return true;
}

// ─── Markdown toolbar（極簡，4 個按鈕）──────────────────────

function MarkdownToolbar({
  textareaRef,
  value,
  setValue,
  compact,
}: {
  textareaRef: React.RefObject<HTMLTextAreaElement | null>;
  value: string;
  setValue: (v: string) => void;
  compact?: boolean;
}) {
  const wrap = (before: string, after: string = before, placeholder = '') => {
    const ta = textareaRef.current;
    if (!ta) return;
    const start = ta.selectionStart;
    const end = ta.selectionEnd;
    const selected = value.slice(start, end);
    const insert = selected || placeholder;
    const next = value.slice(0, start) + before + insert + after + value.slice(end);
    setValue(next);
    requestAnimationFrame(() => {
      ta.focus();
      const pos = start + before.length;
      ta.setSelectionRange(pos, pos + insert.length);
    });
  };

  const linePrefix = (prefix: string) => {
    const ta = textareaRef.current;
    if (!ta) return;
    const start = ta.selectionStart;
    // 找該行行首
    const lineStart = value.lastIndexOf('\n', start - 1) + 1;
    const next = value.slice(0, lineStart) + prefix + value.slice(lineStart);
    setValue(next);
    requestAnimationFrame(() => {
      ta.focus();
      ta.setSelectionRange(start + prefix.length, start + prefix.length);
    });
  };

  return (
    <div className={`mira-md-toolbar${compact ? ' compact' : ''}`}>
      <button type="button" className="mira-md-btn" title="標題（# 開頭）" onClick={() => linePrefix('## ')}>
        H
      </button>
      <button type="button" className="mira-md-btn" title="粗體（⌘+B）" onClick={() => wrap('**', '**', '粗體')}>
        <strong>B</strong>
      </button>
      <button type="button" className="mira-md-btn" title="斜體（⌘+I）" onClick={() => wrap('*', '*', '斜體')}>
        <em>I</em>
      </button>
      <button type="button" className="mira-md-btn" title="條列" onClick={() => linePrefix('- ')}>
        •—
      </button>
      <button type="button" className="mira-md-btn" title="連結" onClick={() => wrap('[', '](url)', '文字')}>
        🔗
      </button>
    </div>
  );
}

// ─── 視覺輔助：source / time / markdown ────────────────────

function inferSource(blocks: KBDBBlock[] | null, pageName?: string): string {
  if (blocks && blocks.length > 0) {
    const main = blocks.find(b => b.type !== 'chat' && b.source);
    if (main?.source) return main.source;
  }
  // fallback：用 page_name 推測
  if (pageName) {
    if (pageName.startsWith('post-')) return 'km-writer-direct';
    // Logseq daily journal 格式：YYYYMMDD（8 位數字）
    if (/^\d{8}$/.test(pageName)) return 'logseq';
    if (pageName.startsWith('km-writer')) return 'km-writer-direct';
    if (pageName === 'MyDay' || pageName.includes('Mini me')) return 'logseq';
  }
  return 'logseq'; // 既有大部分資料來自 logseq sync
}

function SourceBadge({ source }: { source: string }) {
  const s = source.toLowerCase();
  let label = source;
  let cls = 'logseq';
  if (s.includes('logseq')) { label = 'Logseq'; cls = 'logseq'; }
  else if (s.includes('km-writer') || s.includes('mobile') || s.includes('web')) { label = 'Web'; cls = 'mobile'; }
  else if (s.includes('telegram') || s.includes('tg')) { label = 'Telegram'; cls = 'tg'; }
  else if (s.includes('rss')) { label = 'RSS'; cls = 'rss'; }
  else if (s.startsWith('ai-')) { label = 'AI'; cls = 'ai'; }
  else if (s === 'unknown') { label = '?'; cls = ''; }
  return (
    <span className={`src-tag ${cls}`}>
      <span className="src-dot" />
      {label}
    </span>
  );
}

function RelTime({ when }: { when: number | string }) {
  const d = toDate(when);
  const now = Date.now();
  const diff = now - d.getTime();
  let text: string;
  if (diff < 60_000) text = '剛才';
  else if (diff < 3600_000) text = `${Math.floor(diff / 60_000)} 分鐘前`;
  else if (diff < 86400_000) text = `${Math.floor(diff / 3600_000)} 小時前`;
  else if (diff < 7 * 86400_000) text = `${Math.floor(diff / 86400_000)} 天前`;
  else text = d.toLocaleDateString('zh-TW', { year: 'numeric', month: 'numeric', day: 'numeric' });
  return <span className="mira-rel-time" title={d.toLocaleString('zh-TW')}>{text}</span>;
}

// 用 react-markdown 渲染（GFM 支援：表格、刪除線、task list、autolink）
// + 預先 strip Logseq 專屬語法（屬性行、collapsed、id::、logseq.* 等）
// MarkdownView 與 stripLogseqMeta 已抽出至 _shared/markdown.tsx（給 wiki 頁面共用）

// ─── helpers ───────────────────────────────────────────────

function toDate(x: number | string): Date {
  if (typeof x === 'number') return new Date(x * 1000);
  const d = new Date(x);
  return Number.isNaN(d.getTime()) ? new Date(0) : d;
}
