import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { KnowledgeGraphData, KnowledgeGraphNode } from '../lib/knowledgeGraph'
import {
  countSelectedGraphNodes,
  expandCopyMissionGraph,
  fetchCopyMissionGraph,
  graphSelectionDirty,
  mergeGraphDraftWithServer,
  patchCopyMissionGraphNodes,
} from '../lib/copyMissionGraph'
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'
import { api, ApiError } from '../api/client'
import { CopyFlowNav } from '../components/CopyFlowNav'
import { CopyMissionActiveJobsCard } from '../components/CopyMissionActiveJobsCard'
import { CopyMissionDraftCard } from '../components/CopyMissionDraftCard'
import { CopyMissionResearchOverview } from '../components/CopyMissionResearchOverview'
import { VerifiedAuthorBadge } from '../components/VerifiedAuthorBadge'

import type { CopyMissionResearchMapDraft } from '../components/CopyMissionResearchEditor'
import { AcLink, Badge, Button, Card, ErrorText, Notice, PageTitle, SuccessText } from '../components/ui'
import {
  COPY_FLOW,
  copyAnalyzeActionLabel,
  getActiveCopyPersonaId,
  isPersonaNotFoundError,
  rememberCopyPersonaId,
  replicateJobScanPostId,
  type CopyFlowStepKey,
} from '../lib/copyFlow'
import { threadsAuthorPostUrl } from '../lib/threadsLinks'
import { hasPersona8D } from '../lib/styleProfile'
import { bindCopyIslanderActions, ensureCopyActionHandlers, unbindCopyIslanderActions } from '../lib/islander/copyActions'
import { useIslanderPage } from '../lib/islander'
import { requestJobMonitorRefresh } from '../lib/jobMonitorRefresh'
import { jobActivitySummary } from '../lib/jobMonitorUi'
import {
  expandJobTerminalMessage,
  formatActiveJobConflictError,
  isActiveJobStatus,
  isTerminalJobStatus,
  jobErrorMessage,
} from '../lib/jobStatus'
import type { CopyDraftData, JobData, PersonaData, ViralScanPostData } from '../types/api'
import {
  clearStoredMissionJobId,
  hydrateCopyMissionJobs,
  rememberCopyMissionJobIds,
  rememberCopyMissionReplicateJobId,
} from '../lib/copyMissionJobs'

import { viralAuthorTier, viralEngagementTier, viralPostRowClass } from '../lib/viralSignals'
import {
  connectionsSettingsPath,
  fetchMemberCapabilities,
  type MemberCapabilitiesData,
} from '../lib/memberCapabilities'
import type { CopyMissionData } from '../types/copyMission'
import { copyMissionStatusLabel, copyMissionStatusTone, hasMissionResearchMap } from '../types/copyMission'

type LocationState = {
  analyzeJobId?: string
  scanJobId?: string
}

