import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'
import { useNavigate, useSearch } from '@tanstack/react-router'
import {
  Bookmark,
  Copy,
  Download,
  FileCode2,
  GitBranch,
  MoreHorizontal,
  Package,
  Pencil,
  Pin,
  PinOff,
  Play,
  Plus,
  RefreshCw,
  ScrollText,
  Search,
  Share2,
  Terminal,
  Trash2,
} from 'lucide-react'
import { api, type Agent, type Task, type TaskLog } from '@/api'
import { getNodes, type InterconnectNode } from '@/api/interconnect'
import * as interconnect from '@/api/interconnect'
import { useAgents, useTasks, useTaskTags } from '@/api/hooks'
import { LogDetailPanel } from '@/components/business/LogDetailPanel'
import { StatusDot } from '@/components/business/StatusDot'
import { RepoDialog } from '@/components/tasks/RepoDialog'
import { TaskDialog } from '@/components/tasks/TaskDialog'
import { Button, Card, Input, Modal, PageHeader, Select, Switch, Tabs } from '@/components/ui'
import { AGENT_STATUS, TASK_EVENTS, TASK_TYPE } from '@/constants'
import { useEventBus } from '@/hooks/useEventBus'
import { useLogStream } from '@/hooks/useLogStream'
import { copyToClipboard, formatDateTime, getCronDescription } from '@/lib/format'
import { generateTaskPoolCommand } from '@/lib/repo-parser'
import { toast } from '@/lib/toast'
import { cn } from '@/lib/utils'

type TaskView = {
  name: string
  isDefault?: boolean
  query: {
    name?: string
    tags?: string
    agent_id?: string | null
    type?: string
    sort_by?: string
    order?: string
  }
}

