import React, { useEffect, useMemo, useRef, useState } from "react";
import {
  api,
  BrandData,
  PlacementTopicData,
  ScanPostData,
  KnowledgeGraphData,
  KnowledgeGraphNodeData,
  Persona,
} from "../api/haixun";
import { Badge, Button, Card, ErrorText, Field, Input, PageTitle, Select, Textarea } from "../components/ui";
import { formatNano, shortText } from "../lib/format";
import { buildPatrolPlan, patrolOriginLabel, resolvePatrolProduct, resolveProductForPost } from "../lib/patrolPlan";
import { notifyJobCreated } from "../lib/jobEvents";
import { jobStatusLabel } from "../lib/jobStatus";
import { persistPatrolVoicePersonaId, readPatrolVoicePersonaId } from "../lib/patrolPersona";

const TOPICS_PER_PAGE = 6;

type TopicWorkspaceTab = "settings" | "map" | "graph" | "patrol" | "results";

type PublishFeedback = {
  kind: "success" | "error" | "info";
  message: string;
  permalink?: string;
  at: number;
};

function formatPublishError(err: unknown) {
  if (err instanceof Error && err.message.trim()) return err.message.trim();
  return "發佈失敗，請稍後再試";
}

const SCAN_POSTS_LIMIT = 50;

function outreachStatusLabel(status?: string) {
  switch ((status || "").toLowerCase()) {
    case "drafted":
      return "已生成草稿";
    case "published":
      return "已回覆";
    case "skipped":
      return "已略過";
    case "new":
      return "待處理";
    default:
      return status || "待處理";
  }
}

function graphNodeForPost(graph: KnowledgeGraphData | null, post: ScanPostData) {
  if (!graph?.nodes?.length || !post.graph_node_id) return null;
  return graph.nodes.find((n) => n.id === post.graph_node_id) || null;
}

function looksCategoryMismatch(text: string, productLabel: string, matchTags: string[]) {
  const blob = `${productLabel} ${matchTags.join(" ")}`.toLowerCase();
  const body = (text || "").toLowerCase();
  const hasLaundryProduct = /洗衣精|洗衣粉|洗衣劑|洗衣液/.test(blob);
  const hasDishProduct = /洗碗|碗盤/.test(blob);
  const hasDishPost = /洗碗|洗碗機|碗盤|廚房油污/.test(body);
  const hasLaundryPost = /洗衣精|洗衣粉|洗衣劑|洗衣液|衣物|污漬/.test(body);
  if (hasLaundryProduct && hasDishPost && !hasLaundryPost) return true;
  if (hasDishProduct && hasLaundryPost && !hasDishPost) return true;

  const valueLedProduct = /敏感|無香|抗敏|化療|癌症|乳癌|荷爾蒙|香精|香味|過敏/.test(blob);
  const appliancePost = /洗衣機|烘衣機|洗衣槽|洗脫烘/.test(body);
  const coreOrFormPost = /洗衣精|洗衣粉|洗衣劑|無香|抗敏|敏感|化療|癌症|香精|香味|皮膚癢|過敏/.test(body);
  if (valueLedProduct && appliancePost && !coreOrFormPost) return true;
  return false;
}

function placementOpportunity(
  post: ScanPostData,
  node: KnowledgeGraphNodeData | null,
  productLabel: string,
  productTags: string[]
) {
  if (post.latest_draft?.reason?.trim()) return post.latest_draft.reason.trim();
  const parts: string[] = [];
  if (post.search_tag) parts.push(`從「${post.search_tag}」搜尋命中`);
  if (node?.label) parts.push(`對應痛點「${node.label}」`);
  if (node?.relation) parts.push(node.relation);
  if (node?.placement_value) parts.push(node.placement_value);
  if (productLabel) parts.push(`建議自然帶入「${productLabel}」`);
  if (productTags.length > 0) parts.push(`產品標籤：${productTags.slice(0, 4).join("、")}`);
  return parts.join(" · ") || "可先在此閱讀完整討論，再生成草稿構思回覆角度";
}

function patrolGroupTone(key: string) {
  switch (key) {
    case "selected":
      return "hx-flat-subsection--brand";
    case "research_map":
      return "hx-flat-subsection--mint";
    case "product":
      return "hx-flat-subsection--peach";
    case "graph_fallback":
      return "hx-flat-subsection--sky";
    default:
      return "";
  }
}

function pickDefaultGraphLayer(nodes: KnowledgeGraphNodeData[] | undefined, current?: number) {
  const layers = [...new Set((nodes || []).map((n) => n.layer))].sort((a, b) => a - b);
  if (layers.length === 0) return 0;
  if (current != null && layers.includes(current)) return current;
  return layers[0] ?? 0;
}

function safeUrlHostname(url: string) {
  try {
    return new URL(url).hostname;
  } catch {
    return "連結";
  }
}

const WORKSPACE_TABS: Array<{ key: TopicWorkspaceTab; label: string }> = [
  { key: "settings", label: "主題設定" },
  { key: "map", label: "研究地圖" },
  { key: "graph", label: "知識圖譜" },
  { key: "patrol", label: "海巡" },
  { key: "results", label: "找 TA 結果" },
];