export function CopyMissionDetailPage() {
  const navigate = useNavigate()
  const location = useLocation()
  const { missionId = '' } = useParams()
  const [personaId, setPersonaId] = useState(getActiveCopyPersonaId())
  const [personas, setPersonas] = useState<PersonaData[]>([])
  const [missions, setMissions] = useState<CopyMissionData[]>([])
  const [persona, setPersona] = useState<PersonaData | null>(null)
  const [mission, setMission] = useState<CopyMissionData | null>(null)
  const [posts, setPosts] = useState<ViralScanPostData[]>([])
  const [drafts, setDrafts] = useState<CopyDraftData[]>([])
  const [brief, setBrief] = useState('')
  const [selectedTags, setSelectedTags] = useState<string[]>([])
  const [analyzeJob, setAnalyzeJob] = useState<JobData | null>(null)
  const [analyzeJobId, setAnalyzeJobId] = useState<string | null>(null)
  const [scanJob, setScanJob] = useState<JobData | null>(null)
  const [scanJobId, setScanJobId] = useState<string | null>(null)
  const [matrixJob, setMatrixJob] = useState<JobData | null>(null)
  const [matrixJobId, setMatrixJobId] = useState<string | null>(null)
  const [replicateJobs, setReplicateJobs] = useState<JobData[]>([])
  const [loading, setLoading] = useState(true)
  const [analyzing, setAnalyzing] = useState(false)
  const [scanning, setScanning] = useState(false)

  const [generatingId, setGeneratingId] = useState('')
  const [generatingMatrix, setGeneratingMatrix] = useState(false)
  const [savingDraftId, setSavingDraftId] = useState('')
  const [publishingDraftId, setPublishingDraftId] = useState('')
  const [deletingDraftId, setDeletingDraftId] = useState('')
  const [batchDeletingMatrix, setBatchDeletingMatrix] = useState(false)
  const [selectedMatrixDraftIds, setSelectedMatrixDraftIds] = useState<string[]>([])
  const [archiving, setArchiving] = useState(false)
  const [draftEdits, setDraftEdits] = useState<Record<string, { text: string; hook: string }>>({})
  const [editingDraftId, setEditingDraftId] = useState<string | null>(null)
  const [error, setError] = useState('')
  const [message, setMessage] = useState('')
  const [capabilities, setCapabilities] = useState<MemberCapabilitiesData | null>(null)
  const [graph, setGraph] = useState<KnowledgeGraphData | null>(null)
  const [graphLoading, setGraphLoading] = useState(false)
  const [draftNodes, setDraftNodes] = useState<KnowledgeGraphNode[]>([])
  const [savingGraph, setSavingGraph] = useState(false)
  const [expandingKnowledge, setExpandingKnowledge] = useState(false)
  const [expandJob, setExpandJob] = useState<JobData | null>(null)
  const [expandJobId, setExpandJobId] = useState<string | null>(null)
  const [analyzeSucceededTick, setAnalyzeSucceededTick] = useState(0)
  const draftNodesRef = useRef<KnowledgeGraphNode[]>([])
  const savedGraphNodesRef = useRef<KnowledgeGraphNode[]>([])
  const loadRef = useRef<(options?: { silent?: boolean }) => Promise<void>>(async () => undefined)

  const activeStep = useMemo((): CopyFlowStepKey => {
    if (
      location.hash === '#copy-output' ||
      location.hash === '#copy-scan' ||
      location.hash === '#copy-matrix'
    ) {
      return 'output'
    }
    return 'research'
  }, [location.hash])

  const sortedDrafts = useMemo(() => {
    return [...drafts].sort((a, b) => {
      const aMatrix = a.draft_type === 'matrix'
      const bMatrix = b.draft_type === 'matrix'
      if (aMatrix && bMatrix) return (a.sort_order ?? 0) - (b.sort_order ?? 0)
      if (aMatrix !== bMatrix) return aMatrix ? -1 : 1
      return (b.create_at ?? 0) - (a.create_at ?? 0)
    })
  }, [drafts])

  const matrixDrafts = useMemo(
    () => sortedDrafts.filter((draft) => draft.draft_type === 'matrix'),
    [sortedDrafts],
  )

  const sortedPosts = useMemo(() => {
    return [...posts].sort((a, b) => {
      const aVerified = a.author_verified ? 1 : 0
      const bVerified = b.author_verified ? 1 : 0
      if (aVerified !== bVerified) return bVerified - aVerified
      return (b.engagement_score ?? 0) - (a.engagement_score ?? 0)
    })
  }, [posts])

  const setDraftNodesSynced = useCallback((nodes: KnowledgeGraphNode[]) => {
    draftNodesRef.current = nodes
    setDraftNodes(nodes)
  }, [])

  const reloadGraph = useCallback(
    async (options?: { force?: boolean; silent?: boolean }) => {
      if (!personaId || !missionId) return null
      const hasGraph = savedGraphNodesRef.current.length > 0
      if (!options?.silent || !hasGraph) {
        setGraphLoading(true)
      }
      try {
        const data = await fetchCopyMissionGraph(personaId, missionId)
        const serverNodes = data.nodes ?? []
        const localDraft = draftNodesRef.current
        const savedBaseline = savedGraphNodesRef.current.length ? savedGraphNodesRef.current : serverNodes
        const preserveLocal =
          !options?.force && graphSelectionDirty(localDraft, savedBaseline)
        const mergedNodes = preserveLocal ? mergeGraphDraftWithServer(localDraft, serverNodes) : serverNodes
        setGraph(data)
        savedGraphNodesRef.current = serverNodes
        setDraftNodesSynced(mergedNodes)
        return data
      } catch (e) {
        if (e instanceof ApiError && /not found|找不到/i.test(e.message)) {
          setGraph(null)
          savedGraphNodesRef.current = []
          setDraftNodesSynced([])
          return null
        }
        throw e
      } finally {
        setGraphLoading(false)
      }
    },
    [missionId, personaId, setDraftNodesSynced],
  )

  const syncMissionJobs = useCallback(
    async (lastScanJobId?: string) => {
      const snapshot = await hydrateCopyMissionJobs(missionId, lastScanJobId)
      setAnalyzeJob(snapshot.analyzeJob)
      setAnalyzeJobId(snapshot.analyzeJobId)
      setScanJob(snapshot.scanJob)
      setScanJobId(snapshot.scanJobId)
      setExpandJob(snapshot.expandJob)
      setExpandJobId(snapshot.expandJobId)
      if (snapshot.expandJobId) {
        setExpandingKnowledge(true)
      }
      setMatrixJob(snapshot.matrixJob)
      setMatrixJobId(snapshot.matrixJobId)
      setReplicateJobs(snapshot.replicateJobs)
    },
    [missionId],
  )

  const refreshJobById = useCallback(async (jobId: string) => {
    const id = jobId.trim()
    if (!id) return
    const job = await api.get<JobData>(`/api/v1/jobs/${encodeURIComponent(id)}`, { auth: true })
    if (job.template_type === 'expand-copy-mission-graph') {
      if (isActiveJobStatus(job.status)) {
        setExpandJob(job)
        setExpandJobId(job.id)
      } else {
        setExpandJob(null)
        setExpandJobId(null)
      }
      return
    }
    if (job.template_type === 'analyze-copy-mission') {
      if (isTerminalJobStatus(job.status)) {
        setAnalyzeJob(null)
        setAnalyzeJobId(null)
        setAnalyzing(false)
        if (job.status === 'succeeded') {
          setMessage('研究地圖已更新')
          setError('')
          setAnalyzeSucceededTick((tick) => tick + 1)
        } else {
          setError(jobErrorMessage(job) || '研究地圖產生失敗')
        }
        return
      }
      setAnalyzeJob(job)
      setAnalyzeJobId(job.id)
      return
    }
    if (job.template_type === 'scan-viral') {
      if (isActiveJobStatus(job.status)) {
        setScanJob(job)
        setScanJobId(job.id)
      } else {
        setScanJob(null)
        setScanJobId(null)
      }
      return
    }
    if (job.template_type === 'generate-copy-matrix') {
      if (isActiveJobStatus(job.status)) {
        setMatrixJob(job)
        setMatrixJobId(job.id)
      } else {
        setMatrixJob(null)
        setMatrixJobId(null)
      }
      return
    }
    if (job.template_type === 'generate-copy-draft') {
      const postId = replicateJobScanPostId(job)
      setReplicateJobs((prev) => {
        const rest = prev.filter((item) => item.id !== job.id && replicateJobScanPostId(item) !== postId)
        if (isActiveJobStatus(job.status)) {
          return [...rest, job]
        }
        return rest
      })
    }
  }, [])

  const loadNavCatalog = useCallback(async (targetPersonaId: string) => {
    const [personasRes, missionsRes] = await Promise.all([
      api.get<{ list: PersonaData[] }>('/api/v1/personas', { auth: true }),
      api.get<{ list: CopyMissionData[] }>(
        `/api/v1/personas/${encodeURIComponent(targetPersonaId)}/copy-missions`,
        { auth: true },
      ),
    ])
    setPersonas(personasRes.list ?? [])
    setMissions(missionsRes.list ?? [])
  }, [])

  const loadWithPersona = useCallback(
    async (targetPersonaId: string, options?: { silent?: boolean }) => {
      const silent = options?.silent ?? false
      const [personaRes, missionRes, postsRes, draftsRes, capsRes] = await Promise.all([
        api.get<PersonaData>(`/api/v1/personas/${encodeURIComponent(targetPersonaId)}`, { auth: true }),
        api.get<CopyMissionData>(
          `/api/v1/personas/${encodeURIComponent(targetPersonaId)}/copy-missions/${encodeURIComponent(missionId)}`,
          { auth: true },
        ),
        api.get<{ list: ViralScanPostData[]; total: number }>(
          `/api/v1/personas/${encodeURIComponent(targetPersonaId)}/copy-missions/${encodeURIComponent(missionId)}/scan-posts`,
          { auth: true },
        ),
        api.get<{ list: CopyDraftData[]; total: number }>(
          `/api/v1/personas/${encodeURIComponent(targetPersonaId)}/copy-missions/${encodeURIComponent(missionId)}/copy-drafts`,
          { auth: true },
        ),
        fetchMemberCapabilities().catch(() => null),
      ])
      setCapabilities(capsRes)
      setPersona(personaRes)
      setMission(missionRes)
      setBrief(missionRes.brief ?? '')
      setSelectedTags(missionRes.selected_tags ?? [])
      setPosts(postsRes.list ?? [])
      setDrafts(draftsRes.list ?? [])
      await Promise.all([
        syncMissionJobs(missionRes.last_scan_job_id),
        loadNavCatalog(targetPersonaId),
        reloadGraph({ silent }).catch(() => null),
      ])
      return targetPersonaId
    },
    [loadNavCatalog, missionId, reloadGraph, syncMissionJobs],
  )

  const discoverMissionPersonaId = useCallback(async (): Promise<string | null> => {
    const personas = await api.get<{ list: PersonaData[] }>('/api/v1/personas', { auth: true })
    for (const item of personas.list ?? []) {
      try {
        await api.get<CopyMissionData>(
          `/api/v1/personas/${encodeURIComponent(item.id)}/copy-missions/${encodeURIComponent(missionId)}`,
          { auth: true },
        )
        return item.id
      } catch {
        continue
      }
    }
    return null
  }, [missionId])

  const load = useCallback(async (options?: { silent?: boolean }) => {
    if (!personaId || !missionId) return
    const silent = options?.silent ?? false
    if (!silent) {
      setLoading(true)
      setError('')
    }
    try {
      await loadWithPersona(personaId, { silent })
    } catch (e) {
      if (silent) return
      const message = e instanceof ApiError ? e.message : '載入失敗'
      if (e instanceof ApiError && (isPersonaNotFoundError(message) || /copy-mission|找不到|not found/i.test(message))) {
        try {
          const discovered = await discoverMissionPersonaId()
          if (discovered && discovered !== personaId) {
            setPersonaId(discovered)
            rememberCopyPersonaId(discovered)
            await loadWithPersona(discovered)
            setError('先前選擇的人設已失效，已自動切換到任務所屬人設。')
            return
          }
          setError('找不到這個拷貝任務，已回到任務列表。')
          navigate('/matrix', { replace: true })
          return
        } catch {
          // fall through
        }
      }
      if (!silent) setError(message)
    } finally {
      if (!silent) setLoading(false)
    }
  }, [discoverMissionPersonaId, loadWithPersona, missionId, navigate, personaId])

  loadRef.current = load

  useEffect(() => {
    const id = getActiveCopyPersonaId()
    if (id) setPersonaId(id)
  }, [])

  useEffect(() => {
    setMission(null)
    savedGraphNodesRef.current = []
    setGraph(null)
    setDraftNodesSynced([])
    setExpandJob(null)
    setExpandJobId(null)
    setExpandingKnowledge(false)
  }, [missionId, setDraftNodesSynced])

  useEffect(() => {
    if (!personaId || !missionId) return
    void loadRef.current()
  }, [missionId, personaId])

  useEffect(() => {
    const state = location.state as LocationState | null
    if (!state?.analyzeJobId && !state?.scanJobId) return
    if (state.analyzeJobId) {
      rememberCopyMissionJobIds(missionId, { analyzeJobId: state.analyzeJobId })
      setAnalyzeJobId(state.analyzeJobId)
    }
    if (state.scanJobId) {
      rememberCopyMissionJobIds(missionId, { scanJobId: state.scanJobId })
      setScanJobId(state.scanJobId)
    }
    navigate(`${location.pathname}${location.search}`, { replace: true, state: null })
  }, [location.pathname, location.search, location.state, missionId, navigate])

  const lastScrolledHash = useRef('')
  useEffect(() => {
    lastScrolledHash.current = ''
  }, [missionId])

  useEffect(() => {
    const hash = location.hash
    if (!hash || loading) return
    if (lastScrolledHash.current === hash) return
    lastScrolledHash.current = hash
    const id = hash.slice(1)
    const el = document.getElementById(id)
    el?.scrollIntoView({ behavior: 'smooth', block: 'start' })
  }, [location.hash, loading])

  useIslanderPage({
    title: '拷貝任務詳情',
    purpose: '研究地圖 → 延伸知識圖譜勾選 → 相似帳號參考 → 產出矩陣草稿。',
    hints: [
      '產生研究地圖後會自動擴展延伸知識；在「延伸知識」分頁勾選節點並儲存，矩陣產文會優先使用',
      '海巡是選配，產出矩陣只需要研究地圖與 AI key',
      '相似帳號可人工點進去看，不會強迫用貼文仿寫',
    ],
    suggestions: ['產生研究地圖', '查看相似帳號', '產出 5 篇矩陣'],
  })

  const replicateJobIds = useMemo(() => replicateJobs.map((job) => job.id), [replicateJobs])

  const refreshExpandJob = useCallback(
    async (jobId: string) => {
      const job = await api.get<JobData>(`/api/v1/jobs/${encodeURIComponent(jobId)}`, { auth: true })
      if (isTerminalJobStatus(job.status)) {
        setExpandJob(null)
        setExpandJobId(null)
        setExpandingKnowledge(false)
        clearStoredMissionJobId(missionId, 'expand')
        const terminalMsg = expandJobTerminalMessage(job.status)
        if (job.status === 'succeeded') {
          await Promise.all([reloadGraph(), load({ silent: true })])
        }
        if (terminalMsg) {
          setMessage(terminalMsg)
          setError('')
        } else {
          setError(jobErrorMessage(job) || jobActivitySummary(job) || '延伸知識擴展失敗')
        }
        return
      }
      setExpandJob(job)
      setExpandJobId(job.id)
      setMessage(jobActivitySummary(job))
    },
    [load, missionId, reloadGraph],
  )

  const startExpandKnowledge = useCallback(
    async (options?: { supplemental?: boolean }) => {
      if (!personaId || !missionId) return
      const seed = mission?.seed_query?.trim() || mission?.label?.trim()
      if (!seed) {
        setError('請先設定任務種子關鍵字')
        return
      }
      setExpandingKnowledge(true)
      setError('')
      try {
        const started = await expandCopyMissionGraph(personaId, missionId, {
          seed_query: seed,
          supplemental: options?.supplemental,
        })
        if (started.job_id) {
          rememberCopyMissionJobIds(missionId, { expandJobId: started.job_id })
          setExpandJobId(started.job_id)
          requestJobMonitorRefresh()
          await refreshExpandJob(started.job_id)
        } else {
          setExpandingKnowledge(false)
        }
        setMessage(started.message || '延伸知識擴展中…')
      } catch (e) {
        setExpandingKnowledge(false)
        setError(e instanceof ApiError ? formatActiveJobConflictError(e.message) : '啟動延伸知識擴展失敗')
      }
    },
    [mission?.label, mission?.seed_query, missionId, personaId, refreshExpandJob],
  )

  const saveGraphSelection = useCallback(async () => {
    if (!personaId || !missionId) return
    const nodes = draftNodesRef.current
    if (!nodes.length) return
    setSavingGraph(true)
    setError('')
    try {
      const updates = nodes.map((node) => ({
        node_id: node.id,
        selected_for_scan: node.selected_for_scan,
      }))
      const data = await patchCopyMissionGraphNodes(personaId, missionId, updates)
      setGraph(data)
      savedGraphNodesRef.current = data.nodes ?? []
      setDraftNodesSynced(data.nodes ?? [])
      const updated = await api.get<CopyMissionData>(
        `/api/v1/personas/${encodeURIComponent(personaId)}/copy-missions/${encodeURIComponent(missionId)}`,
        { auth: true },
      )
      setMission(updated)
      setMessage('已儲存勾選的延伸知識，矩陣產文會優先使用')
    } catch (e) {
      setError(e instanceof ApiError ? e.message : '儲存勾選失敗')
    } finally {
      setSavingGraph(false)
    }
  }, [missionId, personaId, setDraftNodesSynced])

  useEffect(() => {
    if (!expandJobId) return
    void refreshExpandJob(expandJobId)
    const timer = window.setInterval(() => {
      void refreshExpandJob(expandJobId).catch(() => undefined)
    }, 3000)
    return () => window.clearInterval(timer)
  }, [expandJobId, refreshExpandJob])

  useEffect(() => {
    if (analyzeSucceededTick === 0 || expandJobId || expandingKnowledge) return
    void (async () => {
      const graphData = await reloadGraph({ force: true }).catch(() => null)
      if ((graphData?.nodes?.length ?? 0) > 0) return
      await startExpandKnowledge()
    })()
  }, [analyzeSucceededTick, expandJobId, expandingKnowledge, reloadGraph, startExpandKnowledge])

  useEffect(() => {
    const hasActiveOutputJobs =
      !!matrixJobId || replicateJobIds.length > 0 || generatingMatrix || generatingId !== ''
    const hasActiveResearchJobs = !!expandJobId || expandingKnowledge
    if (!analyzeJobId && !scanJobId && !analyzing && !scanning && !hasActiveOutputJobs && !hasActiveResearchJobs) return
    const timer = window.setInterval(() => {
      if (analyzeJobId) {
        void refreshJobById(analyzeJobId).catch(() => undefined)
      }
      if (scanJobId) {
        void refreshJobById(scanJobId).catch(() => undefined)
      }
      if (matrixJobId) {
        void refreshJobById(matrixJobId).catch(() => undefined)
      }
      for (const jobId of replicateJobIds) {
        void refreshJobById(jobId).catch(() => undefined)
      }
      void load({ silent: true }).catch(() => undefined)
    }, 3000)
    return () => window.clearInterval(timer)
  }, [
    analyzeJobId,
    analyzing,
    expandJobId,
    expandingKnowledge,
    generatingId,
    generatingMatrix,
    load,
    matrixJobId,
    refreshJobById,
    replicateJobIds,
    scanJobId,
    scanning,
  ])

  const startAnalyze = async () => {
    if (!personaId || !missionId) return
    setAnalyzing(true)
    setError('')
    setMessage('')
    try {
      const started = await api.post<{ job_id: string; status: string; message?: string }>(
        `/api/v1/personas/${encodeURIComponent(personaId)}/copy-missions/${encodeURIComponent(missionId)}/analyze-jobs`,
        {},
        { auth: true },
      )
      if (started.job_id) {
        rememberCopyMissionJobIds(missionId, { analyzeJobId: started.job_id })
        setAnalyzeJobId(started.job_id)
        requestJobMonitorRefresh()
        await refreshJobById(started.job_id)
      }
      setMessage(started.message || '研究地圖產生中…')
      await load()
    } catch (e) {
      const message = e instanceof ApiError ? formatActiveJobConflictError(e.message) : '啟動分析失敗'
      setError(message)
    } finally {
      setAnalyzing(false)
    }
  }

  const saveResearchMap = useCallback(
    async (draft: CopyMissionResearchMapDraft) => {
      if (!personaId || !missionId) return
      const updated = await api.patch<CopyMissionData>(
        `/api/v1/personas/${encodeURIComponent(personaId)}/copy-missions/${encodeURIComponent(missionId)}`,
        {
          audience_summary: draft.audience_summary,
          content_goal: draft.content_goal,
          questions: draft.questions,
          pillars: draft.pillars,
          exclusions: draft.exclusions,
          knowledge_items: draft.knowledge_items,
          selected_knowledge_items: draft.selected_knowledge_items,
          benchmark_notes: draft.benchmark_notes,
          selected_tags: draft.selected_tags,
        },
        { auth: true },
      )
      setMission(updated)
      setSelectedTags(updated.selected_tags ?? [])
      setMessage(
        draft.selected_tags.length > 0
          ? `研究地圖已儲存（${draft.selected_tags.length} 組搜尋標籤）`
          : '研究地圖已儲存',
      )
      setError('')
    },
    [missionId, personaId],
  )

  const persistSelectedTags = async (tags: string[]) => {
    if (!personaId || !missionId) return
    setError('')
    try {
      const updated = await api.patch<CopyMissionData>(
        `/api/v1/personas/${encodeURIComponent(personaId)}/copy-missions/${encodeURIComponent(missionId)}`,
        { selected_tags: tags },
        { auth: true },
      )
      setMission(updated)
      setSelectedTags(updated.selected_tags ?? [])
      setMessage('搜尋標籤已更新')
    } catch (e) {
      setError(e instanceof ApiError ? e.message : '儲存標籤失敗')
    }
  }

  const startScan = async () => {
    if (!personaId || !missionId) return
    setScanning(true)
    setError('')
    try {
      const started = await api.post<{ job_id: string; status: string; message?: string }>(
        `/api/v1/personas/${encodeURIComponent(personaId)}/copy-missions/${encodeURIComponent(missionId)}/scan-jobs`,
        {},
        { auth: true },
      )
      if (started.job_id) {
        rememberCopyMissionJobIds(missionId, { scanJobId: started.job_id })
        setScanJobId(started.job_id)
        requestJobMonitorRefresh()
        await refreshJobById(started.job_id)
      }
      setPosts([])
      setDrafts([])
      setDraftEdits({})
      setEditingDraftId(null)
      setMessage(started.message || '已清除舊爆款，海巡進行中…')
      await load()
    } catch (e) {
      const message = e instanceof ApiError ? formatActiveJobConflictError(e.message) : '啟動海巡失敗'
      setError(message)
    } finally {
      setScanning(false)
    }
  }

  const addAccountTag = (username: string) => {
    const tag = username.startsWith('@') ? username : `@${username}`
    if (selectedTags.includes(tag)) return
    if (selectedTags.length >= 6) {
      setError('搜尋標籤最多 6 個，請先編輯研究地圖移除一個再加入帳號')
      return
    }
    void persistSelectedTags([...selectedTags, tag])
  }

  const syncGraphSelectionBeforeMatrix = useCallback(async (): Promise<CopyMissionData | null> => {
    if (!personaId || !missionId) return null
    let nodes = draftNodesRef.current
    if (!nodes.length) {
      const graphData = await fetchCopyMissionGraph(personaId, missionId).catch(() => null)
      nodes = graphData?.nodes ?? []
      if (nodes.length) {
        setGraph(graphData)
        savedGraphNodesRef.current = nodes
        setDraftNodesSynced(nodes)
      }
    }
    if (!nodes.length) return mission
    const updates = nodes.map((node) => ({
      node_id: node.id,
      selected_for_scan: node.selected_for_scan,
    }))
    const data = await patchCopyMissionGraphNodes(personaId, missionId, updates)
    setGraph(data)
    savedGraphNodesRef.current = data.nodes ?? []
    setDraftNodesSynced(data.nodes ?? [])
    const updated = await api.get<CopyMissionData>(
      `/api/v1/personas/${encodeURIComponent(personaId)}/copy-missions/${encodeURIComponent(missionId)}`,
      { auth: true },
    )
    setMission(updated)
    return updated
  }, [mission, missionId, personaId, setDraftNodesSynced])

  useEffect(() => {
    ensureCopyActionHandlers()
    bindCopyIslanderActions({
      personaId,
      missionId,
      keywords: selectedTags,
      onStartAnalyze: () => setAnalyzing(true),
      onStartScan: () => setScanning(true),
      onGenerateMatrix: () => setGeneratingMatrix(true),
      syncGraphBeforeMatrix: () => syncGraphSelectionBeforeMatrix(),
      reload: () => void load(),
    })
    return () => unbindCopyIslanderActions()
  }, [personaId, missionId, selectedTags, load, syncGraphSelectionBeforeMatrix])

  const generateMatrix = async () => {
    if (!personaId || !missionId) return
    setGeneratingMatrix(true)
    setError('')
    setMessage('')
    try {
      const syncedMission = await syncGraphSelectionBeforeMatrix()
      const selectedKnowledge = Math.max(
        countSelectedGraphNodes(draftNodesRef.current),
        syncedMission?.research_map?.selected_knowledge_items?.length ?? 0,
        mission?.research_map?.selected_knowledge_items?.length ?? 0,
      )
      const started = await api.post<{ job_id: string; status: string; message?: string }>(
        `/api/v1/personas/${encodeURIComponent(personaId)}/copy-missions/${encodeURIComponent(missionId)}/matrix-jobs`,
        { count: 5 },
        { auth: true },
      )
      if (started.job_id) {
        rememberCopyMissionJobIds(missionId, { matrixJobId: started.job_id })
        setMatrixJobId(started.job_id)
        requestJobMonitorRefresh()
        await refreshJobById(started.job_id)
      }
      setMessage(
        selectedKnowledge > 0
          ? `依你勾選的 ${selectedKnowledge} 項延伸知識產出矩陣…`
          : started.message || '內容矩陣產出中…',
      )
    } catch (e) {
      const message = e instanceof ApiError ? formatActiveJobConflictError(e.message) : '產出矩陣失敗'
      setError(message)
    } finally {
      setGeneratingMatrix(false)
    }
  }

  const draftEdit = (draft: CopyDraftData) =>
    draftEdits[draft.id] ?? { text: draft.text, hook: draft.hook ?? '' }

  const setDraftEdit = (draft: CopyDraftData, patch: Partial<{ text: string; hook: string }>) => {
    setDraftEdits((prev) => {
      const base = prev[draft.id] ?? { text: draft.text, hook: draft.hook ?? '' }
      return { ...prev, [draft.id]: { ...base, ...patch } }
    })
  }

  const saveDraft = async (draft: CopyDraftData, markReady = false) => {
    if (!personaId) return
    const edit = draftEdit(draft)
    setSavingDraftId(draft.id)
    setError('')
    try {
      const updated = await api.patch<CopyDraftData>(
        `/api/v1/personas/${encodeURIComponent(personaId)}/copy-drafts/${encodeURIComponent(draft.id)}`,
        {
          text: edit.text,
          hook: edit.hook || undefined,
          status: markReady ? 'ready' : undefined,
        },
        { auth: true },
      )
      setDrafts((prev) => prev.map((d) => (d.id === draft.id ? updated : d)))
      setDraftEdits((prev) => {
        const next = { ...prev }
        delete next[draft.id]
        return next
      })
      setEditingDraftId((cur) => (cur === draft.id ? null : cur))
    } catch (e) {
      setError(e instanceof ApiError ? e.message : '儲存草稿失敗')
    } finally {
      setSavingDraftId('')
    }
  }

  const removeDraftsFromState = useCallback((ids: string[]) => {
    const idSet = new Set(ids)
    setDrafts((prev) => prev.filter((draft) => !idSet.has(draft.id)))
    setSelectedMatrixDraftIds((prev) => prev.filter((id) => !idSet.has(id)))
    setEditingDraftId((cur) => (cur && idSet.has(cur) ? null : cur))
    setDraftEdits((prev) => {
      const next = { ...prev }
      for (const id of ids) delete next[id]
      return next
    })
  }, [])

  const deleteDraft = async (draft: CopyDraftData) => {
    if (!personaId) return
    if (!window.confirm('確定刪除此草稿？刪除後無法復原。')) return
    setDeletingDraftId(draft.id)
    setError('')
    try {
      await api.delete<{ draft_id: string; message?: string }>(
        `/api/v1/personas/${encodeURIComponent(personaId)}/copy-drafts/${encodeURIComponent(draft.id)}`,
        { auth: true },
      )
      removeDraftsFromState([draft.id])
      setMessage('草稿已刪除')
    } catch (e) {
      setError(e instanceof ApiError ? e.message : '刪除草稿失敗')
    } finally {
      setDeletingDraftId('')
    }
  }

  const deleteMatrixDrafts = async (draftIds: string[]) => {
    if (!personaId || !missionId) return
    const ids = [...new Set(draftIds.map((id) => id.trim()).filter(Boolean))]
    const deletingAll = ids.length === 0
    const confirmText = deletingAll
      ? `確定刪除全部 ${matrixDrafts.length} 篇矩陣草稿？`
      : `確定刪除選取的 ${ids.length} 篇矩陣草稿？`
    if (!window.confirm(confirmText)) return
    setBatchDeletingMatrix(true)
    setError('')
    try {
      const result = await api.post<{ deleted: number; message?: string }>(
        `/api/v1/personas/${encodeURIComponent(personaId)}/copy-missions/${encodeURIComponent(missionId)}/matrix-drafts/delete`,
        deletingAll ? {} : { draft_ids: ids },
        { auth: true },
      )
      const removed = deletingAll ? matrixDrafts.map((draft) => draft.id) : ids
      removeDraftsFromState(removed)
      setMessage(result.message || `已刪除 ${result.deleted ?? removed.length} 篇矩陣草稿`)
    } catch (e) {
      setError(e instanceof ApiError ? e.message : '刪除矩陣草稿失敗')
    } finally {
      setBatchDeletingMatrix(false)
    }
  }

  const toggleMatrixDraftSelection = (draftId: string, selected: boolean) => {
    setSelectedMatrixDraftIds((prev) => {
      if (selected) return prev.includes(draftId) ? prev : [...prev, draftId]
      return prev.filter((id) => id !== draftId)
    })
  }

  const publishDraft = async (draft: CopyDraftData) => {
    if (!personaId) return
    const edit = draftEdit(draft)
    if (!window.confirm('確定要透過 Threads API 發布這則貼文嗎？')) return
    setPublishingDraftId(draft.id)
    setError('')
    try {
      const result = await api.post<{
        draft_id: string
        permalink?: string
        status: string
        message: string
      }>(
        `/api/v1/personas/${encodeURIComponent(personaId)}/copy-drafts/${encodeURIComponent(draft.id)}/publish`,
        { text: edit.text, confirm: true },
        { auth: true },
      )
      if (result.message) {
        setError('')
      }
      await load()
    } catch (e) {
      setError(e instanceof ApiError ? e.message : '發布失敗')
    } finally {
      setPublishingDraftId('')
    }
  }

  const archiveMission = async () => {
    if (!personaId || !missionId) return
    if (!window.confirm('封存後會停用定期海巡，確定要封存此任務嗎？')) return
    setArchiving(true)
    setError('')
    try {
      await api.patch(
        `/api/v1/personas/${encodeURIComponent(personaId)}/copy-missions/${encodeURIComponent(missionId)}`,
        { status: 'archived' },
        { auth: true },
      )
      await load()
    } catch (e) {
      setError(e instanceof ApiError ? e.message : '封存失敗')
    } finally {
      setArchiving(false)
    }
  }

  const copyDraftText = async (text: string): Promise<boolean> => {
    try {
      await navigator.clipboard.writeText(text)
      setError('')
      setMessage('已複製內文到剪貼簿')
      return true
    } catch {
      setError('無法複製到剪貼簿')
      return false
    }
  }

  const generateDraft = async (scanPostId: string) => {
    if (!personaId || !missionId) return
    setGeneratingId(scanPostId)
    setError('')
    setMessage('')
    try {
      const started = await api.post<{ job_id: string; status: string; message?: string }>(
        `/api/v1/personas/${encodeURIComponent(personaId)}/copy-missions/${encodeURIComponent(missionId)}/copy-draft-jobs`,
        { scan_post_id: scanPostId },
        { auth: true },
      )
      if (started.job_id) {
        rememberCopyMissionReplicateJobId(missionId, scanPostId, started.job_id)
        requestJobMonitorRefresh()
        await refreshJobById(started.job_id)
      }
      setMessage(started.message || '深仿寫產出中…')
      await load()
    } catch (e) {
      const message = e instanceof ApiError ? formatActiveJobConflictError(e.message) : '產出仿寫失敗'
      setError(message)
    } finally {
      setGeneratingId('')
    }
  }

  const handlePersonaChange = async (nextId: string) => {
    rememberCopyPersonaId(nextId)
    setPersonaId(nextId)
    setError('')
    try {
      const missionsRes = await api.get<{ list: CopyMissionData[] }>(
        `/api/v1/personas/${encodeURIComponent(nextId)}/copy-missions`,
        { auth: true },
      )
      const list = missionsRes.list ?? []
      setMissions(list)
      const keepsMission = list.some((item) => item.id === missionId)
      if (!keepsMission) {
        const first = list[0]?.id
        if (first) {
          navigate(`/matrix/missions/${encodeURIComponent(first)}${location.hash}`)
        } else {
          navigate('/matrix')
        }
      }
    } catch (e) {
      setError(e instanceof ApiError ? e.message : '切換人設失敗')
    }
  }

  const handleMissionChange = (nextId: string) => {
    navigate(`/matrix/missions/${encodeURIComponent(nextId)}${location.hash}`)
  }

  if (!personaId) {
    return (
      <Card className="p-8 text-center">
        <p className="text-sm text-ink-secondary">請先從拷貝忍者首頁選擇人設。</p>
        <Link to="/matrix" className="mt-3 inline-block font-semibold text-brand hover:underline">
          返回任務列表
        </Link>
      </Card>
    )
  }

  const has8D = hasPersona8D(persona)
  const mapReady = hasMissionResearchMap(mission)
  const aiReady = capabilities?.ai_ready ?? false
  const discoverReady = capabilities?.discover_ready ?? false
  const publishReady = capabilities?.publish_ready ?? false
  const settingsConnectionsPath = connectionsSettingsPath(capabilities?.active_threads_account_id)
  const displayName = persona?.display_name?.trim() || '未命名人設'
  const missionTitle = mission?.label?.trim() || mission?.seed_query?.trim() || '拷貝任務'
  const stepMeta = COPY_FLOW.find((step) => step.key === activeStep) ?? COPY_FLOW[0]
  const analyzeActive = analyzing || (analyzeJob != null && isActiveJobStatus(analyzeJob.status))
  const scanActive = scanning || (scanJob != null && isActiveJobStatus(scanJob.status))
  const matrixActive =
    generatingMatrix || (matrixJob != null && isActiveJobStatus(matrixJob.status))
  const replicateJobsByPost = useMemo(() => {
    const map = new Map<string, JobData>()
    for (const job of replicateJobs) {
      const postId = replicateJobScanPostId(job)
      if (postId) map.set(postId, job)
    }
    return map
  }, [replicateJobs])
  const hasActiveOutputJobs = matrixActive || replicateJobs.length > 0
  const analyzeActionLabel = copyAnalyzeActionLabel(analyzeJob, analyzing, mapReady)
  const scanBlockedReason = analyzeActive
    ? '研究地圖產生中，請稍候再海巡'
    : scanActive
      ? '海巡任務進行中'
      : !mapReady
        ? '請先產生研究地圖'
        : ''
  const scanClearHint =
    selectedTags.length === 0
      ? '若尚未儲存搜尋查詢，後端會提示需要先新增查詢'
      : posts.length > 0 || drafts.length > 0
        ? '會清除此任務的舊爆款候選與產文草稿，再重新掃描'
        : undefined
  const matrixBlockedReason = !has8D
      ? '請先完成 8D 分析'
      : matrixActive
        ? '矩陣產出中'
        : ''

  const openDraftEdit = (draft: CopyDraftData) => {
    setDraftEdits((prev) => ({
      ...prev,
      [draft.id]: prev[draft.id] ?? { text: draft.text, hook: draft.hook ?? '' },
    }))
    setEditingDraftId(draft.id)
  }

  const closeDraftEdit = (draftId: string) => {
    setEditingDraftId((cur) => (cur === draftId ? null : cur))
    setDraftEdits((prev) => {
      const next = { ...prev }
      delete next[draftId]
      return next
    })
  }

  return (
    <div className="mx-auto w-full max-w-6xl space-y-6">
      <AcLink to="/matrix" className="inline-flex items-center gap-1.5 text-sm">
        ← 拷貝忍者任務
      </AcLink>

      <CopyFlowNav
        active={activeStep}
        missionId={missionId}
        personaId={personaId}
        personas={personas}
        missions={missions}
        onPersonaChange={(id) => void handlePersonaChange(id)}
        onMissionChange={handleMissionChange}
        loading={loading}
        analyzeJobActive={analyzeActive}
        scanJobActive={scanActive}
      />

      <div className="flex flex-wrap items-start justify-between gap-4">
        <PageTitle
          title={stepMeta.label}
          subtitle={
            loading && !mission
              ? '載入中…'
              : `「${missionTitle}」· ${displayName} · ${stepMeta.desc}`
          }
        />
        <div className="hx-page-actions">
          {activeStep === 'research' ? (
            <>
              {mission?.status !== 'archived' ? (
                <Button variant="ghost" onClick={() => void archiveMission()} disabled={archiving || loading}>
                  {archiving ? '封存中…' : '封存任務'}
                </Button>
              ) : null}
              {aiReady ? (
                <Button
                  variant="soft"
                  onClick={() => void startAnalyze()}
                  disabled={loading || analyzeActive}
                  title={analyzeActive ? '研究地圖產生中，請稍候' : undefined}
                >
                  {analyzeActionLabel}
                </Button>
              ) : null}
            </>
          ) : null}
        </div>
      </div>

      <div className="flex flex-wrap items-center gap-2">
        {mission?.status ? (
          <Badge tone={copyMissionStatusTone(mission.status)}>{copyMissionStatusLabel(mission.status)}</Badge>
        ) : null}
        {mapReady ? <Badge tone="success">研究地圖就緒</Badge> : <Badge tone="warning">待產生地圖</Badge>}
        {posts.length ? <Badge tone="sky">爆款 {posts.length}</Badge> : null}
      </div>

      <SuccessText message={message} />
      <ErrorText message={error} />

      {analyzeActive || scanActive || hasActiveOutputJobs ? (
        <CopyMissionActiveJobsCard
          missionId={missionId}
          analyzeJob={analyzeJob}
          scanJob={scanJob}
          matrixJob={matrixJob}
          replicateJobs={replicateJobs}
        />
      ) : null}

      {!has8D ? (
        <Notice
          tone="warning"
          title="建議先完成 8D 分析"
          message="仿寫會套用 style_profile；請到人設頁完成對標帳號 8D。"
        />
      ) : null}

      {brief.trim() || mission?.seed_query ? (
        <Notice
          tone="info"
          title="任務種子"
          message={`種子熱詞：${mission?.seed_query || '—'}${brief.trim() ? ` · 補充：${brief.trim()}` : ''}`}
        />
      ) : null}

      <section id="copy-research" className="space-y-4">
        {capabilities && !aiReady ? (
          <Notice
            tone="warning"
            title="產生研究地圖需要 AI API key"
            message={
              <>
                只需設定 AI key，不需要 Threads API。海巡可用 Chrome 爬蟲 Session。
                {' '}
                {capabilities.ai_hint || '請先在設定頁設定 AI API key。'}
                {' '}
                <Link to="/settings" className="font-semibold text-brand hover:underline">
                  前往設定
                </Link>
              </>
            }
          />
        ) : null}
        <div className="hx-research-map-workspace">
          {analyzeActive && !analyzeJob ? (
            <Card className="mb-5">
              <p className="text-sm text-ink-secondary">研究地圖產生中，正在建立背景任務…</p>
            </Card>
          ) : null}
          <CopyMissionResearchOverview
            mission={mission}
            personaId={personaId}
            loading={loading && !mission}
            onSaveResearchMap={mapReady ? saveResearchMap : undefined}
            onMissionUpdate={setMission}
            onAddScanTag={mapReady ? addAccountTag : undefined}
            onActionMessage={setMessage}
            onActionError={setError}
            onRegenerateResearchMap={() => void startAnalyze()}
            regenerating={analyzeActive}
            aiReady={aiReady}
            graph={graph}
            graphLoading={graphLoading}
            draftNodes={draftNodes}
            savingGraph={savingGraph}
            expandingKnowledge={expandingKnowledge || (expandJob != null && isActiveJobStatus(expandJob.status))}
            discoverReady={discoverReady}
            onToggleGraphNode={(nodeId, selected) => {
              setDraftNodesSynced(
                draftNodesRef.current.map((node) =>
                  node.id === nodeId ? { ...node, selected_for_scan: selected } : node,
                ),
              )
            }}
            onSelectPainNodes={() => {
              setDraftNodesSynced(
                draftNodesRef.current.map((node) => ({
                  ...node,
                  selected_for_scan:
                    node.node_kind === 'pain' ||
                    node.node_kind === 'symptom' ||
                    node.node_kind === 'cause',
                })),
              )
            }}
            onClearGraphSelection={() => {
              setDraftNodesSynced(
                draftNodesRef.current.map((node) => ({ ...node, selected_for_scan: false })),
              )
            }}
            onSaveGraphSelection={() => void saveGraphSelection()}
            onExpandKnowledge={() => void startExpandKnowledge({ supplemental: (graph?.nodes?.length ?? 0) > 0 })}
          />
        </div>
      </section>

      <section id="copy-output" className="hx-copy-output-workspace space-y-4">
        <div className="hx-copy-output-toolbar">
          <div className="min-w-0 space-y-0.5">
            <h2 className="hx-copy-section-title">產文</h2>
            <p className="text-xs leading-relaxed text-ink-secondary">
              產文以人設 8D 為主，內容參考研究地圖與你勾選的延伸知識（含標籤）；按產出矩陣會自動儲存勾選。
            </p>
          </div>
          <div className="flex flex-wrap gap-2">
            <Button
              variant="soft"
              onClick={() => void startScan()}
              disabled={loading || !!scanBlockedReason}
              title={scanBlockedReason || scanClearHint}
            >
              {scanActive ? '找參考中…' : '選配：找參考貼文'}
            </Button>
            {scanActive ? (
              <a href="#copy-mission-active-jobs" className="self-center text-xs font-semibold text-brand hover:underline">
                查看任務進度
              </a>
            ) : null}
            <Button
              onClick={() => void generateMatrix()}
              disabled={loading || !!matrixBlockedReason}
              title={matrixBlockedReason || (aiReady ? undefined : '若尚未設定 AI key，後端會提示前往設定')}
            >
              {matrixActive ? '矩陣產出中…' : '產出 5 篇矩陣'}
            </Button>
          </div>
        </div>

        {capabilities && !discoverReady ? (
          <Notice
            tone="warning"
            title="海巡需要搜尋管道（擇一即可）"
            message={
              <>
                Chrome Session 爬蟲、Threads API、或 Brave/Exa Web Search 任一就緒即可海巡，不必同時具備。
                {' '}
                {capabilities.discover_hint || ''}
                {' '}
                <Link to={settingsConnectionsPath} className="font-semibold text-brand hover:underline">
                  前往連線設定
                </Link>
              </>
            }
          />
        ) : null}

        {capabilities && !aiReady ? (
          <Notice
            tone="warning"
            title="產文需要 AI API key"
            message={
              <>
                產出矩陣與深仿寫只需 AI key，與 Threads API 無關。
                {' '}
                <Link to="/settings" className="font-semibold text-brand hover:underline">
                  前往設定
                </Link>
              </>
            }
          />
        ) : null}

        <div className="space-y-3">
          <h3 className="hx-copy-subsection-title">爆款候選 · {sortedPosts.length}</h3>
          {sortedPosts.length ? (
            <ul className="hx-copy-post-list">
              {sortedPosts.map((post) => {
                const authorHref = threadsAuthorPostUrl(post.author, post.permalink)
                const tier = viralAuthorTier(post)
                const engagementTier = viralEngagementTier(post.engagement_score)
                return (
                  <li key={post.id} className={viralPostRowClass(tier)}>
                    <div className="hx-copy-post-row__meta">
                      <Badge tone="sky">{post.search_tag || '爆款'}</Badge>
                      {post.author_verified ? <VerifiedAuthorBadge compact /> : null}
                      {engagementTier === 'hot' ? <Badge tone="success">高互動</Badge> : null}
                      <span>互動 {post.engagement_score}</span>
                      {post.like_count ? <span className="text-xs text-muted">{post.like_count} 讚</span> : null}
                      {post.author ? (
                        authorHref ? (
                          <a href={authorHref} target="_blank" rel="noreferrer" className="font-semibold text-brand hover:underline">
                            @{post.author}
                          </a>
                        ) : (
                          <span>@{post.author}</span>
                        )
                      ) : null}
                    </div>
                    <p className="hx-copy-post-row__text">{post.text}</p>
                    {post.replies?.length ? (
                      <div className="hx-copy-post-row__replies">
                        {post.replies.slice(0, 2).map((reply, idx) => (
                          <p key={`${post.id}-reply-${idx}`}>
                            {reply.author ? `@${reply.author}：` : ''}
                            {reply.text}
                          </p>
                        ))}
                      </div>
                    ) : null}
                    <Button
                      variant="ghost"
                      type="button"
                      onClick={() => void generateDraft(post.id)}
                      disabled={
                        generatingId === post.id ||
                        replicateJobsByPost.has(post.id) ||
                        !has8D
                      }
                      title={!has8D ? '請先完成 8D 分析' : aiReady ? undefined : '若尚未設定 AI key，後端會提示前往設定'}
                    >
                      {generatingId === post.id || replicateJobsByPost.has(post.id)
                        ? '仿寫中…'
                        : '深仿寫'}
                    </Button>
                  </li>
                )
              })}
            </ul>
          ) : (
            <p className="hx-copy-empty">完成海巡後會列出爆款候選</p>
          )}
        </div>

        <div className="space-y-3 border-t border-line pt-4">
          <div className="flex flex-wrap items-center justify-between gap-3">
            <h3 className="hx-copy-subsection-title">產文草稿 · {sortedDrafts.length}</h3>
            {matrixDrafts.length > 0 ? (
              <div className="flex flex-wrap gap-2">
                <Button
                  variant="ghost"
                  type="button"
                  onClick={() =>
                    setSelectedMatrixDraftIds(
                      selectedMatrixDraftIds.length === matrixDrafts.length
                        ? []
                        : matrixDrafts.map((draft) => draft.id),
                    )
                  }
                  disabled={batchDeletingMatrix}
                >
                  {selectedMatrixDraftIds.length === matrixDrafts.length ? '取消全選' : '全選矩陣'}
                </Button>
                <Button
                  variant="danger"
                  type="button"
                  onClick={() => void deleteMatrixDrafts(selectedMatrixDraftIds)}
                  disabled={batchDeletingMatrix || selectedMatrixDraftIds.length === 0}
                >
                  {batchDeletingMatrix ? '刪除中…' : `刪除選取 (${selectedMatrixDraftIds.length})`}
                </Button>
                <Button
                  variant="soft"
                  type="button"
                  onClick={() => void deleteMatrixDrafts([])}
                  disabled={batchDeletingMatrix}
                >
                  刪除全部矩陣
                </Button>
              </div>
            ) : null}
          </div>
          {capabilities && !publishReady ? (
            <Notice
              tone="info"
              title="發布需要 Threads API（其餘步驟可用爬蟲）"
              message={
                <>
                  {capabilities.publish_hint || '請完成 Threads OAuth / API token 與 User ID 連線後再發布。'}
                  {' '}
                  <Link to={settingsConnectionsPath} className="font-semibold text-brand hover:underline">
                    前往連線設定
                  </Link>
                </>
              }
            />
          ) : null}
          {sortedDrafts.length ? (
            <div className="hx-copy-draft-list">
              {sortedDrafts.map((draft) => (
                <CopyMissionDraftCard
                  key={draft.id}
                  draft={draft}
                  editing={editingDraftId === draft.id}
                  edit={draftEdit(draft)}
                  onEdit={() => openDraftEdit(draft)}
                  onCancelEdit={() => closeDraftEdit(draft.id)}
                  onChange={(patch) => setDraftEdit(draft, patch)}
                  onSave={() => void saveDraft(draft)}
                  onMarkReady={() => void saveDraft(draft, true)}
                  onCopy={() => copyDraftText(draftEdit(draft).text)}
                  onPublish={() => void publishDraft(draft)}
                  onDelete={() => void deleteDraft(draft)}
                  publishReady={publishReady}
                  saving={savingDraftId === draft.id}
                  publishing={publishingDraftId === draft.id}
                  deleting={deletingDraftId === draft.id || batchDeletingMatrix}
                  batchMode={draft.draft_type === 'matrix' && matrixDrafts.length > 0}
                  batchSelected={selectedMatrixDraftIds.includes(draft.id)}
                  onBatchSelect={(selected) => toggleMatrixDraftSelection(draft.id, selected)}
                />
              ))}
            </div>
          ) : (
            <p className="hx-copy-empty">可直接用研究地圖產出矩陣；海巡後也能從候選貼文單篇深仿寫</p>
          )}
        </div>
      </section>
    </div>
  )
}