export default function Tasks() {
  const navigate = useNavigate()
  const search = useSearch({ strict: false }) as { agent_id?: string; type?: string; tags?: string }

  const [page, setPage] = useState(1)
  const [pageSize, setPageSize] = useState(() => {
    const n = Number(localStorage.getItem('tasks_page_size') || '20')
    return [10, 20, 50, 100].includes(n) ? n : 20
  })
  const [name, setName] = useState('')
  const [tags, setTags] = useState('')
  const [type, setType] = useState('all')
  const [agentId, setAgentId] = useState('')
  const [sortBy, setSortBy] = useState('created_at')
  const [order, setOrder] = useState<'asc' | 'desc'>('desc')
  const [debouncedName, setDebouncedName] = useState('')

  const [showTaskDialog, setShowTaskDialog] = useState(false)
  const [showRepoDialog, setShowRepoDialog] = useState(false)
  const [editing, setEditing] = useState<Partial<Task> | undefined>()
  const [isEdit, setIsEdit] = useState(false)
  const [deleteId, setDeleteId] = useState<string | null>(null)
  const [deleteFiles, setDeleteFiles] = useState(false)
  const [batchDeleteOpen, setBatchDeleteOpen] = useState(false)
  const [executingId, setExecutingId] = useState<string | null>(null)
  const [menuId, setMenuId] = useState<string | null>(null)

  const [logOpen, setLogOpen] = useState(false)
  const [selectedLog, setSelectedLog] = useState<TaskLog | null>(null)
  const [stopping, setStopping] = useState(false)
  const [logEmpty, setLogEmpty] = useState<{ title: string; desc: string } | null>(null)
  const { content, loading: logLoading, liveLog } = useLogStream(selectedLog)

  const [views, setViews] = useState<TaskView[]>([])
  const [viewsOpen, setViewsOpen] = useState(false)
  const [newViewName, setNewViewName] = useState('')
  const [savingView, setSavingView] = useState(false)
  const [message, setMessage] = useState('')

  const [exportOpen, setExportOpen] = useState(false)
  const [exportCmd, setExportCmd] = useState('')
  const [depCmdOpen, setDepCmdOpen] = useState(false)
  const [depCmd, setDepCmd] = useState('')

  const [syncOpen, setSyncOpen] = useState(false)
  const [syncTask, setSyncTask] = useState<Task | null>(null)
  const [syncNodes, setSyncNodes] = useState<InterconnectNode[]>([])
  const [syncSelected, setSyncSelected] = useState<string[]>([])
  const [syncing, setSyncing] = useState(false)

  const params = useMemo(
    () => ({
      page,
      page_size: pageSize,
      name: debouncedName || undefined,
      tags: tags || undefined,
      type: type === 'all' ? undefined : type,
      agent_id: agentId || undefined,
      sort_by: sortBy,
      order,
    }),
    [page, pageSize, debouncedName, tags, type, agentId, sortBy, order],
  )

  const { data, isLoading, refetch, isFetching } = useTasks(params)
  const { data: tagsData } = useTaskTags()
  const { data: agentsData } = useAgents()
  const agents = Array.isArray(agentsData) ? agentsData : []
  const tagList = Array.isArray(tagsData) ? tagsData : []
  const items = Array.isArray(data?.data) ? data.data : []
  const total = data?.total || 0
  const totalPages = Math.max(1, Math.ceil(total / pageSize))

  useEffect(() => {
    localStorage.setItem('tasks_page_size', String(pageSize))
  }, [pageSize])

  const agentMap = useMemo(() => {
    const m: Record<string, Agent> = {}
    agents.forEach((a) => {
      m[a.id] = a
    })
    return m
  }, [agents])

  useEffect(() => {
    const t = setTimeout(() => {
      setDebouncedName(name)
      setPage(1)
    }, 300)
    return () => clearTimeout(t)
  }, [name])

  useEffect(() => {
    if (!message) return
    const t = setTimeout(() => setMessage(''), 3000)
    return () => clearTimeout(t)
  }, [message])

  const onTaskEvent = useCallback(() => {
    refetch()
  }, [refetch])

  useEventBus(
    [TASK_EVENTS.SUCCESS, TASK_EVENTS.FAILED, TASK_EVENTS.TIMEOUT, TASK_EVENTS.RUNNING, TASK_EVENTS.QUEUED, TASK_EVENTS.CANCELLED],
    onTaskEvent,
  )

  useEffect(() => {
    if (search.agent_id !== undefined) setAgentId(search.agent_id || '')
    if (search.type) setType(search.type)
    if (search.tags !== undefined) setTags(search.tags || '')
  }, [search.agent_id, search.type, search.tags])

  useEffect(() => {
    if (!menuId && !viewsOpen) return
    const onDoc = () => {
      setMenuId(null)
      setViewsOpen(false)
    }
    document.addEventListener('click', onDoc)
    return () => document.removeEventListener('click', onDoc)
  }, [menuId, viewsOpen])

  useEffect(() => {
    ;(async () => {
      try {
        const res = await api.settings.getSection('task_qviews')
        const raw = res?.['task_views']
        if (!raw) return
        const list = JSON.parse(raw) as TaskView[]
        if (!Array.isArray(list)) return
        setViews(list)
        if (!search.agent_id) {
          const def = list.find((v) => v.isDefault)
          if (def) applyView(def)
        }
      } catch {
        // ignore
      }
    })()
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

  function applyView(view: TaskView, reload = true) {
    setName(view.query.name || '')
    setDebouncedName(view.query.name || '')
    setTags(view.query.tags || '')
    setAgentId(view.query.agent_id || '')
    setType(view.query.type || 'all')
    setSortBy(view.query.sort_by || 'created_at')
    setOrder((view.query.order as 'asc' | 'desc') || 'desc')
    setPage(1)
    if (reload) refetch()
  }

  async function saveCurrentView() {
    if (!newViewName.trim()) {
      setMessage('请输入视图名称')
      toast.warning('请输入视图名称')
      return
    }
    const next: TaskView = {
      name: newViewName.trim(),
      isDefault: false,
      query: {
        name: debouncedName || name,
        tags,
        agent_id: agentId || null,
        type,
        sort_by: sortBy,
        order,
      },
    }
    const updated = [...views, next]
    setSavingView(true)
    try {
      await api.settings.setSection('task_qviews', { task_views: JSON.stringify(updated) })
      setViews(updated)
      setNewViewName('')
      setMessage('视图已保存')
      toast.success('视图已保存')
    } catch (e: any) {
      setMessage(e?.message || '保存视图失败')
      toast.error(e?.message || '保存视图失败')
    } finally {
      setSavingView(false)
    }
  }

  async function deleteView(index: number) {
    const updated = views.filter((_, i) => i !== index)
    try {
      await api.settings.setSection('task_qviews', { task_views: JSON.stringify(updated) })
      setViews(updated)
      setMessage('视图已删除')
      toast.success('视图已删除')
    } catch (e: any) {
      setMessage(e?.message || '删除失败')
      toast.error(e?.message || '删除失败')
    }
  }

  async function toggleDefaultView(index: number) {
    const currently = !!views[index]?.isDefault
    const updated = views.map((v, i) => ({ ...v, isDefault: i === index ? !currently : false }))
    try {
      await api.settings.setSection('task_qviews', { task_views: JSON.stringify(updated) })
      setViews(updated)
      const msg = currently ? '已取消默认视图' : '已设为默认视图'
      setMessage(msg)
      toast.success(msg)
    } catch (e: any) {
      setMessage(e?.message || '设置失败')
      toast.error(e?.message || '设置失败')
    }
  }

  function openCreate() {
    setEditing({
      name: '',
      command: '',
      type: TASK_TYPE.NORMAL,
      schedule: '0 * * * * *',
      timeout: 30,
      enabled: true,
    })
    setIsEdit(false)
    setShowTaskDialog(true)
  }

  function openCreateRepo() {
    setEditing({ name: '', type: TASK_TYPE.REPO, schedule: '0 0 0 * * *', enabled: true })
    setIsEdit(false)
    setShowRepoDialog(true)
  }

  function openEdit(task: Task) {
    setEditing({ ...task })
    setIsEdit(true)
    if (task.type === TASK_TYPE.REPO) setShowRepoDialog(true)
    else setShowTaskDialog(true)
  }

  function duplicateTask(task: Task) {
    const next = { ...task }
    delete (next as any).id
    delete (next as any).last_run
    delete (next as any).next_run
    next.name = `${next.name} - 副本`
    setEditing(next)
    setIsEdit(false)
    if (task.type === TASK_TYPE.REPO) setShowRepoDialog(true)
    else setShowTaskDialog(true)
  }

  async function viewLogs(taskId: string) {
    try {
      const res = await api.logs.list({ task_id: taskId, page: 1, page_size: 1 })
      const latest = res?.data?.[0]
      if (latest) {
        setSelectedLog(latest)
        setLogEmpty(null)
        setLogOpen(true)
        return
      }
      const task = items.find((t) => t.id === taskId)
      setSelectedLog({
        id: '',
        task_id: taskId,
        task_name: task?.name || '未知任务',
        command: task?.command || '',
        status: 'UNEXECUTED',
        duration: 0,
        start_time: '-',
        end_time: '-',
      } as TaskLog)
      setLogEmpty({
        title: '该任务暂无执行记录',
        desc: '此任务尚未被触发执行，目前没有任何运行日志产生。',
      })
      setLogOpen(true)
    } catch (e: any) {
      setMessage(e?.message || '获取日志失败')
      toast.error(e?.message || '获取日志失败')
    }
  }

  async function runTask(id: string) {
    if (executingId) return
    setExecutingId(id)
    try {
      const res = await api.tasks.execute(id)
      if (res?.log_id) {
        await viewLogs(id)
      }
      refetch()
      toast.success('任务已触发执行')
    } catch (e: any) {
      toast.error(e?.message || '执行失败')
    } finally {
      setExecutingId(null)
    }
  }

  async function handleTaskDepInstall(task: Task) {
    try {
      const res = await api.logs.list({ task_id: task.id, page: 1, page_size: 1 })
      const latest = res?.data?.[0]
      if (!latest) {
        setMessage('该任务尚未运行，无日志可分析')
        return
      }
      const cmdRes = await api.deps.getInstallSuggestCmd(latest.id)
      if (cmdRes?.command) {
        setDepCmd(cmdRes.command)
        setDepCmdOpen(true)
      } else {
        setMessage('未生成依赖安装命令')
      }
    } catch (e: any) {
      setMessage(e?.message || '获取依赖安装命令失败')
    }
  }

  function openExportDialog(task: Task) {
    const cmd = generateTaskPoolCommand(task)
    if (!cmd) {
      setMessage('生成导出指令失败')
      toast.error('生成导出指令失败')
      return
    }
    setExportCmd(cmd)
    setExportOpen(true)
  }

  async function openSyncDialog(task: Task) {
    setSyncTask(task)
    setSyncSelected([])
    setSyncOpen(true)
    try {
      const nodes = await getNodes()
      setSyncNodes(Array.isArray(nodes) ? nodes : [])
    } catch {
      setSyncNodes([])
      setMessage('获取互联节点失败')
    }
  }

  async function confirmSyncTask() {
    if (!syncTask || syncSelected.length === 0) {
      setMessage('请选择至少一个目标节点')
      toast.warning('请选择至少一个目标节点')
      return
    }
    setSyncing(true)
    try {
      await interconnect.syncTask(syncSelected, [syncTask])
      setMessage('任务已同步到所选节点')
      toast.success('任务同步成功')
      setSyncOpen(false)
    } catch (e: any) {
      setMessage(e?.message || '同步失败')
      toast.error(e?.message || '同步失败')
    } finally {
      setSyncing(false)
    }
  }

  async function toggleEnabled(task: Task, enabled: boolean) {
    try {
      await api.tasks.update(task.id, { ...task, enabled })
      toast.success(enabled ? '任务已启用' : '任务已禁用')
      refetch()
    } catch (e: any) {
      toast.error(e?.message || '操作失败')
    }
  }

  async function togglePin(task: Task) {
    const pin_type = task.pin_type === 'top' ? 'none' : 'top'
    try {
      await api.tasks.update(task.id, { ...task, pin_type })
      toast.success(pin_type === 'top' ? '已置顶' : '已取消置顶')
      refetch()
    } catch (e: any) {
      toast.error(e?.message || '置顶失败')
    }
  }

  async function confirmDelete() {
    if (!deleteId) return
    try {
      await api.tasks.delete(deleteId, { delete_files: deleteFiles })
      setDeleteId(null)
      refetch()
      toast.success('任务已删除')
    } catch (e: any) {
      toast.error(e?.message || '删除失败')
    }
  }

  async function batchDelete() {
    try {
      await api.tasks.batchDeleteByQuery({
        name: debouncedName || undefined,
        tags: tags || undefined,
        type: type === 'all' ? undefined : type,
        agent_id: agentId || undefined,
      })
      setBatchDeleteOpen(false)
      refetch()
      toast.success('批量删除完成')
    } catch (e: any) {
      toast.error(e?.message || '批量删除失败')
    }
  }

  async function stopLog() {
    const id = liveLog?.id || selectedLog?.id
    if (!id) return
    setStopping(true)
    try {
      await api.tasks.stop(id)
      toast.success('已发送停止指令')
    } catch (e: any) {
      toast.error(e?.message || '停止失败')
    } finally {
      setStopping(false)
      refetch()
    }
  }

  function executorLabel(task: Task) {
    if (!task.agent_id) return '本地'
    return agentMap[task.agent_id]?.name || `Agent #${task.agent_id}`
  }

  function executorOnline(task: Task) {
    if (!task.agent_id) return 'local'
    return agentMap[task.agent_id]?.status === AGENT_STATUS.ONLINE ? 'online' : 'offline'
  }

  return (
    <div>
      <PageHeader
        title="定时任务"
        description="创建、管理与执行任务 / 仓库同步"
        actions={
          <>
            <Button variant="secondary" onClick={() => refetch()} disabled={isFetching}>
              <RefreshCw size={14} className={isFetching ? 'animate-spin' : ''} />
              刷新
            </Button>
            <Button variant="secondary" onClick={openCreateRepo}>
              <GitBranch size={14} />
              仓库同步
            </Button>
            <Button onClick={openCreate}>
              <Plus size={14} />
              新建任务
            </Button>
          </>
        }
      />

      <div className="mb-4 flex flex-wrap items-center gap-2">
        <Tabs
          value={type}
          onValueChange={(v) => {
            setType(v)
            setPage(1)
          }}
          items={[
            { value: 'all', label: '全部' },
            { value: TASK_TYPE.NORMAL, label: '普通任务' },
            { value: TASK_TYPE.REPO, label: '仓库同步' },
          ]}
        />
        <div className="relative min-w-[200px] max-w-xs flex-1">
          <Search size={14} className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-[var(--text-muted)]" />
          <Input className="pl-9" placeholder="搜索任务名" value={name} onChange={(e) => setName(e.target.value)} />
        </div>
        <Select
          className="max-w-[160px]"
          value={tags}
          onChange={(e) => {
            setTags(e.target.value)
            setPage(1)
          }}
        >
          <option value="">全部标签</option>
          {tagList.map((tag) => (
            <option key={tag} value={tag}>
              {tag}
            </option>
          ))}
        </Select>
        <Select
          className="max-w-[160px]"
          value={agentId}
          onChange={(e) => {
            setAgentId(e.target.value)
            setPage(1)
          }}
        >
          <option value="">全部节点</option>
          <option value="local">仅本地</option>
          {agents.map((a) => (
            <option key={a.id} value={a.id}>
              {a.name || a.id}
            </option>
          ))}
        </Select>
        <Select
          className="max-w-[180px]"
          value={`${sortBy}:${order}`}
          onChange={(e) => {
            const [s, o] = e.target.value.split(':')
            setSortBy(s || 'created_at')
            setOrder((o as 'asc' | 'desc') || 'desc')
            setPage(1)
          }}
        >
          <option value="created_at:desc">创建时间 ↓</option>
          <option value="created_at:asc">创建时间 ↑</option>
          <option value="name:asc">名称 A-Z</option>
          <option value="name:desc">名称 Z-A</option>
          <option value="next_run:asc">下次运行 ↑</option>
          <option value="last_run:desc">上次运行 ↓</option>
        </Select>
        <div className="relative">
          <Button
            variant="secondary"
            size="sm"
            onClick={(e) => {
              e.stopPropagation()
              setViewsOpen((v) => !v)
            }}
          >
            <Bookmark size={14} />
            视图
          </Button>
          {viewsOpen ? (
            <div
              className="absolute right-0 top-9 z-30 w-72 rounded-md border border-[var(--border)] bg-[var(--bg-secondary)] p-3 shadow-[var(--shadow-md)]"
              onClick={(e) => e.stopPropagation()}
            >
              <div className="mb-2 text-xs font-medium text-[var(--text-muted)]">筛选视图</div>
              <div className="mb-2 max-h-40 space-y-1 overflow-y-auto">
                {views.length === 0 ? (
                  <div className="py-2 text-xs text-[var(--text-muted)]">暂无已保存视图</div>
                ) : (
                  views.map((view, index) => (
                    <div key={`${view.name}-${index}`} className="flex items-center gap-1 rounded px-1 py-1 hover:bg-[var(--bg-hover)]">
                      <button
                        type="button"
                        className="min-w-0 flex-1 truncate text-left text-sm"
                        onClick={() => {
                          applyView(view)
                          setViewsOpen(false)
                        }}
                      >
                        {view.name}
                        {view.isDefault ? <span className="ml-1 text-[10px] text-amber-500">默认</span> : null}
                      </button>
                      <button
                        type="button"
                        className="rounded p-1 text-[var(--text-muted)] hover:bg-[var(--bg-tertiary)]"
                        title="设为默认"
                        onClick={() => toggleDefaultView(index)}
                      >
                        {view.isDefault ? <Pin size={12} /> : <PinOff size={12} />}
                      </button>
                      <button
                        type="button"
                        className="rounded p-1 text-[var(--danger)] hover:bg-[var(--bg-tertiary)]"
                        title="删除"
                        onClick={() => deleteView(index)}
                      >
                        <Trash2 size={12} />
                      </button>
                    </div>
                  ))
                )}
              </div>
              <div className="flex gap-2">
                <Input placeholder="新视图名称" value={newViewName} onChange={(e) => setNewViewName(e.target.value)} />
                <Button size="sm" disabled={savingView} onClick={saveCurrentView}>
                  保存
                </Button>
              </div>
            </div>
          ) : null}
        </div>
        <Button variant="danger" size="sm" disabled={total === 0} onClick={() => setBatchDeleteOpen(true)}>
          <Trash2 size={14} />
          清空筛选结果
        </Button>
      </div>

      {message ? (
        <div className="mb-3 rounded-md border border-[var(--border)] bg-[var(--bg-tertiary)] px-3 py-2 text-sm">{message}</div>
      ) : null}

      <Card className="overflow-hidden">
        {isLoading ? (
          <div className="p-6 text-[var(--text-muted)]">加载中...</div>
        ) : items.length === 0 ? (
          <div className="p-6 text-[var(--text-muted)]">暂无任务</div>
        ) : (
          <div className="overflow-x-auto">
            <table className="w-full min-w-[960px] text-left text-sm">
              <thead>
                <tr className="border-b border-[var(--border)] bg-[var(--bg-tertiary)] text-[var(--text-muted)]">
                  <th className="px-4 py-3 font-medium">名称</th>
                  <th className="px-4 py-3 font-medium">类型/位置</th>
                  <th className="px-4 py-3 font-medium">调度</th>
                  <th className="px-4 py-3 font-medium">状态</th>
                  <th className="px-4 py-3 font-medium">上次/下次</th>
                  <th className="px-4 py-3 font-medium">启用</th>
                  <th className="px-4 py-3 text-right font-medium">操作</th>
                </tr>
              </thead>
              <tbody>
                {items.map((task) => {
                  const online = executorOnline(task)
                  return (
                    <tr key={task.id} className={cn('border-b border-[var(--border)]', task.pin_type === 'top' && 'bg-[var(--bg-tertiary)]/40')}>
                      <td className="px-4 py-3">
                        <div className="flex items-center gap-2">
                          {task.pin_type === 'top' ? <Pin size={12} className="text-[var(--warning)]" /> : null}
                          {task.type === TASK_TYPE.REPO ? <GitBranch size={14} className="text-[var(--text-muted)]" /> : null}
                          <div className="min-w-0">
                            <div className="font-medium">{task.name}</div>
                            <div className="mt-1 max-w-md truncate text-xs text-[var(--text-muted)]">{task.command || task.remark || '-'}</div>
                            {task.tags ? (
                              <div className="mt-1 flex flex-wrap gap-1">
                                {task.tags
                                  .split(',')
                                  .filter(Boolean)
                                  .map((t) => (
                                    <span key={t} className="rounded bg-[var(--bg-selected)] px-1.5 py-0.5 text-[10px]">
                                      {t.trim()}
                                    </span>
                                  ))}
                              </div>
                            ) : null}
                          </div>
                        </div>
                      </td>
                      <td className="px-4 py-3 text-[var(--text-secondary)]">
                        <div>{task.type === TASK_TYPE.REPO ? '仓库同步' : '普通任务'}</div>
                        <div className="mt-1 flex items-center gap-1.5 text-xs">
                          <span
                            className={cn(
                              'h-1.5 w-1.5 rounded-full',
                              online === 'local' ? 'bg-[var(--info)]' : online === 'online' ? 'bg-[var(--success)]' : 'bg-[var(--text-muted)]',
                            )}
                          />
                          {executorLabel(task)}
                        </div>
                      </td>
                      <td className="px-4 py-3">
                        <div className="font-mono text-xs">{task.schedule || '-'}</div>
                        <div className="mt-1 text-[10px] text-[var(--text-muted)]">{getCronDescription(task.schedule)}</div>
                      </td>
                      <td className="px-4 py-3">
                        <span className="inline-flex items-center gap-1.5 text-xs">
                          <StatusDot state={task.running_status || (task.enabled ? 'success' : 'cancelled')} />
                          {task.running_status || (task.enabled ? '就绪' : '禁用')}
                        </span>
                      </td>
                      <td className="px-4 py-3 text-xs text-[var(--text-muted)]">
                        <div>上次 {formatDateTime(task.last_run)}</div>
                        <div className="mt-1">下次 {formatDateTime(task.next_run)}</div>
                      </td>
                      <td className="px-4 py-3">
                        <Switch checked={!!task.enabled} onCheckedChange={(v) => toggleEnabled(task, v)} />
                      </td>
                      <td className="px-4 py-3">
                        <div className="relative flex justify-end gap-1">
                          <Button size="icon" variant="ghost" title="执行" disabled={!!executingId} onClick={() => runTask(task.id)}>
                            {executingId === task.id ? <RefreshCw size={14} className="animate-spin" /> : <Play size={14} />}
                          </Button>
                          <Button size="icon" variant="ghost" title="编辑" onClick={() => openEdit(task)}>
                            <Pencil size={14} />
                          </Button>
                          <Button
                            size="icon"
                            variant="ghost"
                            title="更多"
                            onClick={(e) => {
                              e.stopPropagation()
                              setMenuId(menuId === task.id ? null : task.id)
                            }}
                          >
                            <MoreHorizontal size={14} />
                          </Button>
                          {menuId === task.id ? (
                            <div className="absolute right-0 top-9 z-20 min-w-[160px] rounded-md border border-[var(--border)] bg-[var(--bg-secondary)] py-1 shadow-[var(--shadow-md)]">
                              <MenuItem
                                onClick={() => {
                                  setMenuId(null)
                                  viewLogs(task.id)
                                }}
                              >
                                <ScrollText size={14} />
                                查看日志
                              </MenuItem>
                              <MenuItem
                                onClick={() => {
                                  setMenuId(null)
                                  togglePin(task)
                                }}
                              >
                                {task.pin_type === 'top' ? <PinOff size={14} /> : <Pin size={14} />}
                                {task.pin_type === 'top' ? '取消置顶' : '置顶'}
                              </MenuItem>
                              <MenuItem
                                onClick={() => {
                                  setMenuId(null)
                                  duplicateTask(task)
                                }}
                              >
                                <Copy size={14} />
                                复制任务
                              </MenuItem>
                              <MenuItem
                                onClick={async () => {
                                  setMenuId(null)
                                  const ok = await copyToClipboard(task.command || task.name)
                                  setMessage(ok ? '命令已复制' : '复制失败')
                                  if (ok) toast.success('命令已复制')
                                  else toast.error('复制失败')
                                }}
                              >
                                <Copy size={14} />
                                复制命令
                              </MenuItem>
                              {task.type === TASK_TYPE.REPO ? (
                                <MenuItem
                                  onClick={() => {
                                    setMenuId(null)
                                    openExportDialog(task)
                                  }}
                                >
                                  <Download size={14} />
                                  导出同步指令
                                </MenuItem>
                              ) : null}
                              <MenuItem
                                onClick={() => {
                                  setMenuId(null)
                                  handleTaskDepInstall(task)
                                }}
                              >
                                <Package size={14} />
                                补全依赖
                              </MenuItem>
                              <MenuItem
                                onClick={() => {
                                  setMenuId(null)
                                  openSyncDialog(task)
                                }}
                              >
                                <Share2 size={14} />
                                同步到节点
                              </MenuItem>
                              {task.command?.includes('/') || task.command?.includes('\\') ? (
                                <MenuItem
                                  onClick={() => {
                                    setMenuId(null)
                                    navigate({ to: '/editor', search: { file: task.command } })
                                  }}
                                >
                                  <FileCode2 size={14} />
                                  打开脚本
                                </MenuItem>
                              ) : null}
                              <MenuItem
                                danger
                                onClick={() => {
                                  setMenuId(null)
                                  setDeleteId(task.id)
                                  setDeleteFiles(false)
                                }}
                              >
                                <Trash2 size={14} />
                                删除
                              </MenuItem>
                            </div>
                          ) : null}
                        </div>
                      </td>
                    </tr>
                  )
                })}
              </tbody>
            </table>
          </div>
        )}
        <div className="flex flex-wrap items-center justify-between gap-3 border-t border-[var(--border)] px-4 py-3">
          <span className="text-sm text-[var(--text-muted)]">
            共 {total} 条 · 第 {page}/{totalPages} 页
          </span>
          <div className="flex flex-wrap items-center gap-2">
            <Select
              className="w-[110px]"
              value={String(pageSize)}
              onChange={(e) => {
                setPageSize(Number(e.target.value) || 20)
                setPage(1)
              }}
            >
              <option value="10">10 条/页</option>
              <option value="20">20 条/页</option>
              <option value="50">50 条/页</option>
              <option value="100">100 条/页</option>
            </Select>
            <Button variant="secondary" size="sm" disabled={page <= 1} onClick={() => setPage((p) => p - 1)}>
              上一页
            </Button>
            <Button variant="secondary" size="sm" disabled={page >= totalPages} onClick={() => setPage((p) => p + 1)}>
              下一页
            </Button>
          </div>
        </div>
      </Card>

      <TaskDialog open={showTaskDialog} task={editing} isEdit={isEdit} onClose={() => setShowTaskDialog(false)} onSaved={() => refetch()} />
      <RepoDialog open={showRepoDialog} task={editing} isEdit={isEdit} onClose={() => setShowRepoDialog(false)} onSaved={() => refetch()} />

      <Modal
        open={!!deleteId}
        title="删除任务"
        onClose={() => setDeleteId(null)}
        footer={
          <>
            <Button variant="secondary" onClick={() => setDeleteId(null)}>
              取消
            </Button>
            <Button variant="danger" onClick={confirmDelete}>
              确认删除
            </Button>
          </>
        }
      >
        <p className="text-sm text-[var(--text-secondary)]">确认删除该任务？此操作不可恢复。</p>
        <label className="mt-3 flex items-center gap-2 text-sm">
          <input type="checkbox" checked={deleteFiles} onChange={(e) => setDeleteFiles(e.target.checked)} />
          同时删除关联文件
        </label>
      </Modal>

      <Modal
        open={batchDeleteOpen}
        title="批量删除"
        onClose={() => setBatchDeleteOpen(false)}
        footer={
          <>
            <Button variant="secondary" onClick={() => setBatchDeleteOpen(false)}>
              取消
            </Button>
            <Button variant="danger" onClick={batchDelete}>
              删除当前筛选全部
            </Button>
          </>
        }
      >
        <p className="text-sm text-[var(--text-secondary)]">将删除当前筛选条件下的全部任务（共 {total} 条），确认继续？</p>
      </Modal>

      <Modal
        open={logOpen}
        title="执行日志"
        onClose={() => {
          setLogOpen(false)
          setLogEmpty(null)
        }}
        size="xl"
        className="!p-0"
      >
        <div className="-m-4">
          {logEmpty && !(liveLog || selectedLog)?.id ? (
            <div className="space-y-2 p-8 text-center">
              <div className="text-sm font-medium">{logEmpty.title}</div>
              <div className="text-sm text-[var(--text-muted)]">{logEmpty.desc}</div>
            </div>
          ) : (
            <LogDetailPanel
              log={liveLog || selectedLog}
              content={content}
              loading={logLoading}
              isStopping={stopping}
              onClose={() => {
                setLogOpen(false)
                setLogEmpty(null)
              }}
              onStop={stopLog}
            />
          )}
        </div>
      </Modal>

      <Modal
        open={exportOpen}
        title="导出同步指令"
        onClose={() => setExportOpen(false)}
        footer={
          <>
            <Button variant="secondary" onClick={() => setExportOpen(false)}>
              关闭
            </Button>
            <Button
              onClick={async () => {
                const ok = await copyToClipboard(exportCmd)
                setMessage(ok ? '同步指令已复制' : '复制失败，请手动复制')
                if (ok) setExportOpen(false)
              }}
            >
              <Copy size={14} />
              复制
            </Button>
          </>
        }
      >
        <pre className="max-h-64 overflow-auto whitespace-pre-wrap rounded-md bg-[var(--bg-primary)] p-3 font-mono text-xs">{exportCmd}</pre>
      </Modal>

      <Modal
        open={depCmdOpen}
        title="补全依赖命令"
        onClose={() => setDepCmdOpen(false)}
        footer={
          <>
            <Button variant="secondary" onClick={() => setDepCmdOpen(false)}>
              关闭
            </Button>
            <Button
              variant="secondary"
              onClick={async () => {
                const ok = await copyToClipboard(depCmd)
                setMessage(ok ? '命令已复制' : '复制失败')
              }}
            >
              <Copy size={14} />
              复制
            </Button>
            <Button
              onClick={() => {
                setDepCmdOpen(false)
                try {
                  sessionStorage.setItem('taskpool_terminal_cmd', depCmd)
                } catch {
                  // ignore
                }
                navigate({ to: '/terminal' })
              }}
            >
              <Terminal size={14} />
              去终端执行
            </Button>
          </>
        }
      >
        <pre className="max-h-64 overflow-auto whitespace-pre-wrap rounded-md bg-[var(--bg-primary)] p-3 font-mono text-xs">{depCmd}</pre>
      </Modal>

      <Modal
        open={syncOpen}
        title="同步任务到节点"
        onClose={() => setSyncOpen(false)}
        footer={
          <>
            <Button variant="secondary" onClick={() => setSyncOpen(false)}>
              取消
            </Button>
            <Button onClick={confirmSyncTask} disabled={syncing || syncSelected.length === 0}>
              {syncing ? '同步中...' : '确认同步'}
            </Button>
          </>
        }
      >
        <div className="space-y-3">
          <p className="text-sm text-[var(--text-secondary)]">
            将任务 <span className="font-medium">{syncTask?.name}</span> 同步到所选子节点
          </p>
          {syncNodes.length === 0 ? (
            <div className="text-sm text-[var(--text-muted)]">暂无可用节点，请先在互联管理中添加</div>
          ) : (
            <div className="max-h-56 space-y-2 overflow-y-auto">
              {syncNodes.map((node) => {
                const checked = syncSelected.includes(node.id)
                return (
                  <label
                    key={node.id}
                    className="flex cursor-pointer items-center gap-2 rounded border border-[var(--border)] px-3 py-2 text-sm hover:bg-[var(--bg-hover)]"
                  >
                    <input
                      type="checkbox"
                      checked={checked}
                      onChange={(e) => {
                        setSyncSelected((prev) => (e.target.checked ? [...prev, node.id] : prev.filter((id) => id !== node.id)))
                      }}
                    />
                    <span className="min-w-0 flex-1 truncate">{node.name}</span>
                    <span className="text-xs text-[var(--text-muted)]">{node.status || 'unknown'}</span>
                  </label>
                )
              })}
            </div>
          )}
        </div>
      </Modal>
    </div>
  )
}

function MenuItem({
  children,
  onClick,
  danger,
}: {
  children: React.ReactNode
  onClick: () => void
  danger?: boolean
}) {
  return (
    <button
      type="button"
      className={cn(
        'flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-[var(--bg-tertiary)]',
        danger && 'text-[var(--danger)]',
      )}
      onClick={onClick}
    >
      {children}
    </button>
  )
}