export function PatrolPage() {
  const [topics, setTopics] = useState<PlacementTopicData[]>([]);
  const [brands, setBrands] = useState<BrandData[]>([]);
  const [personas, setPersonas] = useState<Persona[]>([]);

  const [selected, setSelected] = useState<PlacementTopicData | null>(null);
  const [graph, setGraph] = useState<KnowledgeGraphData | null>(null);
  const [posts, setPosts] = useState<ScanPostData[]>([]);

  // create topic form
  const [newTopic, setNewTopic] = useState({ brand_id: "", topic_name: "", seed_query: "", brief: "", product_id: "" });

  // for generate drafts
  const [selectedPersonaId, setSelectedPersonaId] = useState(() => readPatrolVoicePersonaId());

  // Discovery / 手動匯入
  const [discoveryKeywords, setDiscoveryKeywords] = useState("");
  const [searchQuery, setSearchQuery] = useState("");
  const [activeGraphLayer, setActiveGraphLayer] = useState(0);
  const [savingNodes, setSavingNodes] = useState(false);

  const [message, setMessage] = useState("");
  const [error, setError] = useState<unknown>();
  const [loading, setLoading] = useState(false);
  const [patrolScanning, setPatrolScanning] = useState(false);
  const [testPatrolScanning, setTestPatrolScanning] = useState(false);
  const [devModeEnabled, setDevModeEnabled] = useState(false);
  const [browserConnected, setBrowserConnected] = useState(false);
  const [expandingGraph, setExpandingGraph] = useState(false);
  const [generatingMap, setGeneratingMap] = useState(false);
  const [selectedPostIds, setSelectedPostIds] = useState<string[]>([]);
  const [submittingDraftJobs, setSubmittingDraftJobs] = useState(false);
  const [pendingDraftJobIds, setPendingDraftJobIds] = useState<string[]>([]);
  const [draftJobByPostId, setDraftJobByPostId] = useState<Record<string, string>>({});
  const [draftJobStatusById, setDraftJobStatusById] = useState<Record<string, string>>({});
  const [deletingPosts, setDeletingPosts] = useState(false);
  const [deletingTopicId, setDeletingTopicId] = useState<string | null>(null);
  const [pickedDraftText, setPickedDraftText] = useState<Record<string, string>>({});
  const [pickedDraftIndex, setPickedDraftIndex] = useState<Record<string, number>>({});
  const [savingDraftPostId, setSavingDraftPostId] = useState<string | null>(null);
  const [publishingPostId, setPublishingPostId] = useState<string | null>(null);
  const [publishFeedbackByPostId, setPublishFeedbackByPostId] = useState<Record<string, PublishFeedback>>({});
  const [publishToast, setPublishToast] = useState<PublishFeedback | null>(null);
  const [resultsRecentOnly, setResultsRecentOnly] = useState(false);
  const [resultsMinFit, setResultsMinFit] = useState(0);
  const [patrolJobWatchId, setPatrolJobWatchId] = useState<string | null>(null);
  const [topicPage, setTopicPage] = useState(1);
  const [workspaceTab, setWorkspaceTab] = useState<TopicWorkspaceTab>("settings");
  const workspaceRef = useRef<HTMLDivElement | null>(null);
  const syncedDraftIdRef = useRef<Record<string, string>>({});

  async function load(nextId?: string) {
    setError(undefined);
    try {
      const [topicData, brandData, personaData] = await Promise.all([
        api.placementTopics(),
        api.brands(),
        api.personas(),
      ]);

      const tlist = topicData.list || [];
      const blist = brandData.list || [];
      const plist = personaData.list || [];

      setTopics(tlist);
      setBrands(blist);
      setPersonas(plist);

      if (plist.length > 0) {
        const stored = readPatrolVoicePersonaId();
        const preferred = stored && plist.some((p) => p.id === stored) ? stored : plist[0].id;
        setSelectedPersonaId((current) => {
          if (current && plist.some((p) => p.id === current)) return current;
          return preferred;
        });
      }

      const nextTopic = tlist.find((t) => t.id === (nextId || selected?.id)) || tlist[0] || null;
      setSelected(nextTopic);

      if (nextTopic) {
        await loadTopicDetails(nextTopic.id);
      } else {
        setGraph(null);
        setPosts([]);
      }
    } catch (err) {
      setError(err);
    }
  }

  async function loadTopicDetails(topicId: string) {
    setLoading(true);
    try {
      const [g, p] = await Promise.allSettled([
        api.getPlacementTopicGraph(topicId).catch(() => null),
        api.listPlacementTopicScanPosts(topicId, { limit: SCAN_POSTS_LIMIT }),
      ]);

      setGraph(g.status === "fulfilled" ? g.value : null);
      setPosts(p.status === "fulfilled" ? p.value.list || [] : []);
      if (g.status === "fulfilled" && g.value?.nodes?.length) {
        setActiveGraphLayer((current) => pickDefaultGraphLayer(g.value?.nodes, current));
      }

    } catch (err) {
      setError(err);
    } finally {
      setLoading(false);
    }
  }

  useEffect(() => {
    void load();
    void (async () => {
      try {
        const [placementSettings, caps] = await Promise.all([api.placementSettings(), api.capabilities()]);
        setDevModeEnabled(Boolean((placementSettings as { dev_mode_enabled?: boolean })?.dev_mode_enabled));
        const accountId = caps.active_threads_account_id;
        if (accountId) {
          const conn = await api.getThreadsConnection(accountId);
          setBrowserConnected(conn.browser_connected);
        } else {
          setBrowserConnected(false);
        }
      } catch {
        setDevModeEnabled(false);
        setBrowserConnected(false);
      }
    })();
  }, []);

  useEffect(() => {
    function onPlacementSettingsChanged(event: Event) {
      const detail = (event as CustomEvent<{ dev_mode_enabled?: boolean }>).detail;
      if (detail?.dev_mode_enabled !== undefined) {
        setDevModeEnabled(detail.dev_mode_enabled);
      }
    }
    window.addEventListener("haixun:placement-settings-changed", onPlacementSettingsChanged);
    return () => window.removeEventListener("haixun:placement-settings-changed", onPlacementSettingsChanged);
  }, []);

  useEffect(() => {
    setDiscoveryKeywords("");
  }, [selected?.id]);

  // ---------- Topic create ----------
  async function createTopic() {
    if (!newTopic.brand_id || !newTopic.topic_name || !newTopic.seed_query || !newTopic.brief) {
      setError(new Error("請選擇品牌並填寫主題名稱、種子查詢、摘要"));
      return;
    }
    try {
      const created = await api.createPlacementTopic({
        brand_id: newTopic.brand_id,
        topic_name: newTopic.topic_name,
        seed_query: newTopic.seed_query,
        brief: newTopic.brief,
        product_id: newTopic.product_id || undefined,
      });
      setMessage("主題已建立");
      setNewTopic({ brand_id: "", topic_name: "", seed_query: "", brief: "", product_id: "" });
      await load(created.id);
    } catch (err) {
      setError(err);
    }
  }

  async function saveTopic() {
    if (!selected) return;
    try {
      const updated = await api.updatePlacementTopic(selected.id, {
        topic_name: selected.topic_name,
        seed_query: selected.seed_query,
        brief: selected.brief,
      });
      setMessage("主題已更新");
      await load(updated.id);
    } catch (err) {
      setError(err);
    }
  }

  async function deleteTopic(topic: PlacementTopicData) {
    const label = topic.topic_name || topic.seed_query || topic.id;
    if (!window.confirm(`確定刪除主題「${label}」？\n相關研究地圖、知識圖譜與候選討論一併刪除，無法復原。`)) return;
    setDeletingTopicId(topic.id);
    setError(undefined);
    try {
      await api.deletePlacementTopic(topic.id);
      setMessage(`已刪除主題：${label}`);
      if (selected?.id === topic.id) {
        setSelected(null);
        setGraph(null);
        setPosts([]);
      }
      await load();
    } catch (err) {
      setError(err);
    } finally {
      setDeletingTopicId(null);
    }
  }

  // ---------- Knowledge Graph for Topic ----------
  async function expandGraph() {
    if (!selected || expandingGraph) return;
    setExpandingGraph(true);
    setError(undefined);
    try {
      const res = await api.expandPlacementTopicGraph(selected.id, {
        seed_query: selected.seed_query || "",
        supplemental: true,
        expand_strategy: "hybrid",
      });
      notifyJobCreated({ jobId: res.job_id, label: "知識圖譜擴展" });
      setMessage(`知識圖譜擴展任務建立：${res.job_id}（將產出更詳細的三層節點）`);
      setTimeout(() => load(selected.id), 1500);
    } catch (err) {
      setError(err);
    } finally {
      setExpandingGraph(false);
    }
  }

  // 關鍵：從種子關鍵字 + 品牌產品產生研究地圖（使用現有詳細 prompt，直接產結構化輸出）
  async function generateResearchMap() {
    if (!selected || generatingMap) return;
    setGeneratingMap(true);
    setError(undefined);
    setMessage("正在請 AI 用詳細 prompt 產生研究地圖 + 三層知識圖譜...");
    try {
      const res = await api.expandPlacementTopicGraph(selected.id, {
        seed_query: selected.seed_query || discoveryKeywords || "",
        supplemental: true,
        regenerate_map: true,
        expand_strategy: "hybrid",
      });
      notifyJobCreated({ jobId: res.job_id, label: "研究地圖 + 知識圖譜" });
      setMessage(`研究地圖 + 知識圖譜任務已建立：${res.job_id}。完成後請查看上方分塊呈現的結果。`);
      setTimeout(() => load(selected.id), 2000);
    } catch (err) {
      setError(err);
    } finally {
      setGeneratingMap(false);
    }
  }

  async function reloadGraph() {
    if (!selected) return;
    const g = await api.getPlacementTopicGraph(selected.id);
    setGraph(g);
    setActiveGraphLayer((current) => pickDefaultGraphLayer(g.nodes, current));
  }

  function applyGraphNodeSelection(nodeId: string, selectedForScan: boolean) {
    setGraph((prev) => {
      if (!prev?.nodes) return prev;
      return {
        ...prev,
        nodes: prev.nodes.map((n) => (n.id === nodeId ? { ...n, selected_for_scan: selectedForScan } : n)),
      };
    });
  }

  async function toggleNodeScan(node: KnowledgeGraphNodeData) {
    if (!selected || !graph) return;
    const nextSelected = !node.selected_for_scan;
    applyGraphNodeSelection(node.id, nextSelected);
    try {
      const updated = await api.patchPlacementTopicGraphNodes(selected.id, {
        updates: [{ node_id: node.id, selected_for_scan: nextSelected }],
      });
      setGraph(updated);
      setActiveGraphLayer((current) => pickDefaultGraphLayer(updated.nodes, current));
    } catch (err) {
      applyGraphNodeSelection(node.id, !nextSelected);
      setError(err);
    }
  }

  // === 複製前端找 TA 知識圖譜的操作方法 ===
  async function selectPainNodes() {
    if (!selected) return;
    let currentGraph = graph;
    if (!currentGraph?.nodes?.length) {
      try {
        currentGraph = await api.getPlacementTopicGraph(selected.id);
        setGraph(currentGraph);
        setActiveGraphLayer(pickDefaultGraphLayer(currentGraph.nodes));
      } catch (err) {
        setError(err);
        return;
      }
    }
    const updates = currentGraph.nodes
      .filter((n) => n.node_kind === "pain" || (n.relevance_tags || []).some((t) => /痛|問題|困擾/i.test(t)) || n.product_fit_score >= 60)
      .map((n) => ({ node_id: n.id, selected_for_scan: true }));
    if (updates.length === 0) {
      setMessage("找不到可自動勾選的痛點節點，請手動勾選或先擴展知識圖譜。");
      return;
    }
    try {
      const updated = await api.patchPlacementTopicGraphNodes(selected.id, { updates });
      setGraph(updated);
      setActiveGraphLayer((current) => pickDefaultGraphLayer(updated.nodes, current));
      setMessage(`已勾選 ${updates.length} 個痛點節點。`);
    } catch (err) {
      setError(err);
    }
  }

  function clearNodeSelection() {
    if (!graph || !selected) return;
    const updates = graph.nodes.filter(n => n.selected_for_scan).map(n => ({ node_id: n.id, selected_for_scan: false }));
    if (updates.length === 0) return;
    (async () => {
      try {
        const updated = await api.patchPlacementTopicGraphNodes(selected.id, { updates });
        setGraph(updated);
      } catch (err) { setError(err); }
    })();
  }

  async function saveNodeSelection() {
    if (!graph || !selected) return;
    setSavingNodes(true);
    try {
      // 目前 patch 已在 toggle 處理；這裡可觸發一次全量或提示已即時儲存
      // 為求一致，直接重新載入
      await reloadGraph();
      setMessage('勾選已儲存（用於後續找 TA 發現）');
    } catch (err) {
      setError(err);
    } finally {
      setSavingNodes(false);
    }
  }

  // ---------- 海巡：綜合勾選節點 + 圖譜 + 產品，精簡打 API ----------
  async function startTestPatrolScan() {
    if (!selected) return;
    const plan = buildPatrolPlan({
      topic: selected,
      graph,
      brand: currentBrand,
      manualKeywords: discoveryKeywords,
      maxKeywords: 10,
    });
    if (plan.keywords.length === 0) {
      setError(new Error("請先產生研究地圖，或勾選知識圖譜痛點節點，或手動輸入海巡關鍵字"));
      return;
    }

    const selectedNodeIds = (graph?.nodes || []).filter((n) => n.selected_for_scan).map((n) => n.id);
    const manualKeywords = discoveryKeywords.trim().split(/\s+/).filter(Boolean);

    setTestPatrolScanning(true);
    setError(undefined);
    try {
      const r = await api.startPlacementTopicScan(selected.id, {
        dual_track: true,
        patrol_mode: true,
        test_patrol: true,
        node_ids: selectedNodeIds.length > 0 ? selectedNodeIds : undefined,
        patrol_keywords: manualKeywords.length > 0 ? manualKeywords : undefined,
      });
      notifyJobCreated({ jobId: r.job_id, label: "測試海巡（爬蟲）" });
      watchPatrolJob(r.job_id);
      setMessage(
        `測試海巡任務已建立（${r.job_id}）。使用 Chrome 爬蟲搜尋，關鍵字：${plan.keywords.join(" · ")}`
      );
    } catch (err) {
      setError(err);
    } finally {
      setTestPatrolScanning(false);
    }
  }

  async function startPatrolScan() {
    if (!selected) return;
    const plan = buildPatrolPlan({
      topic: selected,
      graph,
      brand: currentBrand,
      manualKeywords: discoveryKeywords,
      maxKeywords: 10,
    });
    if (plan.keywords.length === 0) {
      setError(new Error("請先產生研究地圖，或勾選知識圖譜痛點節點，或手動輸入海巡關鍵字"));
      return;
    }

    const selectedNodeIds = (graph?.nodes || []).filter((n) => n.selected_for_scan).map((n) => n.id);
    const manualKeywords = discoveryKeywords.trim().split(/\s+/).filter(Boolean);

    setPatrolScanning(true);
    setError(undefined);
    try {
      const r = await api.startPlacementTopicScan(selected.id, {
        dual_track: true,
        patrol_mode: true,
        node_ids: selectedNodeIds.length > 0 ? selectedNodeIds : undefined,
        patrol_keywords: manualKeywords.length > 0 ? manualKeywords : undefined,
      });
      notifyJobCreated({ jobId: r.job_id, label: "海巡找痛點" });
      watchPatrolJob(r.job_id);
      setMessage(
        `海巡任務已建立（${r.job_id}）。預計 ${plan.queryBudget} 次搜尋：優先 Threads 關鍵字，沒結果再用 Search API。` +
          ` 關鍵字：${plan.keywords.join(" · ")}`
      );
    } catch (err) {
      setError(err);
    } finally {
      setPatrolScanning(false);
    }
  }

  async function reloadPosts(overrides?: { recent_7d?: boolean; product_fit_min?: number }) {
    if (!selected) return;
    const recentOnly = overrides?.recent_7d ?? resultsRecentOnly;
    const minFit = overrides?.product_fit_min ?? resultsMinFit;
    const p = await api.listPlacementTopicScanPosts(selected.id, {
      limit: SCAN_POSTS_LIMIT,
      recent_7d: recentOnly || undefined,
      product_fit_min: minFit > 0 ? minFit : undefined,
    });
    setPosts(p.list || []);
    setSelectedPostIds((ids) => ids.filter((id) => (p.list || []).some((post) => post.id === id)));
  }

  function watchPatrolJob(jobId: string) {
    setPatrolJobWatchId(jobId);
  }

  useEffect(() => {
    setSelectedPostIds([]);
    setPickedDraftText({});
    setPickedDraftIndex({});
    setDraftJobByPostId({});
    syncedDraftIdRef.current = {};
    setPublishFeedbackByPostId({});
    setPublishToast(null);
  }, [selected?.id]);

  useEffect(() => {
    if (!publishToast || publishToast.kind !== "success") return;
    const timer = window.setTimeout(() => setPublishToast(null), 12000);
    return () => window.clearTimeout(timer);
  }, [publishToast]);

  useEffect(() => {
    const textUpdates: Record<string, string> = {};
    const indexUpdates: Record<string, number> = {};
    for (const post of posts) {
      const draftId = post.latest_draft?.id;
      if (!draftId || syncedDraftIdRef.current[post.id] === draftId) continue;
      syncedDraftIdRef.current[post.id] = draftId;
      textUpdates[post.id] = post.latest_draft?.drafts?.[0]?.text ?? "";
      indexUpdates[post.id] = 0;
    }
    if (Object.keys(textUpdates).length > 0) {
      setPickedDraftText((prev) => ({ ...prev, ...textUpdates }));
      setPickedDraftIndex((prev) => ({ ...prev, ...indexUpdates }));
    }
  }, [posts]);

  useEffect(() => {
    if (workspaceTab === "results" && selected) {
      void reloadPosts();
    }
  }, [workspaceTab, selected?.id]);

  useEffect(() => {
    if (!patrolJobWatchId || !selected) return;
    const terminal = new Set(["succeeded", "failed", "cancelled", "canceled"]);
    let cancelled = false;

    const poll = async () => {
      try {
        const data = await api.jobs(1);
        const job = (data.list || []).find((j) => j.id === patrolJobWatchId);
        if (!job || terminal.has(job.status)) {
          if (!cancelled) {
            await reloadPosts({ product_fit_min: 0 });
            if (workspaceTab !== "results") {
              setMessage((prev) => prev || "海巡任務已完成，可到「找 TA 結果」查看候選討論。");
            }
            setPatrolJobWatchId(null);
          }
        }
      } catch {
        // keep polling
      }
    };

    void poll();
    const timer = window.setInterval(() => void poll(), 2500);
    return () => {
      cancelled = true;
      window.clearInterval(timer);
    };
  }, [patrolJobWatchId, selected?.id, workspaceTab]);

  function togglePostSelection(postId: string, checked: boolean) {
    setSelectedPostIds((ids) => (checked ? [...ids, postId] : ids.filter((id) => id !== postId)));
  }

  function toggleAllPosts(checked: boolean) {
    setSelectedPostIds(checked ? posts.map((p) => p.id) : []);
  }

  async function deletePosts(postIds: string[]) {
    if (!selected || postIds.length === 0) return;
    if (!window.confirm(`確定刪除 ${postIds.length} 筆候選討論？刪除後無法復原。`)) return;
    setDeletingPosts(true);
    setError(undefined);
    try {
      if (postIds.length === 1) {
        await api.deletePlacementTopicScanPost(selected.id, postIds[0]);
      } else {
        await api.batchDeletePlacementTopicScanPosts(selected.id, postIds);
      }
      setSelectedPostIds((ids) => ids.filter((id) => !postIds.includes(id)));
      await reloadPosts();
      setMessage(`已刪除 ${postIds.length} 筆候選討論`);
    } catch (err) {
      setError(err);
    } finally {
      setDeletingPosts(false);
    }
  }

  const DRAFT_JOB_ACTIVE = useMemo(() => new Set(["running", "queued", "pending", "waiting_worker", "cancel_requested"]), []);
  const DRAFT_JOB_TERMINAL = useMemo(() => new Set(["succeeded", "failed", "cancelled", "canceled"]), []);

  useEffect(() => {
    if (pendingDraftJobIds.length === 0) return;
    let cancelled = false;

    async function pollDraftJobs() {
      try {
        const results = await Promise.all(pendingDraftJobIds.map((id) => api.getJob(id).catch(() => null)));
        const stillActive: string[] = [];
        let shouldReload = false;
        let failedMessage = "";

        for (const job of results) {
          if (!job) continue;
          setDraftJobStatusById((prev) => ({ ...prev, [job.id]: job.status }));
          if (DRAFT_JOB_ACTIVE.has(job.status)) {
            stillActive.push(job.id);
            continue;
          }
          if (DRAFT_JOB_TERMINAL.has(job.status)) {
            shouldReload = true;
            if (job.status === "failed" && job.error) {
              failedMessage = job.error;
            }
          }
        }

        if (!cancelled && shouldReload) {
          await reloadPosts();
          if (failedMessage) {
            setError(new Error(`獲客草稿任務失敗：${failedMessage}`));
          } else if (stillActive.length === 0) {
            setMessage("獲客草稿背景任務已完成，貼文已更新");
          }
        }
        if (!cancelled) {
          setPendingDraftJobIds(stillActive);
          if (stillActive.length === 0) {
            setDraftJobByPostId({});
          }
        }
      } catch {
        // ignore polling errors
      }
    }

    void pollDraftJobs();
    const timer = window.setInterval(() => void pollDraftJobs(), 2000);
    return () => {
      cancelled = true;
      window.clearInterval(timer);
    };
  }, [pendingDraftJobIds, DRAFT_JOB_ACTIVE, DRAFT_JOB_TERMINAL]);

  function enqueueDraftJobs(
    entries: Array<{ jobId: string; postId?: string }>,
    message: string,
  ) {
    const jobIds = entries.map((e) => e.jobId).filter(Boolean);
    if (jobIds.length === 0) {
      setError(new Error("背景任務建立失敗：API 未回傳 job_id"));
      return;
    }
    for (const jobId of jobIds) {
      notifyJobCreated({ jobId, label: "獲客草稿", templateType: "generate-outreach-draft" });
      setDraftJobStatusById((prev) => ({ ...prev, [jobId]: "queued" }));
    }
    setDraftJobByPostId((prev) => {
      const next = { ...prev };
      for (const entry of entries) {
        if (entry.postId && entry.jobId) next[entry.postId] = entry.jobId;
      }
      return next;
    });
    setPendingDraftJobIds((prev) => [...new Set([...prev, ...jobIds])]);
    const preview = jobIds[0]?.slice(0, 8);
    setMessage(`${message}${preview ? `（任務 ${preview}…，請看右下角「任務」）` : ""}`);
  }

  // ---------- Generate & Publish (背景任務，不卡住頁面) ----------
  async function generateDraft(post: ScanPostData) {
    if (!selected) return;
    if (!selectedPersonaId) {
      setError(new Error("請先選擇人設（Voice Persona），才能生成回覆草稿"));
      return;
    }
    setSubmittingDraftJobs(true);
    setError(undefined);
    try {
      const brand = brands.find((b) => b.id === selected.brand_id);
      const matchedProduct = resolveProductForPost({
        brand,
        topic: selected,
        searchTag: post.search_tag,
        postText: post.text,
      });
      const data = await api.startPlacementTopicOutreachDraftJobs(selected.id, {
        scan_post_id: post.id,
        count: 3,
        voice_persona_id: selectedPersonaId,
        product_id: matchedProduct?.id,
        regenerate: (post.latest_draft?.drafts?.length || 0) > 0,
      });
      enqueueDraftJobs(
        (data.jobs || []).map((j) => ({ jobId: j.job_id, postId: post.id })),
        data.message || "獲客草稿已在背景執行",
      );
    } catch (err) {
      setError(err);
    } finally {
      setSubmittingDraftJobs(false);
    }
  }

  async function generateDraftsForSelected() {
    if (!selected || selectedPostIds.length === 0) return;
    if (!selectedPersonaId) {
      setError(new Error("請先選擇人設（Voice Persona），才能批量生成草稿"));
      return;
    }
    setSubmittingDraftJobs(true);
    setError(undefined);
    try {
      const data = await api.startPlacementTopicOutreachDraftJobs(selected.id, {
        scan_post_ids: selectedPostIds,
        count: 3,
        voice_persona_id: selectedPersonaId,
      });
      const jobs = data.jobs || [];
      enqueueDraftJobs(
        jobs.map((j, idx) => ({ jobId: j.job_id, postId: selectedPostIds[idx] })),
        data.message || `已送出 ${selectedPostIds.length} 個獲客草稿任務`,
      );
    } catch (err) {
      setError(err);
    } finally {
      setSubmittingDraftJobs(false);
    }
  }

  async function saveDraftEdit(post: ScanPostData) {
    if (!selected) return;
    const draftId = post.latest_draft?.id;
    if (!draftId) return;
    const draftIndex = pickedDraftIndex[post.id] ?? 0;
    const text = (pickedDraftText[post.id] ?? "").trim();
    if (!text) {
      setError(new Error("草稿內容不可為空"));
      return;
    }
    setSavingDraftPostId(post.id);
    setError(undefined);
    try {
      const updated = await api.updatePlacementTopicOutreachDraft(selected.id, draftId, {
        draft_index: draftIndex,
        text,
      });
      setMessage("草稿已儲存");
      const savedText = updated.drafts?.[draftIndex]?.text || text;
      setPosts((prev) =>
        prev.map((item) =>
          item.id === post.id
            ? {
                ...item,
                latest_draft: updated,
              }
            : item,
        ),
      );
      setPickedDraftText((prev) => ({ ...prev, [post.id]: savedText }));
    } catch (err) {
      setError(err);
    } finally {
      setSavingDraftPostId(null);
    }
  }

  function resolveReplyTargetId(post: ScanPostData) {
    const externalId = (post.external_id || "").trim();
    if (externalId) return externalId;
    const permalink = (post.permalink || "").trim();
    const match = permalink.match(/threads\.(?:com|net)\/@[^/]+\/post\/([^/?#]+)/i);
    return match?.[1]?.trim() || "";
  }

  async function copyDraftForManualReply(text: string, permalink?: string) {
    const payload = text.trim();
    if (!payload) {
      setError(new Error("沒有可複製的草稿內容"));
      return;
    }
    try {
      await navigator.clipboard.writeText(payload);
      setMessage("已複製留言到剪貼簿，請到 Threads 原文手動貼上");
      if (permalink) {
        window.open(permalink, "_blank", "noopener,noreferrer");
      }
    } catch {
      setError(new Error("無法寫入剪貼簿，請手動複製草稿內容"));
    }
  }

  function showPublishFeedback(postId: string, feedback: Omit<PublishFeedback, "at">) {
    const entry: PublishFeedback = { ...feedback, at: Date.now() };
    setPublishFeedbackByPostId((prev) => ({ ...prev, [postId]: entry }));
    setPublishToast(entry);
    if (feedback.kind === "success") {
      setError(undefined);
      setMessage(feedback.message);
    } else if (feedback.kind === "error") {
      setError(new Error(feedback.message));
      setMessage("");
    }
  }

  function scrollPostIntoView(postId: string) {
    window.setTimeout(() => {
      document.querySelector(`[data-post-id="${postId}"]`)?.scrollIntoView({ behavior: "smooth", block: "nearest" });
    }, 80);
  }

  async function publishDraft(post: ScanPostData, text: string) {
    if (!selected) return;
    const replyText = text.trim();
    if (!replyText) {
      const msg = "請先填寫或選擇要發佈的草稿內容";
      showPublishFeedback(post.id, { kind: "error", message: msg });
      scrollPostIntoView(post.id);
      return;
    }
    const replyTargetId = resolveReplyTargetId(post);
    if (!replyTargetId) {
      const msg = "此貼文缺少 Threads 貼文 ID，無法用 API 回覆。請改用手動留言。";
      showPublishFeedback(post.id, { kind: "error", message: msg });
      scrollPostIntoView(post.id);
      return;
    }
    if (!window.confirm("確認透過 Threads API 發送此留言？")) return;
    setPublishingPostId(post.id);
    setError(undefined);
    setMessage("");
    setPublishFeedbackByPostId((prev) => {
      const next = { ...prev };
      delete next[post.id];
      return next;
    });
    try {
      const res = await api.publishPlacementTopicOutreachDraft(selected.id, {
        scan_post_id: post.id,
        text: replyText,
        confirm: true,
      });
      const permalink = res.published_permalink || res.permalink;
      const msg = permalink
        ? "留言已成功發佈到 Threads"
        : (res.message || "獲客留言已成功透過 API 發送");
      showPublishFeedback(post.id, { kind: "success", message: msg, permalink });
      scrollPostIntoView(post.id);
      await reloadPosts();
    } catch (err) {
      const msg = formatPublishError(err);
      showPublishFeedback(post.id, { kind: "error", message: msg });
      scrollPostIntoView(post.id);
    } finally {
      setPublishingPostId(null);
    }
  }

  function selectTopic(t: PlacementTopicData, tab: TopicWorkspaceTab = workspaceTab) {
    setWorkspaceTab(tab);
    void load(t.id);
    requestAnimationFrame(() => {
      workspaceRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
    });
  }

  const currentBrand = brands.find((b) => b.id === selected?.brand_id);
  const selectedPersona = personas.find((p) => p.id === selectedPersonaId);

  const topicProduct = useMemo(
    () => resolvePatrolProduct(currentBrand, selected),
    [currentBrand, selected]
  );

  const patrolPlan = useMemo(
    () =>
      buildPatrolPlan({
        topic: selected,
        graph,
        brand: currentBrand,
        manualKeywords: discoveryKeywords,
        maxKeywords: 10,
      }),
    [selected, graph, currentBrand, discoveryKeywords]
  );

  const patrolChecks = useMemo(() => {
    const hasMap = !!(selected?.research_map?.audience_summary);
    const items: Array<{ key: string; ok: boolean; text: string; hint?: string }> = [
      {
        key: "map",
        ok: hasMap,
        text: hasMap ? "研究地圖已就緒" : "尚未產生研究地圖",
        hint: hasMap ? undefined : "到「主題設定」產生研究地圖",
      },
      {
        key: "nodes",
        ok: patrolPlan.selectedNodeCount > 0,
        text:
          patrolPlan.selectedNodeCount > 0
            ? `已勾選 ${patrolPlan.selectedNodeCount} 個痛點節點`
            : "尚未勾選痛點節點",
        hint:
          patrolPlan.selectedNodeCount > 0
            ? undefined
            : "可點「自動勾選痛點」或到知識圖譜手動勾選",
      },
      {
        key: "keywords",
        ok: patrolPlan.finalItems.length > 0,
        text:
          patrolPlan.finalItems.length > 0
            ? `將搜尋 ${patrolPlan.finalItems.length} 組關鍵字（約 ${patrolPlan.queryBudget} 次查詢）`
            : "尚無可用關鍵字，無法出發",
        hint:
          patrolPlan.finalItems.length > 0
            ? `產品：${patrolPlan.productLabel} · Threads 優先`
            : "完成上方項目，或下方手動輸入關鍵字",
      },
    ];
    if (patrolPlan.isManual) {
      items.push({ key: "manual", ok: true, text: "手動覆寫模式", hint: "已跳過自動綜合邏輯" });
    }
    return items;
  }, [selected, patrolPlan]);

  const filteredTopics = useMemo(() => {
    const q = searchQuery.trim().toLowerCase();
    if (!q) return topics;
    return topics.filter((t) =>
      (t.topic_name || "").toLowerCase().includes(q) ||
      (t.seed_query || "").toLowerCase().includes(q) ||
      (t.brief || "").toLowerCase().includes(q) ||
      (t.brand_display_name || "").toLowerCase().includes(q) ||
      t.id.toLowerCase().includes(q)
    );
  }, [topics, searchQuery]);

  const topicTotalPages = Math.max(1, Math.ceil(filteredTopics.length / TOPICS_PER_PAGE));

  const paginatedTopics = useMemo(() => {
    const start = (topicPage - 1) * TOPICS_PER_PAGE;
    return filteredTopics.slice(start, start + TOPICS_PER_PAGE);
  }, [filteredTopics, topicPage]);

  useEffect(() => {
    setTopicPage(1);
  }, [searchQuery]);

  useEffect(() => {
    if (topicPage > topicTotalPages) {
      setTopicPage(topicTotalPages);
    }
  }, [topicPage, topicTotalPages]);

  return (
    <div className="page-grid">
      <PageTitle
        title="找 TA"
        subtitle="輸入關鍵字 → AI 產生研究地圖 + Search API 查周邊知識 → 找出可被你產品解決痛點的公開討論與客戶（只用公開資訊，同一帳號 5 小時內不重複爬）"
        action={<Button onClick={() => void load()}>重新載入</Button>}
      />
      <ErrorText error={error} />
      {message ? <Card><pre className="hx-text-sm" style={{ whiteSpace: "pre-wrap", margin: 0 }}>{message}</pre></Card> : null}

      {/* 主題列表 + 新增主題 */}
      <Card title="找 TA 主題列表" tone="mint">
        <div style={{ marginBottom: "1rem" }}>
          <Field label="搜尋主題" hint="可輸入名稱、種子關鍵字或品牌">
            <Input
              value={searchQuery || ''}
              onChange={(e) => setSearchQuery(e.target.value)}
              placeholder="搜尋主題名稱、種子關鍵字、品牌或摘要"
            />
          </Field>
        </div>
        <div className="page-grid">
          <div style={{ marginBottom: "1rem" }}>
            <div className="grid-2" style={{ gap: "0.75rem" }}>
              <Field label="關聯品牌">
                <Select value={newTopic.brand_id} onChange={(e) => setNewTopic({ ...newTopic, brand_id: e.target.value })}>
                  <option value="">-- 選擇品牌 --</option>
                  {brands.map((b) => (
                    <option key={b.id} value={b.id}>{b.display_name || b.id}</option>
                  ))}
                </Select>
              </Field>
              <Field label="主題名稱">
                <Input
                  value={newTopic.topic_name}
                  onChange={(e) => setNewTopic({ ...newTopic, topic_name: e.target.value })}
                  placeholder="例如：夜間保養討論"
                />
              </Field>
            </div>
            <div className="grid-2" style={{ gap: "0.75rem", marginTop: "0.5rem" }}>
              <Field label="種子關鍵字 / 痛點描述（打這裡最重要）">
                <Input
                  value={newTopic.seed_query}
                  onChange={(e) => setNewTopic({ ...newTopic, seed_query: e.target.value })}
                  placeholder="例如：化療後 皮膚乾 沐浴乳 推薦"
                />
              </Field>
              <Field label="主題焦點（簡短）">
                <Input
                  value={newTopic.brief}
                  onChange={(e) => setNewTopic({ ...newTopic, brief: e.target.value })}
                  placeholder="為目標客群解決什麼痛點"
                />
              </Field>
            </div>
            <div style={{ marginTop: "0.5rem" }}>
              <Button variant="primary" onClick={() => void createTopic()}>新增找 TA 主題</Button>
              <span className="subtle" style={{ marginLeft: "0.5rem" }}>建立後輸入種子關鍵字 → AI 用詳細 prompt 產生研究地圖 + 知識圖譜</span>
            </div>
          </div>

          <div className="topic-pick-list">
            {filteredTopics.length === 0 && (
              <p className="muted">
                {searchQuery.trim() ? "沒有符合搜尋條件的主題。" : "尚未建立主題。按「新增找 TA 主題」開始。"}
              </p>
            )}
            {paginatedTopics.map((t) => {
              const hasMap = !!(t.research_map && t.research_map.audience_summary);
              const isActive = t.id === selected?.id;
              return (
                <div
                  key={t.id}
                  className={`topic-pick-item ${isActive ? "topic-pick-item--active" : ""}`}
                  role="button"
                  tabIndex={0}
                  onClick={() => selectTopic(t, workspaceTab)}
                  onKeyDown={(e) => {
                    if (e.key === "Enter" || e.key === " ") {
                      e.preventDefault();
                      selectTopic(t, workspaceTab);
                    }
                  }}
                >
                  <div className="topic-pick-head">
                    <div>
                      <strong className="hx-text-base">{t.topic_name || t.seed_query || t.id}</strong>
                      {t.brand_display_name && <p className="hx-text-xs muted" style={{ margin: "0.2rem 0 0" }}>品牌：{t.brand_display_name}</p>}
                      {t.seed_query && <p className="hx-text-xs muted" style={{ margin: "0.15rem 0 0" }}>種子：{t.seed_query}</p>}
                    </div>
                    <Badge variant={hasMap ? "success" : "warning"}>{hasMap ? "研究地圖已就緒" : "研究地圖待產生"}</Badge>
                  </div>
                  {t.brief && <p className="muted hx-text-sm" style={{ margin: "0.4rem 0 0" }}>{shortText(t.brief, 100)}</p>}
                  <div className="topic-pick-actions" onClick={(e) => e.stopPropagation()}>
                    <Button variant={isActive ? "primary" : "soft"} onClick={() => selectTopic(t, "settings")}>主題設定</Button>
                    <Button variant="soft" onClick={() => selectTopic(t, "map")}>研究地圖</Button>
                    <Button variant="soft" onClick={() => selectTopic(t, "graph")}>知識圖譜</Button>
                    <Button variant="soft" onClick={() => selectTopic(t, "patrol")}>海巡</Button>
                    <Button variant="soft" onClick={() => selectTopic(t, "results")}>找 TA 結果</Button>
                    <Button
                      variant="danger"
                      loading={deletingTopicId === t.id}
                      disabled={deletingTopicId !== null && deletingTopicId !== t.id}
                      onClick={() => void deleteTopic(t)}
                    >
                      刪除
                    </Button>
                  </div>
                </div>
              );
            })}
          </div>

          {filteredTopics.length > 0 && (
            <div className="hx-pager">
              <span className="hx-pager-meta">
                第 {topicPage} / {topicTotalPages} 頁 · 共 {filteredTopics.length} 個主題
                {searchQuery.trim() ? ` · 搜尋「${searchQuery.trim()}」` : ""}
              </span>
              <div className="button-row">
                <Button variant="soft" disabled={topicPage <= 1} onClick={() => setTopicPage((p) => Math.max(1, p - 1))}>
                  上一頁
                </Button>
                <Button variant="soft" disabled={topicPage >= topicTotalPages} onClick={() => setTopicPage((p) => Math.min(topicTotalPages, p + 1))}>
                  下一頁
                </Button>
              </div>
            </div>
          )}
        </div>
      </Card>

      {selected && (
        <div ref={workspaceRef}>
          <Card
            title={`工作區：${selected.topic_name || selected.seed_query || selected.id}`}
            tone="sky"
          >
            <div className="hx-flex-wrap" style={{ marginBottom: "0.75rem" }}>
              <Badge variant="brand">{currentBrand?.display_name || selected.brand_id}</Badge>
              {selected.seed_query ? <Badge variant="neutral">種子：{shortText(selected.seed_query, 36)}</Badge> : null}
              {loading ? <Badge variant="warning">載入中…</Badge> : null}
            </div>

            <div className="hx-tabs" role="tablist" aria-label="找 TA 工作區分頁">
              {WORKSPACE_TABS.map((tab) => (
                <button
                  key={tab.key}
                  type="button"
                  role="tab"
                  aria-selected={workspaceTab === tab.key}
                  className={`hx-tab ${workspaceTab === tab.key ? "hx-tab--active" : ""}`}
                  onClick={() => setWorkspaceTab(tab.key)}
                >
                  {tab.label}
                </button>
              ))}
            </div>

            {workspaceTab === "settings" && (
          <div className="patrol-tab-panel hx-flat-block hx-flat-block--form" style={{ maxWidth: "720px" }}>
              <div className="grid-2">
                <Field label="主題名稱">
                  <Input value={selected.topic_name || ""} onChange={(e) => setSelected({ ...selected, topic_name: e.target.value })} />
                </Field>
                <Field label="種子關鍵字 / 痛點">
                  <Input value={selected.seed_query || ""} onChange={(e) => setSelected({ ...selected, seed_query: e.target.value })} />
                </Field>
              </div>
              <Field label="摘要">
                <Textarea value={selected.brief || ""} onChange={(e) => setSelected({ ...selected, brief: e.target.value })} rows={2} />
              </Field>
              <div className="button-row">
                <Button onClick={() => void saveTopic()}>儲存主題</Button>
                <Button variant="primary" loading={generatingMap} onClick={() => void generateResearchMap()}>AI 產生研究地圖 + 知識圖譜</Button>
                <Button loading={expandingGraph} onClick={() => void expandGraph()}>擴展知識圖譜</Button>
                <Button onClick={() => void reloadPosts()}>重新載入候選貼文</Button>
                <Button
                  variant="danger"
                  loading={deletingTopicId === selected.id}
                  onClick={() => void deleteTopic(selected)}
                >
                  刪除此主題
                </Button>
                <span className="subtle">關聯品牌：{currentBrand?.display_name || selected.brand_id}</span>
              </div>
              <p className="subtle hx-text-sm" style={{ margin: 0 }}>
                輸入種子關鍵字後點「AI 產生研究地圖 + 知識圖譜」，會產出結構化的受眾、問題、支柱與三層知識圖譜。
              </p>
          </div>
            )}

            {workspaceTab === "map" && (
          <div className="patrol-tab-panel hx-flat-block hx-text-sm">
            {!selected?.research_map || !selected.research_map.audience_summary ? (
              <div>
                <p className="muted">尚未產生。請到「主題設定」點「AI 產生研究地圖 + 知識圖譜」。</p>
                <Button variant="primary" loading={generatingMap} onClick={() => void generateResearchMap()}>立即產生研究地圖與知識圖譜</Button>
              </div>
            ) : (
              <>
                <section className="hx-flat-section hx-flat-section--mint">
                  <h3 className="hx-section-title">受眾摘要（TA）</h3>
                  <p style={{ margin: 0, lineHeight: 1.65 }}>{selected.research_map.audience_summary}</p>
                </section>

                <section className="hx-flat-section hx-flat-section--sky">
                  <h3 className="hx-section-title">內容目標 / 置入時機</h3>
                  <p style={{ margin: 0, lineHeight: 1.65 }}>{selected.research_map.content_goal}</p>
                </section>

                {selected.research_map.questions && selected.research_map.questions.length > 0 && (
                  <section className="hx-flat-section hx-flat-section--lavender">
                    <h3 className="hx-section-title">受眾會問什麼（痛點導向問題）</h3>
                    {selected.research_map.questions.map((q, i) => (
                      <div key={i} className="hx-flat-chip">{q}</div>
                    ))}
                  </section>
                )}

                {selected.research_map.pillars && selected.research_map.pillars.length > 0 && (
                  <section className="hx-flat-section hx-flat-section--peach">
                    <h3 className="hx-section-title">內容支柱方向</h3>
                    <div className="hx-flex-wrap">
                      {selected.research_map.pillars.map((p, i) => (
                        <Badge key={i} variant="neutral">{p}</Badge>
                      ))}
                    </div>
                  </section>
                )}

                {selected.research_map.exclusions && selected.research_map.exclusions.length > 0 && (
                  <section className="hx-flat-section hx-flat-section--muted">
                    <h3 className="hx-section-title">排除項目</h3>
                    <div className="hx-flex-wrap">
                      {selected.research_map.exclusions.map((e, i) => (
                        <Badge key={i} variant="neutral">{e}</Badge>
                      ))}
                    </div>
                  </section>
                )}

                {selected.research_map.patrol_keywords && selected.research_map.patrol_keywords.length > 0 && (
                  <section className="hx-flat-section hx-flat-section--brand">
                    <h3 className="hx-section-title">建議搜尋關鍵字</h3>
                    <div className="hx-flex-wrap">
                      {selected.research_map.patrol_keywords.map((k, i) => (
                        <Badge key={i} variant="brand">{k}</Badge>
                      ))}
                    </div>
                  </section>
                )}

                <div className="button-row">
                  <Button loading={generatingMap} onClick={() => void generateResearchMap()}>重新產生研究地圖 + 知識圖譜</Button>
                  <Button variant="soft" loading={expandingGraph} onClick={() => void expandGraph()}>只擴展知識圖譜</Button>
                </div>
              </>
            )}
          </div>
            )}

            {workspaceTab === "graph" && (
          <div className="patrol-tab-panel hx-flat-block">
              <div className="button-row" style={{ flexWrap: "wrap", gap: "0.5rem" }}>
                <Button loading={expandingGraph} onClick={() => void expandGraph()}>擴展 / 補充知識圖譜</Button>
                <Button onClick={() => void reloadGraph()}>重新載入圖譜</Button>
                {graph && graph.nodes?.length > 0 && (
                  <>
                    <Button variant="soft" onClick={() => void selectPainNodes()}>勾選痛點節點</Button>
                    <Button variant="ghost" onClick={() => clearNodeSelection()}>清除勾選</Button>
                    <Button onClick={() => void saveNodeSelection()} disabled={savingNodes}>
                      {savingNodes ? "儲存中…" : "儲存勾選（用於海巡）"}
                    </Button>
                  </>
                )}
              </div>

              {graph && graph.nodes && graph.nodes.length > 0 ? (
                <>
                  <div className="hx-stat-strip">
                    <Badge variant="brand">種子：{selected?.seed_query || ""}</Badge>
                    <Badge variant="neutral">{graph.nodes.length} 個節點</Badge>
                    {graph.brave_sources && graph.brave_sources.length > 0 && (
                      <Badge variant="sky">{graph.brave_sources.length} 個參考連結</Badge>
                    )}
                    <Badge variant={(graph as any).pain_tag_count >= 8 ? "success" : "warning"}>
                      痛點候選 {(graph as any).pain_tag_count || 0}
                    </Badge>
                    <Badge variant="neutral">已勾選 {graph.nodes.filter((n) => n.selected_for_scan).length}</Badge>
                  </div>

                  <div className="button-row">
                    {[0, 1, 2].map((l) => {
                      const layerNodes = graph.nodes.filter((n) => n.layer === l);
                      const label = l === 0 ? "核心層 (L0)" : l === 1 ? "延伸層 (L1)" : "周邊層 (L2)";
                      const isActive = activeGraphLayer === l;
                      return (
                        <Button
                          key={l}
                          variant={isActive ? "primary" : "soft"}
                          onClick={() => setActiveGraphLayer(l)}
                        >
                          {label} ({layerNodes.length})
                        </Button>
                      );
                    })}
                  </div>

                  <section className="hx-flat-section hx-flat-section--peach">
                    <h3 className="hx-section-title">
                      {activeGraphLayer === 0 ? "核心層 (L0)" : activeGraphLayer === 1 ? "延伸層 (L1)" : "周邊層 (L2)"}
                    </h3>
                    <p className="hx-section-hint">
                      延伸知識由內而外擴展（核心 → 直接相關 → 周邊情境）。勾選痛點節點後點「儲存勾選」供海巡使用。
                    </p>
                    <div className="hx-node-flat-list">
                      {graph.nodes.filter((n) => n.layer === activeGraphLayer).length === 0 ? (
                        <p className="hx-flat-empty">這一層目前沒有節點，請切換上方 L0 / L1 / L2 分頁。</p>
                      ) : null}
                      {graph.nodes
                        .filter((n) => n.layer === activeGraphLayer)
                        .sort((a, b) => b.product_fit_score - a.product_fit_score)
                        .map((node) => {
                          const evidence = (node.evidence || []).filter((e) => e.url);
                          return (
                            <div
                              key={node.id}
                              className={`hx-node-flat ${node.selected_for_scan ? "hx-node-flat--picked" : ""}`}
                            >
                              <div style={{ display: "flex", alignItems: "center", gap: "0.5rem", marginBottom: "0.4rem", flexWrap: "wrap" }}>
                                <input
                                  type="checkbox"
                                  checked={!!node.selected_for_scan}
                                  onChange={() => void toggleNodeScan(node)}
                                />
                                <strong className="hx-text-base">{node.label}</strong>
                                <Badge variant="neutral">{node.node_kind || node.type}</Badge>
                                <Badge variant={node.product_fit_score >= 70 ? "success" : node.product_fit_score >= 40 ? "warning" : "neutral"}>
                                  契合 {node.product_fit_score}
                                </Badge>
                              </div>

                              {node.relation && (
                                <p className="hx-text-sm" style={{ margin: "0.35rem 0", lineHeight: 1.55 }}>
                                  <strong>脈絡：</strong> {node.relation}
                                </p>
                              )}
                              {node.placement_value && (
                                <p className="hx-callout">
                                  <strong>置入價值：</strong> {node.placement_value}
                                </p>
                              )}

                              {((node.relevance_tags && node.relevance_tags.length) || (node.recency_tags && node.recency_tags.length)) && (
                                <div style={{ marginTop: "0.35rem" }}>
                                  <div className="hx-text-xs muted" style={{ marginBottom: "0.2rem" }}>搜尋建議 / 標籤</div>
                                  <div className="hx-flex-wrap">
                                    {(node.relevance_tags || []).slice(0, 4).map((q: string, i: number) => (
                                      <Badge key={"r" + i} variant="neutral">{q}</Badge>
                                    ))}
                                    {(node.recency_tags || []).slice(0, 3).map((q: string, i: number) => (
                                      <Badge key={"c" + i} variant="brand">{q}</Badge>
                                    ))}
                                  </div>
                                </div>
                              )}

                              {evidence.length > 0 && (
                                <div className="hx-text-xs muted" style={{ marginTop: "0.45rem" }}>
                                  <span className="font-semibold">來源：</span>
                                  {evidence.slice(0, 3).map((ev: any, i: number) => (
                                    <a
                                      key={i}
                                      href={ev.url}
                                      target="_blank"
                                      rel="noreferrer"
                                      className="hx-link-chip"
                                    >
                                      {ev.url ? safeUrlHostname(ev.url) : "連結"}
                                    </a>
                                  ))}
                                  {evidence.length > 3 && <span>+{evidence.length - 3}</span>}
                                </div>
                              )}
                            </div>
                          );
                        })}
                    </div>
                  </section>
                </>
              ) : (
                <p className="muted">尚未產生知識圖譜。請先產生研究地圖，然後點「擴展知識圖譜」產生三層延伸知識。</p>
              )}
          </div>
            )}

            {workspaceTab === "patrol" && (
          <div className="patrol-tab-panel hx-flat-block">
              <section className="hx-flat-section hx-flat-section--brand">
                <h3 className="hx-section-title">出發前確認</h3>
                <ul className="hx-patrol-checklist">
                  {patrolChecks.map((check) => (
                    <li key={check.key} className={`hx-patrol-check hx-patrol-check--${check.ok ? "ok" : "warn"}`}>
                      <span className="hx-patrol-check-icon" aria-hidden="true">{check.ok ? "✓" : "!"}</span>
                      <div>
                        <strong>{check.text}</strong>
                        {check.hint ? <p className="hx-patrol-check-hint">{check.hint}</p> : null}
                      </div>
                    </li>
                  ))}
                </ul>
              </section>

              <section className="hx-flat-section hx-flat-section--lavender">
                <h3 className="hx-section-title">將送出的搜尋關鍵字（依相關性精選）</h3>
                <p className="hx-section-hint" style={{ marginTop: 0 }}>
                  研究地圖與圖譜只是候選來源。實際搜尋會依管道自動裁切：Threads API 用短核心詞（痛點＋品類）；Search API 近期軌用短詞＋請問，相關軌用稍長的求助句以提高精準度。
                </p>
                {patrolPlan.finalItems.length > 0 ? (
                  <div className="hx-patrol-kw-list">
                    {patrolPlan.finalItems.map((item, idx) => (
                      <div key={`${item.keyword}-${idx}`} className="hx-patrol-kw">
                        <span className="hx-patrol-kw-num">{idx + 1}</span>
                        <span className="hx-patrol-kw-text">{item.keyword}</span>
                        <Badge variant="neutral">{patrolOriginLabel(item.origin)}</Badge>
                      </div>
                    ))}
                  </div>
                ) : (
                  <p className="hx-flat-empty">完成上方確認項目後，關鍵字會出現在這裡。</p>
                )}
              </section>

              {!patrolPlan.isManual && patrolPlan.groups.some((g) => g.items.length > 0) ? (
                <details className="hx-patrol-details">
                  <summary>追溯：候選來源（不等於最終送出順序）</summary>
                  <div className="hx-patrol-details-body">
                    {patrolPlan.groups.map((group) =>
                      group.items.length > 0 ? (
                        <div key={group.key} className={`hx-flat-subsection ${patrolGroupTone(group.key)}`}>
                          <h4 className="hx-subsection-title">{group.title}</h4>
                          <div className="hx-flex-wrap">
                            {group.items.map((item, idx) => (
                              <Badge key={`${group.key}-${item.keyword}-${idx}`} variant="neutral">
                                {item.keyword}
                              </Badge>
                            ))}
                          </div>
                        </div>
                      ) : null
                    )}
                  </div>
                </details>
              ) : null}

              <Field label="改用自己的關鍵字（選填）" hint="空白時由系統依產品與研究地圖自動精選；有填則作為加權候選">
                <Input
                  value={discoveryKeywords}
                  onChange={(e) => setDiscoveryKeywords(e.target.value)}
                  placeholder="空白表示使用自動精選"
                />
              </Field>

              <div className="button-row">
                <Button
                  variant="primary"
                  loading={patrolScanning}
                  onClick={() => void startPatrolScan()}
                  disabled={patrolPlan.keywords.length === 0}
                  title="使用 Threads API + Web Search 正式海巡"
                >
                  開始海巡（API）
                </Button>
                {devModeEnabled ? (
                  <Button
                    variant="soft"
                    loading={testPatrolScanning}
                    onClick={() => void startTestPatrolScan()}
                    disabled={patrolPlan.keywords.length === 0 || !browserConnected}
                    title={
                      browserConnected
                        ? "使用 Chrome 爬蟲測試海巡（不走 Threads API / Search API）"
                        : "請先到 Threads 帳號頁同步 Chrome Extension Session"
                    }
                  >
                    測試海巡（爬蟲）
                  </Button>
                ) : null}
                {patrolPlan.selectedNodeCount === 0 ? (
                  <Button variant="soft" onClick={() => void selectPainNodes()}>自動勾選痛點</Button>
                ) : null}
                {!selected?.research_map?.audience_summary ? (
                  <Button variant="soft" loading={generatingMap} onClick={() => void generateResearchMap()}>
                    產生研究地圖
                  </Button>
                ) : null}
              </div>
              {devModeEnabled && !browserConnected ? (
                <p className="muted hx-text-sm" style={{ margin: 0 }}>
                  測試海巡需先在「設定」開啟開發模式，並到「Threads 帳號」同步 Chrome Extension Session。
                </p>
              ) : null}
          </div>
            )}

            {workspaceTab === "results" && (
          <div className="patrol-tab-panel hx-flat-block">
              <section className="hx-flat-section hx-flat-section--sky">
                <h3 className="hx-section-title">在此閱讀、判斷、回覆</h3>
                <p className="hx-section-hint">
                  完整討論與留言都留在這裡。只保留<strong>有明確需求／痛點</strong>、<strong>你的產品能解決</strong>、且<strong>品類不錯配</strong>的討論；海巡會<strong>先搜近 7 天</strong>，某關鍵字結果不足時自動往<strong>近 14 天、近 30 天</strong>擴搜。
                  {topicProduct
                    ? ` 主題預設產品：${topicProduct.label}（每則貼文會依內容自動推薦最適合的 SKU）`
                    : " 請先在品牌綁定產品與 match tags。"}
                </p>
                <div className="hx-ta-filters">
                  <label className="hx-ta-filter">
                    <input type="checkbox" checked={resultsRecentOnly} onChange={(e) => setResultsRecentOnly(e.target.checked)} />
                    只顯示近 7 天
                  </label>
                  <label className="hx-ta-filter">
                    產品契合 ≥
                    <select
                      className="select"
                      value={resultsMinFit}
                      onChange={(e) => setResultsMinFit(Number(e.target.value))}
                      style={{ width: "5.5rem", minHeight: "2.2rem" }}
                    >
                      {[0, 35, 40, 48, 55, 65].map((n) => (
                        <option key={n} value={n}>{n === 0 ? "全部" : n}</option>
                      ))}
                    </select>
                  </label>
                  <Button variant="soft" onClick={() => void reloadPosts()}>套用篩選</Button>
                </div>
                {resultsRecentOnly || resultsMinFit > 0 ? (
                  <p className="muted hx-text-sm" style={{ margin: "0.5rem 0 0" }}>
                    目前套用篩選，較低契合或較舊的候選可能暫時看不到。將「產品契合」設為「全部」可顯示海巡存下的所有結果。
                  </p>
                ) : null}
                <Field
                  label="回覆人設 (Voice Persona)"
                  hint={
                    personas.length === 0
                      ? "尚無人設，請先到「人設」頁建立"
                      : "草稿會依此人設語氣生成；發佈留言一律走 Threads API"
                  }
                >
                  <Select
                    value={selectedPersonaId}
                    onChange={(e) => {
                      const nextId = e.target.value;
                      setSelectedPersonaId(nextId);
                      persistPatrolVoicePersonaId(nextId);
                    }}
                    style={{ maxWidth: "360px" }}
                    disabled={personas.length === 0}
                  >
                    {personas.length === 0 && <option value="">尚未有人設</option>}
                    {personas.map((p) => (
                      <option key={p.id} value={p.id}>{p.display_name || p.id}</option>
                    ))}
                  </Select>
                </Field>
              </section>

              <div className="hx-ta-toolbar">
                <label className="hx-ta-select-all">
                  <input
                    type="checkbox"
                    checked={posts.length > 0 && selectedPostIds.length === posts.length}
                    onChange={(e) => toggleAllPosts(e.target.checked)}
                    disabled={posts.length === 0}
                  />
                  全選（{selectedPostIds.length}/{posts.length}）
                </label>
                <Button
                  variant="primary"
                  loading={submittingDraftJobs}
                  disabled={selectedPostIds.length === 0 || !selectedPersonaId}
                  onClick={() => void generateDraftsForSelected()}
                >
                  批量生成草稿（背景）
                </Button>
                {pendingDraftJobIds.length > 0 ? (
                  <span className="muted hx-text-sm">草稿任務進行中：{pendingDraftJobIds.length}（可離開此頁）</span>
                ) : null}
                <Button
                  variant="danger"
                  loading={deletingPosts}
                  disabled={selectedPostIds.length === 0}
                  onClick={() => void deletePosts(selectedPostIds)}
                >
                  刪除選取
                </Button>
                <Button variant="soft" onClick={() => void reloadPosts()}>重新載入</Button>
              </div>

              {posts.length === 0 ? (
                <p className="hx-flat-empty">
                  {resultsRecentOnly || resultsMinFit > 0
                    ? "篩選後沒有符合的候選。試著關閉「只顯示近 7 天」或將產品契合改為「全部」。"
                    : patrolJobWatchId
                      ? "海巡進行中，完成後會自動載入結果；也可稍後按「重新載入」。"
                      : "目前沒有候選討論。請到「海巡」啟動搜尋。"}
                </p>
              ) : (
                <div className="hx-ta-result-list">
                  {posts.map((post) => {
                    const node = graphNodeForPost(graph, post);
                    const postProduct = resolveProductForPost({
                      brand: currentBrand,
                      topic: selected,
                      searchTag: post.search_tag,
                      postText: post.text,
                    });
                    const opportunity = placementOpportunity(
                      post,
                      node,
                      postProduct?.label || "",
                      postProduct?.match_tags || []
                    );
                    const drafts = post.latest_draft?.drafts || [];
                    const draftIndex = pickedDraftIndex[post.id] ?? 0;
                    const pickedText = pickedDraftText[post.id] ?? drafts[draftIndex]?.text ?? drafts[0]?.text ?? "";
                    const draftDirty =
                      drafts.length > 0 &&
                      pickedText.trim() !== (drafts[draftIndex]?.text ?? drafts[0]?.text ?? "").trim();
                    const postDraftJobId = draftJobByPostId[post.id];
                    const postDraftJobStatus = postDraftJobId ? draftJobStatusById[postDraftJobId] : "";
                    const postDraftJobActive = postDraftJobStatus ? DRAFT_JOB_ACTIVE.has(postDraftJobStatus) : false;
                    const replyTargetId = resolveReplyTargetId(post);
                    const canPublishViaApi = Boolean(replyTargetId && pickedText.trim());
                    const publishFeedback = publishFeedbackByPostId[post.id];
                    const isSelected = selectedPostIds.includes(post.id);
                    const categoryMismatch = looksCategoryMismatch(post.text, postProduct?.label || "", postProduct?.match_tags || []);
                    const productDiffersFromTopicDefault =
                      Boolean(topicProduct && postProduct && topicProduct.id !== postProduct.id);
                    const isRecent =
                      post.priority === "gold" ||
                      (post.recency_days != null && post.recency_days > 0 && post.recency_days <= 7) ||
                      post.priority === "recent";
                    return (
                      <article
                        key={post.id}
                        data-post-id={post.id}
                        className={`hx-ta-result${isSelected ? " hx-ta-result--selected" : ""}${categoryMismatch ? " hx-ta-result--risky" : ""}`}
                      >
                        <header className="hx-ta-result-head">
                          <input
                            type="checkbox"
                            checked={isSelected}
                            onChange={(e) => togglePostSelection(post.id, e.target.checked)}
                          />
                          <div className="hx-ta-result-meta">
                            <div className="hx-ta-result-author-row">
                              <strong className="hx-text-base">@{post.author || "未知作者"}</strong>
                              {post.posted_at ? <span className="hx-ta-date">{post.posted_at}</span> : null}
                              {!isRecent && resultsRecentOnly ? <Badge variant="warning">較舊</Badge> : null}
                            </div>
                            <div className="hx-flex-wrap" style={{ marginTop: "0.45rem" }}>
                              <Badge variant={post.solved_by_product ? "success" : "warning"}>
                                {post.solved_by_product ? "產品可解決" : "待評估"}
                              </Badge>
                              <Badge variant="brand">巡樓 {post.placement_score ?? 0}</Badge>
                              <Badge variant={post.product_fit_score >= 55 ? "success" : "neutral"}>
                                產品契合 {post.product_fit_score ?? 0}
                              </Badge>
                              <Badge variant="sky">{outreachStatusLabel(post.outreach_status)}</Badge>
                              {postDraftJobActive ? (
                                <Badge variant="brand">
                                  草稿生成中{postDraftJobStatus ? ` · ${jobStatusLabel(postDraftJobStatus)}` : ""}
                                </Badge>
                              ) : null}
                              {categoryMismatch ? <Badge variant="danger">品類可能不符</Badge> : null}
                              {productDiffersFromTopicDefault ? <Badge variant="sky">依貼文推薦</Badge> : null}
                            </div>
                            <dl className="hx-ta-result-facts">
                              <div><dt>命中關鍵字</dt><dd>{post.search_tag || "—"}</dd></div>
                              <div><dt>搜尋軌道</dt><dd>{post.query_dimension === "recency" ? `近 ${post.recency_days || 7} 天` : post.query_dimension || "—"}</dd></div>
                              <div><dt>建議置入</dt><dd>{postProduct?.label || "未綁定產品"}</dd></div>
                            </dl>
                          </div>
                        </header>

                        <div className="hx-ta-result-body">
                          <blockquote className="hx-ta-result-quote">{post.text}</blockquote>
                          {post.replies && post.replies.length > 0 && (
                            <div className="hx-ta-replies">
                              <div className="hx-text-xs muted" style={{ marginBottom: "0.35rem" }}>
                                底下留言（{post.replies.length}）— 在這裡看完再決定怎麼回
                              </div>
                              {post.replies.map((reply, idx) => (
                                <div key={`${post.id}-reply-${idx}`} className="hx-ta-reply">
                                  <strong>@{reply.author || "匿名"}</strong>
                                  {reply.like_count != null ? <span className="muted"> · {reply.like_count} 讚</span> : null}
                                  <p>{reply.text}</p>
                                </div>
                              ))}
                            </div>
                          )}
                        </div>

                        <section className="hx-ta-fit">
                          <h4 className="hx-subsection-title">置入建議</h4>
                          <div className="hx-flex-wrap" style={{ marginBottom: "0.45rem" }}>
                            <Badge variant="brand">
                              建議產品：{postProduct?.label || "未綁定產品"}
                            </Badge>
                            {node?.label ? <Badge variant="neutral">痛點：{node.label}</Badge> : null}
                            {node?.product_fit_score != null ? (
                              <Badge variant={node.product_fit_score >= 70 ? "success" : "warning"}>
                                節點契合 {node.product_fit_score}
                              </Badge>
                            ) : null}
                          </div>
                          <p className="hx-text-sm" style={{ margin: 0, lineHeight: 1.6 }}>{opportunity}</p>
                        </section>

                        {drafts.length > 0 && (
                          <section className="hx-ta-drafts">
                            <h4 className="hx-subsection-title">
                              回覆草稿（依「{selectedPersona?.display_name || "未選人設"}」語氣，可編輯後以 API 發佈）
                            </h4>
                            {post.latest_draft?.create_at ? (
                              <p className="hx-text-xs muted" style={{ margin: "0 0 0.35rem" }}>
                                最新生成：{formatNano(post.latest_draft.create_at)}
                                {post.latest_draft.id ? ` · #${post.latest_draft.id.slice(0, 8)}` : ""}
                              </p>
                            ) : null}
                            {post.latest_draft?.reason ? (
                              <p className="hx-section-hint">{post.latest_draft.reason}</p>
                            ) : null}
                            {postProduct?.placement_url ? (
                              <p className="hx-section-hint">
                                置入網址：
                                <a href={postProduct.placement_url} target="_blank" rel="noreferrer" className="hx-link-chip">
                                  {postProduct.placement_url}
                                </a>
                                （AI 會嘗試自然帶入至少一則草稿）
                              </p>
                            ) : null}
                            <div className="hx-ta-draft-options">
                              {drafts.map((draft, idx) => (
                                <label key={`${post.id}-draft-${idx}`} className={`hx-ta-draft-option${draftIndex === idx ? " hx-ta-draft-option--active" : ""}`}>
                                  <input
                                    type="radio"
                                    name={`draft-${post.id}`}
                                    checked={draftIndex === idx}
                                    onChange={() => {
                                      setPickedDraftIndex((prev) => ({ ...prev, [post.id]: idx }));
                                      setPickedDraftText((prev) => ({ ...prev, [post.id]: draft.text }));
                                    }}
                                  />
                                  <div>
                                    {draft.angle ? <Badge variant="neutral">{draft.angle}</Badge> : null}
                                    {draft.rationale ? <p className="hx-text-xs muted">{draft.rationale}</p> : null}
                                  </div>
                                </label>
                              ))}
                            </div>
                            <div style={{ marginTop: "0.75rem" }}>
                              <Field label="草稿內容（可直接修改，Enter 換行）">
                                <Textarea
                                  className="textarea--draft"
                                  value={pickedText}
                                  rows={6}
                                  onChange={(e) => setPickedDraftText((prev) => ({ ...prev, [post.id]: e.target.value }))}
                                />
                              </Field>
                              <div className="hx-ta-draft-preview">
                                <p className="hx-text-xs muted" style={{ margin: 0 }}>發佈預覽（Threads 會保留換行）</p>
                                <p className="hx-ta-draft-preview-text">{pickedText || "（空白）"}</p>
                              </div>
                            </div>
                            {draftDirty ? (
                              <div className="button-row" style={{ marginTop: "0.5rem" }}>
                                <Button
                                  variant="soft"
                                  loading={savingDraftPostId === post.id}
                                  onClick={() => void saveDraftEdit(post)}
                                >
                                  儲存修改
                                </Button>
                              </div>
                            ) : null}
                          </section>
                        )}

                        {publishingPostId === post.id ? (
                          <div className="hx-publish-feedback hx-publish-feedback--info" role="status">
                            正在透過 Threads API 發送留言，請稍候…
                          </div>
                        ) : null}
                        {publishFeedback ? (
                          <div
                            className={`hx-publish-feedback hx-publish-feedback--${publishFeedback.kind}`}
                            role={publishFeedback.kind === "error" ? "alert" : "status"}
                          >
                            <strong>
                              {publishFeedback.kind === "success"
                                ? "發佈成功"
                                : publishFeedback.kind === "error"
                                  ? "發佈失敗"
                                  : "提示"}
                            </strong>
                            <p>{publishFeedback.message}</p>
                            {publishFeedback.permalink ? (
                              <a href={publishFeedback.permalink} target="_blank" rel="noreferrer" className="hx-link-chip">
                                查看已發佈回覆
                              </a>
                            ) : null}
                          </div>
                        ) : null}

                        <footer className="hx-ta-result-actions">
                          <Button
                            variant="primary"
                            loading={submittingDraftJobs || postDraftJobActive}
                            disabled={!selectedPersonaId || postDraftJobActive}
                            onClick={() => void generateDraft(post)}
                          >
                            {postDraftJobActive
                              ? "草稿生成中…"
                              : drafts.length > 0
                                ? "重新生成草稿（背景）"
                                : "生成草稿（背景）"}
                          </Button>
                          {pickedText ? (
                            <>
                              <Button
                                variant="soft"
                                loading={publishingPostId === post.id}
                                disabled={!canPublishViaApi || publishingPostId === post.id}
                                onClick={() => void publishDraft(post, pickedText)}
                              >
                                {publishingPostId === post.id ? "發佈中…" : "發佈回覆（API）"}
                              </Button>
                              <Button
                                variant="soft"
                                onClick={() => void copyDraftForManualReply(pickedText, post.permalink)}
                              >
                                複製並開啟原文
                              </Button>
                            </>
                          ) : null}
                          {pickedText && !replyTargetId ? (
                            <span className="muted hx-text-sm">此貼文缺少 Threads ID，請改用手動留言</span>
                          ) : null}
                          <Button variant="danger" onClick={() => void deletePosts([post.id])}>刪除</Button>
                          {post.permalink ? (
                            <a href={post.permalink} target="_blank" rel="noreferrer" className="hx-link-chip">
                              對照 Threads 原文
                            </a>
                          ) : null}
                          {post.published_permalink ? (
                            <a href={post.published_permalink} target="_blank" rel="noreferrer" className="hx-link-chip">
                              已發佈回覆
                            </a>
                          ) : null}
                        </footer>
                      </article>
                    );
                  })}
                </div>
              )}
          </div>
            )}
          </Card>
        </div>
      )}
      {publishToast ? (
        <div
          className={`hx-publish-toast hx-publish-toast--${publishToast.kind}`}
          role={publishToast.kind === "error" ? "alert" : "status"}
        >
          <div className="hx-publish-toast__title">
            {publishToast.kind === "success" ? "留言發佈成功" : publishToast.kind === "error" ? "留言發佈失敗" : "留言發佈"}
          </div>
          <p className="hx-publish-toast__message">{publishToast.message}</p>
          {publishToast.permalink ? (
            <a href={publishToast.permalink} target="_blank" rel="noreferrer" className="hx-link-chip">
              開啟 Threads 回覆
            </a>
          ) : null}
          <button
            type="button"
            className="hx-publish-toast__close"
            aria-label="關閉提示"
            onClick={() => setPublishToast(null)}
          >
            ×
          </button>
        </div>
      ) : null}
    </div>
  );
}
