import { useState, useRef, useEffect } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import {
  ArrowLeft, FolderOpen, Database, BookOpen, Search,
  CheckSquare, Square, RefreshCw, Play, Pause, Trash2,
  ChevronRight, ChevronDown, FileText, File, AlertCircle,
  Zap, Layers, Network, CheckCircle, XCircle, Clock,
  Eye, EyeOff, Folder, Settings2, BookMarked,
} from 'lucide-react'
import toast from 'react-hot-toast'
import { knowledgeApi, type KBFile, type KBJob, type WikiPage, type KBDirNode, type KBChunk, type WikiSchedule } from '../api/knowledge'
import { providersApi, type Provider } from '../api/providers'
import { Spinner, Button } from '../components/ui'
import { useAppStore } from '../store/app'

// ─── Job Status Badge ──────────────────────────────────────────

function JobStatusBadge({ status }: { status: KBJob['status'] | string }) {
  const cfg: Record<string, { icon: React.ReactNode; label: string; cls: string }> = {
    pending: { icon: <Clock size={12}/>, label: '等待中', cls: 'text-yellow-700 bg-yellow-100' },
    running: { icon: <RefreshCw size={12} className="animate-spin"/>, label: '运行中', cls: 'text-blue-700 bg-blue-100' },
    completed: { icon: <CheckCircle size={12}/>, label: '完成', cls: 'text-green-700 bg-green-100' },
    failed: { icon: <XCircle size={12}/>, label: '失败', cls: 'text-red-700 bg-red-100' },
  }
  const c = cfg[status as string] ?? { icon: null, label: status, cls: 'text-gray-600 bg-gray-100' }

  return (
    <span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${c.cls}`}>
      {c.icon}{c.label}
    </span>
  )
}

// ─── Tab 1: File Manager ───────────────────────────────────────

// Recursive tree node for a single directory
function DirTreeNode({
  kbId,
  node,
  ignoredSet,
  ancestorIgnored,
  depth,
  onToggle,
  saving,
}: {
  kbId: string
  node: KBDirNode
  ignoredSet: Set<string>
  ancestorIgnored: boolean
  depth: number
  onToggle: (path: string, shouldIgnore: boolean) => void
  saving: boolean
}) {
  const [expanded, setExpanded] = useState(false)
  // Children are loaded lazily: null = not yet fetched, [] = fetched (possibly empty)
  const [lazyChildren, setLazyChildren] = useState<KBDirNode[] | null>(
    node.children.length > 0 ? node.children : null
  )
  const [loadingChildren, setLoadingChildren] = useState(false)

  const selfIgnored = ignoredSet.has(node.relative_path)
  const effectivelyIgnored = selfIgnored || ancestorIgnored
  const canExpand = node.has_children || node.children.length > 0

  const handleExpand = async () => {
    const next = !expanded
    setExpanded(next)
    if (next && lazyChildren === null) {
      setLoadingChildren(true)
      try {
        const res = await knowledgeApi.listDirs(kbId, node.relative_path)
        setLazyChildren(res.dirs)
      } catch {
        setLazyChildren([])
      } finally {
        setLoadingChildren(false)
      }
    }
  }

  return (
    <div>
      <div
        className={`flex items-center gap-1.5 py-1.5 pr-3 group
          ${effectivelyIgnored ? 'bg-orange-50/60' : 'hover:bg-gray-50'}`}
        style={{ paddingLeft: `${12 + depth * 18}px` }}
      >
        {/* Expand / collapse arrow */}
        <button
          onClick={handleExpand}
          className={`shrink-0 text-gray-400 hover:text-gray-600 ${canExpand ? '' : 'invisible'}`}
        >
          {loadingChildren
            ? <RefreshCw size={13} className="animate-spin text-indigo-400"/>
            : expanded ? <ChevronDown size={13}/> : <ChevronRight size={13}/>}
        </button>

        <Folder
          size={14}
          className={selfIgnored ? 'text-orange-300 shrink-0' : ancestorIgnored ? 'text-gray-300 shrink-0' : 'text-indigo-400 shrink-0'}
        />

        <span
          className={`flex-1 text-sm font-mono truncate
            ${selfIgnored ? 'line-through text-gray-400'
              : ancestorIgnored ? 'text-gray-400'
              : 'text-gray-700'}`}
        >
          {node.name}/
        </span>

        {ancestorIgnored && !selfIgnored && (
          <span className="text-xs text-gray-400 px-1.5">继承</span>
        )}

        {!ancestorIgnored && (
          <button
            onClick={() => onToggle(node.relative_path, !selfIgnored)}
            disabled={saving}
            title={selfIgnored ? '恢复显示此目录' : '隐藏此目录（含子目录）'}
            className={`shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 transition-opacity
              ${selfIgnored
                ? 'text-orange-500 hover:bg-orange-100 !opacity-100'
                : 'text-gray-400 hover:text-gray-700 hover:bg-gray-200'}`}
          >
            {selfIgnored ? <Eye size={14}/> : <EyeOff size={14}/>}
          </button>
        )}
      </div>

      {/* Lazily-loaded children */}
      {expanded && lazyChildren && lazyChildren.map(child => (
        <DirTreeNode
          key={child.relative_path}
          kbId={kbId}
          node={child}
          ignoredSet={ignoredSet}
          ancestorIgnored={effectivelyIgnored}
          depth={depth + 1}
          onToggle={onToggle}
          saving={saving}
        />
      ))}
    </div>
  )
}

// Count all ignored entries (own + descendants)
function countIgnored(nodes: KBDirNode[], ignoredSet: Set<string>): number {
  let n = 0
  for (const node of nodes) {
    if (ignoredSet.has(node.relative_path)) n++
    n += countIgnored(node.children, ignoredSet)
  }
  return n
}

// Collect all ignored paths recursively so "全部恢复" works
function collectIgnoredPaths(nodes: KBDirNode[], ignoredSet: Set<string>, acc: string[] = []): string[] {
  for (const node of nodes) {
    if (ignoredSet.has(node.relative_path)) acc.push(node.relative_path)
    collectIgnoredPaths(node.children, ignoredSet, acc)
  }
  return acc
}

function DirIgnorePanel({
  kbId,
  dirs,
  ignoredDirs,
  onToggle,
  onClearAll,
  saving,
}: {
  kbId: string
  dirs: KBDirNode[]
  ignoredDirs: string[]
  onToggle: (path: string, shouldIgnore: boolean) => void
  onClearAll: () => void
  saving: boolean
}) {
  const [open, setOpen] = useState(false)
  const ignoredSet = new Set(ignoredDirs)
  const ignoredCount = countIgnored(dirs, ignoredSet)

  return (
    <div className="border border-gray-200 rounded-xl overflow-hidden">
      <button
        onClick={() => setOpen(o => !o)}
        className="w-full flex items-center justify-between px-4 py-2.5 bg-gray-50 hover:bg-gray-100 transition-colors"
      >
        <div className="flex items-center gap-2 text-sm font-medium text-gray-700">
          <Settings2 size={14} className="text-gray-500" />
          目录过滤
          {ignoredCount > 0 && (
            <span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-xs bg-orange-100 text-orange-700 font-medium">
              <EyeOff size={10}/> {ignoredCount} 已隐藏
            </span>
          )}
        </div>
        <ChevronDown size={14} className={`text-gray-400 transition-transform ${open ? 'rotate-180' : ''}`}/>
      </button>

      {open && (
        <>
          {dirs.length === 0 ? (
            <p className="px-4 py-3 text-xs text-gray-400">根目录下无子文件夹</p>
          ) : (
            <div className="py-1 max-h-72 overflow-y-auto">
              {dirs.map(node => (
                <DirTreeNode
                  key={node.relative_path}
                  kbId={kbId}
                  node={node}
                  ignoredSet={ignoredSet}
                  ancestorIgnored={false}
                  depth={0}
                  onToggle={onToggle}
                  saving={saving}
                />
              ))}
            </div>
          )}
          {ignoredCount > 0 && (
            <div className="px-4 py-2 bg-orange-50 border-t border-orange-100 flex items-center justify-between">
              <p className="text-xs text-orange-600">已隐藏的目录不会出现在文件列表中，也不会被加入知识体。</p>
              <button onClick={onClearAll} disabled={saving}
                className="text-xs text-orange-700 hover:underline ml-4 shrink-0 disabled:opacity-50">
                全部恢复
              </button>
            </div>
          )}
        </>
      )}
    </div>
  )
}

function FileManager({ kbId }: { kbId: string }) {
  const qc = useQueryClient()
  const [selected, setSelected] = useState<Set<string>>(new Set())
  // Current browsed subdirectory path (empty = root)
  const [currentPath, setCurrentPath] = useState('')
  // Filename search (debounced to query)
  const [nameInput, setNameInput] = useState('')
  const [nameFilter, setNameFilter] = useState('')

  // Debounce name filter
  useEffect(() => {
    const t = setTimeout(() => setNameFilter(nameInput), 350)
    return () => clearTimeout(t)
  }, [nameInput])

  const { data, isLoading, isFetching, refetch } = useQuery({
    queryKey: ['kb-files', kbId, currentPath, nameFilter],
    queryFn: () => knowledgeApi.listFiles(kbId, { path: currentPath, q: nameFilter }),
    keepPreviousData: true,
  } as any)

  const addMut = useMutation({
    mutationFn: (paths: string[]) => knowledgeApi.addFiles(kbId, paths),
    onSuccess: (r) => {
      toast.success(`已加入 ${r.added} 个文件`)
      setSelected(new Set())
      qc.invalidateQueries({ queryKey: ['kb-files', kbId] })
    },
    onError: (e: any) => toast.error(e.message ?? '加入失败'),
  })

  const ignoreMut = useMutation({
    mutationFn: (ignored: string[]) => knowledgeApi.updateIgnoreDirs(kbId, ignored),
    onSuccess: () => qc.invalidateQueries({ queryKey: ['kb-files', kbId] }),
    onError: (e: any) => toast.error(e.message ?? '保存失败'),
  })

  const handleToggleDir = (dirPath: string, shouldIgnore: boolean) => {
    const current = new Set((data as any)?.ignored_dirs ?? [])
    if (shouldIgnore) current.add(dirPath)
    else current.delete(dirPath)
    ignoreMut.mutate(Array.from(current) as string[])
  }

  const handleClearAll = () => ignoreMut.mutate([])

  const d = data as any
  const files: KBFile[] = d?.files ?? []
  const dirs = d?.dirs ?? []
  const ignoredDirs: string[] = d?.ignored_dirs ?? []
  const notAdded = files.filter(f => !f.added)
  const added = files.filter(f => f.added)

  const toggle = (path: string) => setSelected(s => {
    const n = new Set(s); n.has(path) ? n.delete(path) : n.add(path); return n
  })
  const toggleAll = () => {
    if (selected.size === notAdded.length) setSelected(new Set())
    else setSelected(new Set(notAdded.map(f => f.path)))
  }

  const extIcon = (type: string) => {
    if (type === 'pdf') return <FileText size={14} className="text-red-400"/>
    if (type === 'md') return <FileText size={14} className="text-blue-400"/>
    return <File size={14} className="text-gray-400"/>
  }
  const fmtSize = (b: number) => b > 1024*1024 ? `${(b/1024/1024).toFixed(1)}MB` : `${(b/1024).toFixed(0)}KB`

  // Breadcrumb parts
  const crumbs = currentPath ? currentPath.split('/') : []
  const navigateTo = (idx: number) => {
    setCurrentPath(idx < 0 ? '' : crumbs.slice(0, idx + 1).join('/'))
    setSelected(new Set())
  }

  return (
    <div className="space-y-4">
      {/* Header row */}
      <div className="flex items-center justify-between gap-2 flex-wrap">
        <p className="text-xs text-gray-500">
          根目录: <code className="bg-gray-100 px-1 rounded">{d?.root_dir}</code>
        </p>
        <div className="flex gap-2 flex-wrap">
          <Button variant="secondary" size="sm" icon={<RefreshCw size={13} className={isFetching ? 'animate-spin' : ''}/>} onClick={() => refetch()}>刷新</Button>
          {selected.size > 0 && (
            <Button size="sm" icon={<Play size={13}/>}
              onClick={() => addMut.mutate(Array.from(selected))}
              disabled={addMut.isPending}>
              加入选中 ({selected.size})
            </Button>
          )}
          {notAdded.length > 0 && (
            <Button size="sm" variant="secondary"
              onClick={() => addMut.mutate(notAdded.map(f => f.path))}
              disabled={addMut.isPending}>
              一键加入全部 ({notAdded.length})
            </Button>
          )}
        </div>
      </div>

      {/* Breadcrumb + search row */}
      <div className="flex items-center gap-2 flex-wrap">
        {/* Breadcrumb */}
        <div className="flex items-center gap-1 text-sm min-w-0 flex-1">
          <button onClick={() => navigateTo(-1)}
            className={`flex items-center gap-1 px-2 py-1 rounded hover:bg-gray-100 transition-colors shrink-0 ${currentPath === '' ? 'text-gray-400 cursor-default pointer-events-none' : 'text-indigo-600'}`}>
            <FolderOpen size={13}/> <span className="text-xs">根目录</span>
          </button>
          {crumbs.map((part, idx) => (
            <span key={idx} className="flex items-center gap-1 min-w-0">
              <ChevronRight size={12} className="text-gray-300 shrink-0"/>
              <button onClick={() => navigateTo(idx)}
                className={`px-1.5 py-0.5 rounded text-xs font-mono hover:bg-gray-100 truncate max-w-[140px] ${idx === crumbs.length - 1 ? 'text-gray-700 font-semibold' : 'text-indigo-600'}`}>
                {part}
              </button>
            </span>
          ))}
        </div>
        {/* Search box */}
        <div className="relative shrink-0">
          <Search size={13} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none"/>
          <input
            value={nameInput}
            onChange={e => setNameInput(e.target.value)}
            placeholder="文件名过滤..."
            className="pl-7 pr-3 py-1.5 text-xs border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-400 outline-none w-44"
          />
        </div>
      </div>

      {/* Directory ignore panel */}
      {dirs.length > 0 && (
        <DirIgnorePanel
          kbId={kbId}
          dirs={dirs}
          ignoredDirs={ignoredDirs}
          onToggle={handleToggleDir}
          onClearAll={handleClearAll}
          saving={ignoreMut.isPending}
        />
      )}

      {isLoading ? (
        <div className="flex justify-center py-8"><Spinner /></div>
      ) : (
        <>
          {/* Already added */}
          {added.length > 0 && (
            <div className="bg-green-50 border border-green-200 rounded-lg overflow-hidden">
              <div className="px-4 py-2 bg-green-100 text-xs font-semibold text-green-700">
                已加入知识体 ({added.length})
              </div>
              <div className="divide-y divide-green-100 max-h-48 overflow-y-auto">
                {added.map(f => (
                  <div key={f.path} className="flex items-center gap-2 px-4 py-2 text-sm">
                    {extIcon(f.type)}
                    <span className="flex-1 truncate text-gray-700 font-mono text-xs">{f.relative_path}</span>
                    <span className="text-xs text-gray-400 shrink-0">{fmtSize(f.size)}</span>
                    <CheckCircle size={14} className="text-green-500 shrink-0"/>
                  </div>
                ))}
              </div>
            </div>
          )}

          {/* Not yet added */}
          {notAdded.length > 0 && (
            <div className="border border-gray-200 rounded-lg overflow-hidden">
              <div className="px-4 py-2 bg-gray-50 text-xs font-semibold text-gray-600 flex items-center gap-2">
                <button onClick={toggleAll} className="text-indigo-600 hover:text-indigo-800">
                  {selected.size === notAdded.length ? <CheckSquare size={14}/> : <Square size={14}/>}
                </button>
                待加入 ({notAdded.length})
                {nameFilter && <span className="font-normal text-gray-400">（含过滤条件"{nameFilter}"）</span>}
              </div>
              <div className="divide-y divide-gray-100 max-h-96 overflow-y-auto">
                {notAdded.map(f => (
                  <div key={f.path} onClick={() => toggle(f.path)}
                    className={`flex items-center gap-2 px-4 py-2 text-sm cursor-pointer hover:bg-gray-50 ${selected.has(f.path) ? 'bg-indigo-50' : ''}`}>
                    <button onClick={e => { e.stopPropagation(); toggle(f.path) }}>
                      {selected.has(f.path) ? <CheckSquare size={14} className="text-indigo-500"/> : <Square size={14} className="text-gray-300"/>}
                    </button>
                    {extIcon(f.type)}
                    <span className="flex-1 truncate text-gray-700 font-mono text-xs">{f.relative_path}</span>
                    <span className="text-xs text-gray-400 shrink-0">{fmtSize(f.size)}</span>
                  </div>
                ))}
              </div>
            </div>
          )}

          {files.length === 0 && (
            <div className="text-center py-10 text-gray-400">
              <FolderOpen size={32} className="mx-auto mb-2"/>
              <p className="text-sm">
                {nameFilter ? `没有匹配"${nameFilter}"的文件` :
                  ignoredDirs.length > 0
                    ? `此目录下未找到文件（${ignoredDirs.length} 个目录已隐藏）`
                    : '此目录没有支持的文件（PDF/TXT/DOCX/MD/HTML/CSV）'}
              </p>
            </div>
          )}
        </>
      )}
    </div>
  )
}

// ─── Chunk Browser ────────────────────────────────────────────

function ChunkBrowser({ kbId }: { kbId: string }) {
  const [open, setOpen] = useState(false)
  const [q, setQ] = useState('')
  const [srcFilter, setSrcFilter] = useState('')
  const [page, setPage] = useState(1)
  const [expandedId, setExpandedId] = useState<string | null>(null)
  const [inputQ, setInputQ] = useState('')
  const [inputSrc, setInputSrc] = useState('')

  const { data, isLoading, isFetching } = useQuery({
    queryKey: ['kb-chunks', kbId, q, srcFilter, page],
    queryFn: () => knowledgeApi.listChunks(kbId, { q, source: srcFilter, page, limit: 20 }),
    enabled: open,
    keepPreviousData: true,
  } as any)

  const result = data as any

  const doSearch = () => {
    setQ(inputQ)
    setSrcFilter(inputSrc)
    setPage(1)
    setExpandedId(null)
  }

  const srcColor = (src: string) => {
    // Deterministic pastel color from source name hash
    let h = 0
    for (let i = 0; i < src.length; i++) h = (h * 31 + src.charCodeAt(i)) & 0xffff
    const hue = h % 360
    return `hsl(${hue},55%,88%)`
  }

  return (
    <div className="border border-gray-200 rounded-xl overflow-hidden">
      {/* Header toggle */}
      <button
        onClick={() => setOpen(o => !o)}
        className="w-full flex items-center justify-between px-4 py-2.5 bg-gray-50 hover:bg-gray-100 transition-colors"
      >
        <div className="flex items-center gap-2 text-sm font-medium text-gray-700">
          <Search size={14} className="text-gray-500" />
          分块浏览
          {result?.total_all !== undefined && (
            <span className="text-xs text-gray-400 font-normal">共 {result.total_all.toLocaleString()} 个 chunk</span>
          )}
        </div>
        <ChevronDown size={14} className={`text-gray-400 transition-transform ${open ? 'rotate-180' : ''}`} />
      </button>

      {open && (
        <div className="p-4 space-y-3">
          {/* Search bar */}
          <div className="flex gap-2">
            <input
              value={inputQ}
              onChange={e => setInputQ(e.target.value)}
              onKeyDown={e => e.key === 'Enter' && doSearch()}
              placeholder="关键词过滤文本内容..."
              className="flex-1 border border-gray-300 rounded-lg px-3 py-1.5 text-xs focus:ring-2 focus:ring-indigo-400 outline-none"
            />
            <input
              value={inputSrc}
              onChange={e => setInputSrc(e.target.value)}
              onKeyDown={e => e.key === 'Enter' && doSearch()}
              placeholder="文件名..."
              className="w-32 border border-gray-300 rounded-lg px-3 py-1.5 text-xs focus:ring-2 focus:ring-indigo-400 outline-none"
            />
            <Button size="sm" onClick={doSearch} icon={<Search size={12}/>}>搜索</Button>
            {(q || srcFilter) && (
              <button onClick={() => { setQ(''); setSrcFilter(''); setInputQ(''); setInputSrc(''); setPage(1) }}
                className="text-xs text-gray-400 hover:text-gray-700 px-2">清除</button>
            )}
          </div>

          {/* Result count */}
          {result && (q || srcFilter) && (
            <p className="text-xs text-gray-500">
              过滤结果：<b>{result.total.toLocaleString()}</b> 个 chunk（共 {result.total_all.toLocaleString()}）
            </p>
          )}

          {/* Chunk list */}
          {isLoading ? (
            <div className="flex justify-center py-6"><Spinner /></div>
          ) : (
            <div className="space-y-2">
              {result?.chunks?.map((c: KBChunk) => {
                const isExpanded = expandedId === c.id
                return (
                  <div key={c.id} className="border border-gray-100 rounded-lg overflow-hidden hover:border-gray-300 transition-colors">
                    {/* Chunk header */}
                    <button
                      onClick={() => setExpandedId(isExpanded ? null : c.id)}
                      className="w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-gray-50"
                    >
                      <span
                        className="shrink-0 px-1.5 py-0.5 rounded text-xs font-mono font-medium"
                        style={{ backgroundColor: srcColor(c.source), color: '#374151' }}
                      >
                        {c.source.length > 20 ? '…' + c.source.slice(-20) : c.source}
                      </span>
                      <span className="text-xs text-gray-400 shrink-0">#{c.chunk_id}</span>
                      <span className="text-xs text-gray-400 shrink-0">{c.length}字</span>
                      <span className="flex-1 text-xs text-gray-600 truncate">
                        {c.text.slice(0, 100)}
                      </span>
                      {isExpanded
                        ? <ChevronDown size={12} className="text-gray-400 shrink-0"/>
                        : <ChevronRight size={12} className="text-gray-400 shrink-0"/>}
                    </button>
                    {/* Expanded full text */}
                    {isExpanded && (
                      <div className="px-3 pb-3 pt-1 bg-gray-50 border-t border-gray-100">
                        <pre className="text-xs text-gray-700 whitespace-pre-wrap leading-relaxed font-sans">{c.text}</pre>
                      </div>
                    )}
                  </div>
                )
              })}
              {result?.chunks?.length === 0 && (
                <p className="text-center py-6 text-xs text-gray-400">未找到匹配的 chunk</p>
              )}
            </div>
          )}

          {/* Pagination */}
          {result && result.pages > 1 && (
            <div className="flex items-center justify-center gap-2 pt-1">
              <button disabled={page === 1} onClick={() => setPage(p => p - 1)}
                className="px-2 py-1 text-xs border border-gray-200 rounded disabled:opacity-40 hover:bg-gray-50">
                ← 上一页
              </button>
              <span className="text-xs text-gray-500">{page} / {result.pages}</span>
              <button disabled={page >= result.pages} onClick={() => setPage(p => p + 1)}
                className="px-2 py-1 text-xs border border-gray-200 rounded disabled:opacity-40 hover:bg-gray-50">
                下一页 →
              </button>
            </div>
          )}
        </div>
      )}
    </div>
  )
}

// ─── Shared: Log Terminal ──────────────────────────────────────

function LogTerminal({ log, running, maxH = 'max-h-72' }: { log: string; running: boolean; maxH?: string }) {
  const bottomRef = useRef<HTMLDivElement>(null)
  useEffect(() => {
    if (running) bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
  }, [log, running])

  if (!log) return null
  return (
    <div className={`relative bg-gray-950 rounded-lg overflow-auto ${maxH}`}>
      <pre className="text-green-400 text-xs p-3 whitespace-pre-wrap font-mono leading-relaxed">{log}</pre>
      {running && (
        <span className="absolute bottom-2 right-3 text-green-500 text-xs animate-pulse">● 运行中</span>
      )}
      <div ref={bottomRef} />
    </div>
  )
}

// ─── Tab 2: Index Builder ──────────────────────────────────────

function IndexStats({ kbId }: { kbId: string }) {
  const { data: kb, refetch } = useQuery({
    queryKey: ['kb-detail', kbId],
    queryFn: () => knowledgeApi.get(kbId),
    refetchInterval: 5000,
  })
  const s = kb?.index_status
  if (!s) return null

  const fmtSize = (bytes?: number) =>
    bytes ? bytes > 1024 * 1024 ? `${(bytes / 1024 / 1024).toFixed(1)} MB` : `${(bytes / 1024).toFixed(0)} KB` : ''

  return (
    <div className="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-4">
      {/* BM25 */}
      <div className={`rounded-xl border p-3 ${s.bm25.exists ? 'border-blue-200 bg-blue-50' : 'border-gray-200 bg-gray-50'}`}>
        <div className="flex items-center gap-1.5 mb-1.5">
          <Database size={13} className={s.bm25.exists ? 'text-blue-500' : 'text-gray-400'} />
          <span className="text-xs font-semibold text-gray-700">BM25 关键词</span>
          {s.bm25.exists
            ? <CheckCircle size={11} className="text-green-500 ml-auto" />
            : <XCircle size={11} className="text-gray-300 ml-auto" />}
        </div>
        {s.bm25.exists ? (
          <div className="space-y-0.5">
            <p className="text-sm font-bold text-blue-700">{s.bm25.total_chunks?.toLocaleString() ?? '—'} chunks</p>
            <p className="text-xs text-gray-500">{s.bm25.total_files?.toLocaleString()} 个文件</p>
          </div>
        ) : (
          <p className="text-xs text-gray-400">未构建</p>
        )}
      </div>

      {/* Vector */}
      <div className={`rounded-xl border p-3 ${s.vector.exists ? 'border-purple-200 bg-purple-50' : 'border-gray-200 bg-gray-50'}`}>
        <div className="flex items-center gap-1.5 mb-1.5">
          <Layers size={13} className={s.vector.exists ? 'text-purple-500' : 'text-gray-400'} />
          <span className="text-xs font-semibold text-gray-700">向量语义</span>
          {s.vector.exists
            ? <CheckCircle size={11} className="text-green-500 ml-auto" />
            : <XCircle size={11} className="text-gray-300 ml-auto" />}
        </div>
        {s.vector.exists
          ? <p className="text-xs text-gray-500">已构建</p>
          : <p className="text-xs text-gray-400">未构建</p>}
      </div>

      {/* Wiki */}
      <div className={`rounded-xl border p-3 ${s.wiki.exists ? 'border-green-200 bg-green-50' : 'border-gray-200 bg-gray-50'}`}>
        <div className="flex items-center gap-1.5 mb-1.5">
          <Network size={13} className={s.wiki.exists ? 'text-green-500' : 'text-gray-400'} />
          <span className="text-xs font-semibold text-gray-700">Wiki 图谱</span>
          {s.wiki.exists
            ? <CheckCircle size={11} className="text-green-500 ml-auto" />
            : <XCircle size={11} className="text-gray-300 ml-auto" />}
        </div>
        {s.wiki.exists ? (
          <div className="space-y-0.5">
            <p className="text-sm font-bold text-green-700">{s.wiki.total_pages} 页</p>
            {s.wiki.subdirs && (
              <p className="text-xs text-gray-500">
                {Object.entries(s.wiki.subdirs).filter(([,v]) => v > 0).map(([k,v]) => `${k}:${v}`).join(' · ')}
              </p>
            )}
          </div>
        ) : (
          <p className="text-xs text-gray-400">未编译</p>
        )}
      </div>

      {/* PageIndex */}
      {(() => {
        const pi = (s as any).pageindex ?? { exists: false }
        return (
          <div className={`rounded-xl border p-3 ${pi.exists ? 'border-orange-200 bg-orange-50' : 'border-gray-200 bg-gray-50'}`}>
            <div className="flex items-center gap-1.5 mb-1.5">
              <BookMarked size={13} className={pi.exists ? 'text-orange-500' : 'text-gray-400'} />
              <span className="text-xs font-semibold text-gray-700">PageIndex</span>
              {pi.exists
                ? <CheckCircle size={11} className="text-green-500 ml-auto" />
                : <XCircle size={11} className="text-gray-300 ml-auto" />}
            </div>
            {pi.exists ? (
              <div className="space-y-0.5">
                <p className="text-sm font-bold text-orange-700">{pi.total_pages?.toLocaleString() ?? '—'} 页</p>
                <p className="text-xs text-gray-500">{pi.total_docs?.toLocaleString() ?? '—'} 个文档</p>
              </div>
            ) : (
              <p className="text-xs text-gray-400">未构建</p>
            )}
          </div>
        )
      })()}
    </div>
  )
}

function IndexBuilder({ kbId }: { kbId: string }) {
  const [activeLog, setActiveLog] = useState<string | null>(null)
  const [chunkStrategy, setChunkStrategy] = useState<'token' | 'contextual' | 'page'>('token')
  const qc = useQueryClient()

  const { data: jobsData, refetch: refetchJobs } = useQuery({
    queryKey: ['kb-jobs', kbId],
    queryFn: () => knowledgeApi.listJobs(kbId),
    refetchInterval: 2000,
  })

  // Poll active job for live log
  const jobs = jobsData?.jobs ?? []
  const runningJob = jobs.find(j => j.status === 'running')

  const { data: liveJob } = useQuery({
    queryKey: ['kb-job-live', kbId, runningJob?.id],
    queryFn: () => knowledgeApi.getJob(kbId, runningJob!.id),
    enabled: !!runningJob,
    refetchInterval: 1500,
  })

  const buildMut = useMutation({
    mutationFn: ({ type, mode }: { type: string; mode: string }) =>
      knowledgeApi.buildIndex(kbId, type, mode, chunkStrategy),
    onSuccess: (r) => {
      toast.success(`已触发构建 (Job: ${r.job_id})`)
      setActiveLog(r.job_id)
      qc.invalidateQueries({ queryKey: ['kb-jobs', kbId] })
      qc.invalidateQueries({ queryKey: ['kb-detail', kbId] })
    },
    onError: (e: any) => toast.error(e.message ?? '触发失败'),
  })

  const latestByType = (type: string) =>
    jobs.find(j => j.job_type.startsWith(`index:${type}`) || j.job_type === type)

  const indexTypes = [
    { key: 'bm25', label: 'BM25 关键词索引', desc: '轻量、无需 GPU，适合精确关键词检索', icon: <Database size={18} className="text-blue-500"/> },
    { key: 'vector', label: '向量语义索引', desc: '需要 bge-m3 嵌入服务，支持语义搜索', icon: <Layers size={18} className="text-purple-500"/> },
    { key: 'graph', label: '图谱索引', desc: '从 Wiki 编译结果构建实体关系图', icon: <Network size={18} className="text-green-500"/> },
  ]

  // Determine which log to show for a given job id
  const getDisplayLog = (jobId: string) => {
    if (liveJob?.id === jobId) return liveJob.log
    return jobs.find(j => j.id === jobId)?.log ?? ''
  }

  return (
    <div className="space-y-4">
      {/* Index status cards */}
      <IndexStats kbId={kbId} />

      <div className="flex items-center justify-between">
        <div className="flex items-center gap-3">
          <label className="text-xs font-medium text-gray-600">分块策略:</label>
          <select
            value={chunkStrategy}
            onChange={e => setChunkStrategy(e.target.value as 'token' | 'contextual' | 'page')}
            className="text-xs border border-gray-300 rounded px-2 py-1"
          >
            <option value="token">Token 分块（默认）</option>
            <option value="contextual">Contextual 分块（LLM 增强）</option>
            <option value="page">PageIndex（页面级，无向量）</option>
          </select>
          {chunkStrategy === 'contextual' && (
            <span className="text-xs text-amber-600 bg-amber-50 px-2 py-0.5 rounded">
              每篇文档额外 1 次 LLM 调用生成摘要
            </span>
          )}
          {chunkStrategy === 'page' && (
            <span className="text-xs text-blue-600 bg-blue-50 px-2 py-0.5 rounded">
              构建 rag_page_index.json，使用 pageindex 模式搜索
            </span>
          )}
        </div>
        <Button variant="secondary" size="sm" icon={<RefreshCw size={13}/>}
          onClick={() => { refetchJobs(); qc.invalidateQueries({ queryKey: ['kb-detail', kbId] }) }}>
          刷新状态
        </Button>
      </div>

      {indexTypes.map(({ key, label, desc, icon }) => {
        const latest = latestByType(key)
        const isRunning = latest?.status === 'running'
        const logVisible = activeLog === latest?.id
        const displayLog = latest ? getDisplayLog(latest.id) : ''

        return (
          <div key={key} className="border border-gray-200 rounded-xl p-4">
            <div className="flex items-start justify-between gap-4">
              <div className="flex items-start gap-3">
                <div className="mt-0.5">{icon}</div>
                <div>
                  <p className="text-sm font-semibold text-gray-800">{label}</p>
                  <p className="text-xs text-gray-500 mt-0.5">{desc}</p>
                  {latest && (
                    <div className="flex items-center gap-2 mt-1.5">
                      <JobStatusBadge status={latest.status}/>
                      {latest.created_at && (
                        <span className="text-xs text-gray-400">{latest.created_at.slice(0, 16)}</span>
                      )}
                    </div>
                  )}
                </div>
              </div>
              <div className="flex gap-2 shrink-0">
                <Button size="sm" variant="secondary"
                  onClick={() => buildMut.mutate({ type: key, mode: 'incremental' })}
                  disabled={buildMut.isPending || isRunning}>
                  增量更新
                </Button>
                <Button size="sm"
                  onClick={() => buildMut.mutate({ type: key, mode: 'full' })}
                  disabled={buildMut.isPending || isRunning}
                  icon={<Play size={12}/>}>
                  全量构建
                </Button>
              </div>
            </div>

            {latest && (
              <div className="mt-3 space-y-2">
                {/* Log toggle */}
                {displayLog && (
                  <button
                    onClick={() => setActiveLog(logVisible ? null : latest.id)}
                    className="text-xs text-indigo-600 hover:underline flex items-center gap-1"
                  >
                    {logVisible ? <ChevronDown size={12}/> : <ChevronRight size={12}/>}
                    {isRunning ? '查看实时日志' : '查看日志'}
                    {isRunning && <RefreshCw size={10} className="text-indigo-400 animate-spin ml-1"/>}
                  </button>
                )}
                {logVisible && <LogTerminal log={displayLog} running={isRunning} />}

                {latest.error && (
                  <div className="flex items-center gap-2 text-xs text-red-600 bg-red-50 rounded p-2">
                    <AlertCircle size={12}/> {latest.error}
                  </div>
                )}
              </div>
            )}
          </div>
        )
      })}

      {/* Chunk browser — only show when BM25 index exists */}
      <ChunkBrowser kbId={kbId} />
    </div>
  )
}

// ─── Tab 3: Wiki Browser ───────────────────────────────────────

function WikiBrowser({ kbId }: { kbId: string }) {
  const qc = useQueryClient()
  const [selectedPage, setSelectedPage] = useState<string | null>(null)
  const [pageContent, setPageContent] = useState('')
  const [pageLoading, setPageLoading] = useState(false)
  const [concurrency, setConcurrency] = useState(2)
  const [wikiMode, setWikiMode] = useState<'incremental' | 'rebuild'>('incremental')
  const [showSchedule, setShowSchedule] = useState(false)

  const { data: tree, isLoading: treeLoading, refetch: refetchTree } = useQuery({
    queryKey: ['kb-wiki-tree', kbId],
    queryFn: () => knowledgeApi.wikiTree(kbId),
  })

  const [showLog, setShowLog] = useState(false)

  const { data: jobsData } = useQuery({
    queryKey: ['kb-jobs', kbId],
    queryFn: () => knowledgeApi.listJobs(kbId),
    refetchInterval: 2000,
  })

  const wikiJob = jobsData?.jobs.find(j => j.job_type === 'wiki')
  const isRunning = wikiJob?.status === 'running'

  // Poll live job log when running
  const { data: liveWikiJob } = useQuery({
    queryKey: ['kb-wiki-live', kbId, wikiJob?.id],
    queryFn: () => knowledgeApi.getJob(kbId, wikiJob!.id),
    enabled: !!wikiJob && isRunning,
    refetchInterval: 1500,
  })

  const displayLog = (liveWikiJob ?? wikiJob)?.log ?? ''

  // Auto-show log when job starts
  useEffect(() => {
    if (isRunning) setShowLog(true)
  }, [isRunning])

  const compileMut = useMutation({
    mutationFn: () => knowledgeApi.buildWiki(kbId, wikiMode, concurrency),
    onSuccess: () => {
      toast.success('Wiki 编译已启动')
      setShowLog(true)
      qc.invalidateQueries({ queryKey: ['kb-jobs', kbId] })
    },
    onError: (e: any) => toast.error(e.message ?? '启动失败'),
  })

  const pauseMut = useMutation({
    mutationFn: () => knowledgeApi.pauseWiki(kbId),
    onSuccess: (r) => toast.success(r.action === 'paused' ? '已暂停' : '已恢复'),
  })

  const delAnalysisMut = useMutation({
    mutationFn: (name: string) => knowledgeApi.deleteAnalysis(kbId, name),
    onSuccess: () => { toast.success('已删除'); refetchTree() },
  })

  const { data: scheduleData, refetch: refetchSchedule } = useQuery({
    queryKey: ['kb-wiki-schedule', kbId],
    queryFn: () => knowledgeApi.wikiSchedule(kbId),
  })

  const scheduleMut = useMutation({
    mutationFn: (data: Partial<WikiSchedule>) => knowledgeApi.updateWikiSchedule(kbId, data),
    onSuccess: () => { toast.success('定时设置已更新'); refetchSchedule() },
    onError: (e: any) => toast.error(e.message ?? '更新失败'),
  })

  const loadPage = async (path: string) => {
    setSelectedPage(path)
    setPageLoading(true)
    try {
      const r = await knowledgeApi.wikiPage(kbId, path)
      setPageContent(r.content)
    } catch {
      setPageContent('页面加载失败')
    } finally {
      setPageLoading(false)
    }
  }

  const fmtSize = (b: number) => b > 1024 ? `${(b/1024).toFixed(1)}K` : `${b}B`

  const PageList = ({ pages, cat }: { pages: WikiPage[]; cat: string }) => (
    <div className="mb-3">
      <p className="text-xs font-semibold text-gray-500 uppercase px-2 mb-1">{cat} ({pages.length})</p>
      <div className="space-y-0.5">
        {pages.map(p => (
          <button key={p.path}
            onClick={() => loadPage(p.path)}
            className={`w-full text-left flex items-center justify-between px-2 py-1.5 rounded text-xs hover:bg-gray-100 ${selectedPage === p.path ? 'bg-indigo-50 text-indigo-700 font-medium' : 'text-gray-700'}`}
          >
            <span className="truncate">{p.name}</span>
            <div className="flex items-center gap-1 shrink-0">
              <span className="text-gray-400">{fmtSize(p.size)}</span>
              {cat === 'analyses' && (
                <button onClick={e => { e.stopPropagation(); delAnalysisMut.mutate(p.name) }}
                  className="text-gray-300 hover:text-red-500 ml-1"><Trash2 size={11}/></button>
              )}
            </div>
          </button>
        ))}
      </div>
    </div>
  )

  return (
    <div className="space-y-4">
      {/* Compile panel */}
      <div className="border border-gray-200 rounded-xl p-4">
        <div className="flex items-center justify-between mb-3">
          <div>
            <p className="text-sm font-semibold text-gray-800">Wiki 编译控制</p>
            <p className="text-xs text-gray-500 mt-0.5">LLM 逐文档阅读生成 sources/entities/topics 三层知识页面</p>
          </div>
          {wikiJob && <JobStatusBadge status={wikiJob.status}/>}
        </div>
        <div className="flex items-center gap-4 mb-3">
          <div className="flex items-center gap-2">
            <label className="text-xs text-gray-600">模式:</label>
            <select value={wikiMode} onChange={e => setWikiMode(e.target.value as 'incremental' | 'rebuild')}
              className="text-xs border border-gray-300 rounded px-2 py-1">
              <option value="incremental">增量（仅新文档）</option>
              <option value="rebuild">全量重建</option>
            </select>
          </div>
          <div className="flex items-center gap-2">
            <label className="text-xs text-gray-600">并发数:</label>
            <input type="number" min={1} max={8} value={concurrency}
              onChange={e => setConcurrency(Number(e.target.value))}
              className="w-16 text-xs border border-gray-300 rounded px-2 py-1" />
          </div>
        </div>
        <div className="flex gap-2 flex-wrap">
          <Button size="sm" icon={<Play size={12}/>}
            onClick={() => compileMut.mutate()} disabled={compileMut.isPending || isRunning}>
            开始编译
          </Button>
          <Button size="sm" variant="secondary" icon={<Pause size={12}/>}
            onClick={() => pauseMut.mutate()}>
            暂停/恢复
          </Button>
          <Button size="sm" variant="secondary" icon={<RefreshCw size={13}/>}
            onClick={() => { refetchTree(); qc.invalidateQueries({ queryKey: ['kb-jobs', kbId] }) }}>
            刷新
          </Button>
          {displayLog && (
            <button
              onClick={() => setShowLog(v => !v)}
              className="text-xs text-indigo-600 hover:underline flex items-center gap-1 ml-auto"
            >
              {showLog ? <ChevronDown size={12}/> : <ChevronRight size={12}/>}
              {isRunning ? '实时日志' : '查看日志'}
              {isRunning && <RefreshCw size={10} className="animate-spin text-indigo-400 ml-1"/>}
            </button>
          )}
        </div>

        {/* Live / historical log terminal */}
        {showLog && displayLog && (
          <div className="mt-3">
            <LogTerminal log={displayLog} running={isRunning} maxH="max-h-64" />
          </div>
        )}

        {wikiJob?.error && (
          <div className="mt-2 flex items-center gap-2 text-xs text-red-600 bg-red-50 rounded p-2">
            <AlertCircle size={12}/> {wikiJob.error}
          </div>
        )}

        {/* Schedule toggle */}
        <div className="mt-3 border-t border-gray-100 pt-3">
          <button
            onClick={() => setShowSchedule(v => !v)}
            className="text-xs text-gray-500 hover:text-gray-700 flex items-center gap-1"
          >
            {showSchedule ? <ChevronDown size={12}/> : <ChevronRight size={12}/>}
            定时自动重编译
            {scheduleData?.enabled && (
              <span className="ml-1 px-1.5 py-0.5 bg-green-100 text-green-700 rounded text-xs font-medium">已开启</span>
            )}
          </button>
          {showSchedule && (
            <div className="mt-2 flex items-center gap-4 flex-wrap">
              <label className="flex items-center gap-2 text-xs text-gray-700">
                <input
                  type="checkbox"
                  checked={scheduleData?.enabled ?? false}
                  onChange={e => scheduleMut.mutate({ enabled: e.target.checked, interval: scheduleData?.interval ?? 'weekly' })}
                  className="rounded"
                />
                启用定时编译
              </label>
              <div className="flex items-center gap-2">
                <label className="text-xs text-gray-600">周期:</label>
                <select
                  value={scheduleData?.interval ?? 'weekly'}
                  onChange={e => scheduleMut.mutate({ enabled: scheduleData?.enabled ?? false, interval: e.target.value as 'daily' | 'weekly' })}
                  className="text-xs border border-gray-300 rounded px-2 py-1"
                  disabled={!scheduleData?.enabled}
                >
                  <option value="daily">每天</option>
                  <option value="weekly">每周</option>
                </select>
              </div>
              {scheduleData?.next_run && scheduleData.enabled && (
                <span className="text-xs text-gray-400">下次: {scheduleData.next_run.slice(0, 16)}</span>
              )}
              {scheduleData?.last_run && (
                <span className="text-xs text-gray-400">上次: {scheduleData.last_run.slice(0, 16)}</span>
              )}
            </div>
          )}
        </div>
      </div>

      {/* Wiki browser */}
      {treeLoading ? (
        <div className="flex justify-center p-4"><Spinner /></div>
      ) : !tree?.sources.length && !tree?.entities.length && !tree?.topics.length ? (
        <div className="text-center py-8 text-gray-400">
          <BookOpen size={28} className="mx-auto mb-2"/>
          <p className="text-sm">Wiki 尚未生成。点击"开始编译"后，LLM 将为每个文档生成知识页面。</p>
        </div>
      ) : (
        <div className="flex gap-4 min-h-[400px]">
          {/* Sidebar */}
          <div className="w-56 shrink-0 border border-gray-200 rounded-xl overflow-y-auto p-2">
            {tree?.index_content && (
              <button
                onClick={() => setPageContent(tree.index_content)}
                className={`w-full text-left px-2 py-1.5 rounded text-xs mb-2 hover:bg-gray-100 ${!selectedPage ? 'bg-indigo-50 text-indigo-700 font-medium' : 'text-gray-700'}`}
              >
                📋 index.md
              </button>
            )}
            {tree?.sources && <PageList pages={tree.sources} cat="sources" />}
            {tree?.entities && <PageList pages={tree.entities} cat="entities" />}
            {tree?.topics && <PageList pages={tree.topics} cat="topics" />}
            {tree?.analyses && tree.analyses.length > 0 && <PageList pages={tree.analyses} cat="analyses" />}
          </div>
          {/* Content */}
          <div className="flex-1 border border-gray-200 rounded-xl p-4 overflow-y-auto">
            {pageLoading ? (
              <div className="flex justify-center p-8"><Spinner /></div>
            ) : pageContent ? (
              <pre className="text-sm text-gray-700 whitespace-pre-wrap font-sans leading-relaxed">{pageContent}</pre>
            ) : (
              <div className="text-center py-8 text-gray-400 text-sm">←  选择左侧页面查看内容</div>
            )}
          </div>
        </div>
      )}
    </div>
  )
}

// ─── Tab 4: Search Test ────────────────────────────────────────

interface QAState {
  answer: string
  chunks: any[]
  status: string
  streaming: boolean
  showSources: boolean
}

function emptyQA(): QAState {
  return { answer: '', chunks: [], status: '', streaming: false, showSources: false }
}

function QAPanel({
  state, onToggleSources, mode,
}: {
  state: QAState
  onToggleSources: () => void
  mode: string
}) {
  const answerRef = useRef<HTMLDivElement>(null)
  useEffect(() => {
    if (state.streaming) answerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' })
  }, [state.answer, state.streaming])

  const modeLabel: Record<string, string> = {
    bm25: 'BM25', lambda: 'LambdaRAG', vector: '向量', wiki: 'Wiki', fusion: 'RAG Fusion', pageindex: 'PageIndex',
  }

  return (
    <div className="space-y-3">
      {/* Status line */}
      {state.status && !state.answer && (
        <p className="text-xs text-gray-400 flex items-center gap-1">
          {state.streaming && <RefreshCw size={10} className="animate-spin"/>}
          {state.status}
        </p>
      )}

      {/* Answer box */}
      {(state.answer || state.streaming) && (
        <div className="border border-indigo-200 bg-indigo-50 rounded-xl p-4">
          <div className="flex items-center gap-2 mb-2">
            <Zap size={13} className="text-indigo-500 shrink-0"/>
            <span className="text-xs font-semibold text-indigo-700">{modeLabel[mode] ?? mode} 回答</span>
            {state.streaming && <RefreshCw size={11} className="text-indigo-400 animate-spin"/>}
          </div>
          <div className="text-sm text-gray-800 leading-relaxed whitespace-pre-wrap" ref={answerRef}>
            {state.answer}{state.streaming && <span className="animate-pulse">▌</span>}
          </div>
        </div>
      )}

      {/* Source chunks — collapsible */}
      {state.chunks.length > 0 && (
        <div className="border border-gray-200 rounded-xl overflow-hidden">
          <button
            onClick={onToggleSources}
            className="w-full flex items-center justify-between px-3 py-2 bg-gray-50 hover:bg-gray-100 text-xs font-medium text-gray-600"
          >
            <span className="flex items-center gap-1.5">
              <FileText size={12}/> 参考来源（{state.chunks.length} 个 chunk）
            </span>
            {state.showSources ? <ChevronDown size={12}/> : <ChevronRight size={12}/>}
          </button>
          {state.showSources && (
            <div className="divide-y divide-gray-100 max-h-80 overflow-y-auto">
              {state.chunks.map((c: any, i: number) => (
                <div key={i} className="px-3 py-2.5">
                  <div className="flex items-center gap-2 mb-1">
                    <span className="text-xs font-semibold text-indigo-600 truncate">[来源{i+1}] {c.source}</span>
                    {c.score !== undefined && (
                      <span className="text-xs bg-gray-100 text-gray-500 px-1.5 rounded ml-auto shrink-0">
                        {typeof c.score === 'number' ? c.score.toFixed(3) : c.score}
                      </span>
                    )}
                  </div>
                  <p className="text-xs text-gray-600 leading-relaxed line-clamp-3">{c.text}</p>
                </div>
              ))}
            </div>
          )}
        </div>
      )}

      {/* Empty state */}
      {!state.answer && !state.streaming && state.chunks.length === 0 && !state.status && (
        <div className="text-center py-10 text-gray-400 text-sm">输入问题后点击搜索</div>
      )}
    </div>
  )
}

// ── Provider + Model Selector ──────────────────────────────────

interface LLMSelector {
  providerId: string
  model: string
}

function ProviderModelSelect({
  value,
  onChange,
  providers,
  compact,
}: {
  value: LLMSelector
  onChange: (v: LLMSelector) => void
  providers: Provider[]
  compact?: boolean
}) {
  const pdef = providers.find(p => p.id === value.providerId)
  const models = pdef?.models ?? []

  return (
    <div className={`flex items-center gap-1.5 ${compact ? '' : 'flex-wrap'}`}>
      <label className="text-xs text-gray-500 whitespace-nowrap">LLM:</label>
      <select
        value={value.providerId}
        onChange={e => {
          const np = providers.find(p => p.id === e.target.value)
          onChange({ providerId: e.target.value, model: np?.models[0] ?? '' })
        }}
        className="text-xs border border-gray-300 rounded px-2 py-1 bg-white max-w-[160px]"
      >
        {providers.filter(p => p.configured).map(p => (
          <option key={p.id} value={p.id}>{p.name}</option>
        ))}
        {providers.filter(p => p.configured).length === 0 && (
          <option value="">— 未配置提供商 —</option>
        )}
      </select>
      {models.length > 0 && (
        <select
          value={value.model}
          onChange={e => onChange({ ...value, model: e.target.value })}
          className="text-xs border border-gray-300 rounded px-2 py-1 bg-white max-w-[180px]"
        >
          {models.map(m => <option key={m} value={m}>{m}</option>)}
        </select>
      )}
    </div>
  )
}

function SearchTest({ kbId }: { kbId: string }) {
  const [query, setQuery] = useState('')
  const [leftMode, setLeftMode] = useState('bm25')
  const [rightMode, setRightMode] = useState('bm25')
  const [topK, setTopK] = useState(5)
  const [abMode, setAbMode] = useState(false)
  const [left, setLeft] = useState<QAState>(emptyQA())
  const [right, setRight] = useState<QAState>(emptyQA())
  // Use AbortController instead of EventSource for proper auth support
  const leftAbortRef = useRef<AbortController | null>(null)
  const rightAbortRef = useRef<AbortController | null>(null)

  // ── Provider / Model state ──
  const [providers, setProviders] = useState<Provider[]>([])
  const [leftLLM, setLeftLLM] = useState<LLMSelector>({ providerId: 'ollama', model: '' })
  const [rightLLM, setRightLLM] = useState<LLMSelector>({ providerId: 'ollama', model: '' })

  const apiKey = useAppStore(s => s.apiKey)

  useEffect(() => {
    providersApi.list().then(res => {
      const configured = res.providers.filter(p => p.configured && p.id !== 'claude-code')
      setProviders(configured)
      if (configured.length > 0) {
        const def = configured.find(p => p.id === 'ollama') ?? configured[0]
        setLeftLLM({ providerId: def.id, model: def.models[0] ?? '' })
        setRightLLM({ providerId: def.id, model: def.models[0] ?? '' })
      }
    }).catch(() => {})
  }, [])

  const modes = [
    { value: 'bm25', label: 'BM25 关键词' },
    { value: 'lambda', label: 'LambdaRAG 四路融合' },
    { value: 'vector', label: '向量语义' },
    { value: 'wiki', label: 'Wiki 全文' },
    { value: 'fusion', label: 'RAG Fusion（多查询+RRF）' },
    { value: 'pageindex', label: 'PageIndex（页面级无向量）' },
  ]

  const startQA = (side: 'left' | 'right', mode: string) => {
    if (!query.trim()) return
    const abortRef = side === 'left' ? leftAbortRef : rightAbortRef
    const setState = side === 'left' ? setLeft : setRight
    const llm = side === 'left' ? leftLLM : rightLLM

    // Cancel previous request
    abortRef.current?.abort()
    const ctrl = new AbortController()
    abortRef.current = ctrl

    setState(emptyQA())
    setState(s => ({ ...s, streaming: true, status: '正在检索...' }))

    const params = new URLSearchParams({
      q: query,
      mode,
      top_k: String(topK),
      provider_id: llm.providerId,
      model: llm.model,
    })
    const url = `/api/v1/knowledge/${kbId}/search/stream?${params}`

    // Use fetch instead of EventSource to support Authorization header
    ;(async () => {
      try {
        const res = await fetch(url, {
          headers: { Authorization: apiKey ? `Bearer ${apiKey}` : '' },
          signal: ctrl.signal,
        })
        if (!res.ok) {
          setState(s => ({ ...s, streaming: false, status: `错误 ${res.status}` }))
          return
        }
        const reader = res.body!.getReader()
        const decoder = new TextDecoder()
        let buf = ''
        while (true) {
          const { done, value } = await reader.read()
          if (done) break
          buf += decoder.decode(value, { stream: true })
          // Process complete SSE lines
          const lines = buf.split('\n')
          buf = lines.pop() ?? ''
          for (const line of lines) {
            if (!line.startsWith('data: ')) continue
            const payload = line.slice(6).trim()
            if (!payload) continue
            try {
              const d = JSON.parse(payload)
              if (d.type === 'status') {
                setState(s => ({ ...s, status: d.text }))
              } else if (d.type === 'chunks') {
                setState(s => ({ ...s, chunks: d.chunks ?? [], status: '' }))
              } else if (d.type === 'chunk') {
                setState(s => ({ ...s, answer: s.answer + d.text, status: '' }))
              } else if (d.type === 'done') {
                setState(s => ({ ...s, streaming: false, status: '' }))
              }
            } catch { /* ignore malformed */ }
          }
        }
        setState(s => ({ ...s, streaming: false }))
      } catch (err: unknown) {
        if ((err as { name?: string })?.name === 'AbortError') return
        setState(s => ({ ...s, streaming: false, status: '连接错误' }))
      }
    })()
  }

  const stopQA = (side: 'left' | 'right') => {
    const abortRef = side === 'left' ? leftAbortRef : rightAbortRef
    const setState = side === 'left' ? setLeft : setRight
    abortRef.current?.abort()
    abortRef.current = null
    setState(s => ({ ...s, streaming: false }))
  }

  const doSearch = () => {
    startQA('left', leftMode)
    if (abMode) startQA('right', rightMode)
  }

  return (
    <div className="space-y-4">
      {/* Search bar */}
      <div className="border border-gray-200 rounded-xl p-4">
        <div className="flex gap-2 mb-3">
          <input
            value={query}
            onChange={e => setQuery(e.target.value)}
            onKeyDown={e => e.key === 'Enter' && !e.shiftKey && doSearch()}
            placeholder="输入问题，自动检索并生成回答..."
            className="flex-1 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 outline-none"
          />
          <Button
            onClick={left.streaming ? () => stopQA('left') : doSearch}
            icon={left.streaming ? <Pause size={14}/> : <Search size={14}/>}
          >
            {left.streaming ? '停止' : '搜索'}
          </Button>
        </div>
        <div className="flex items-center gap-4 flex-wrap">
          {/* Retrieval mode (only shown in non-AB mode) */}
          {!abMode && (
            <div className="flex items-center gap-2">
              <label className="text-xs text-gray-600">检索:</label>
              <select value={leftMode} onChange={e => setLeftMode(e.target.value)}
                className="text-xs border border-gray-300 rounded px-2 py-1">
                {modes.map(m => <option key={m.value} value={m.value}>{m.label}</option>)}
              </select>
            </div>
          )}
          <div className="flex items-center gap-2">
            <label className="text-xs text-gray-600">Top-K:</label>
            <input type="number" min={1} max={20} value={topK}
              onChange={e => setTopK(Number(e.target.value))}
              className="w-14 text-xs border border-gray-300 rounded px-2 py-1" />
          </div>
          {/* LLM selector (non-AB: single; AB: shown per-side below) */}
          {!abMode && (
            <ProviderModelSelect
              value={leftLLM}
              onChange={setLeftLLM}
              providers={providers}
            />
          )}
          <label className="flex items-center gap-1.5 text-xs text-gray-600 cursor-pointer ml-auto">
            <input type="checkbox" checked={abMode} onChange={e => setAbMode(e.target.checked)} className="rounded"/>
            A/B 对比
          </label>
        </div>
      </div>

      {/* Results */}
      {abMode ? (
        <div className="grid grid-cols-2 gap-4">
          {/* Left column */}
          <div>
            <div className="flex items-center justify-between gap-2 mb-2 flex-wrap">
              <select value={leftMode} onChange={e => setLeftMode(e.target.value)}
                className="text-xs border border-gray-300 rounded px-2 py-1">
                {modes.map(m => <option key={m.value} value={m.value}>{m.label}</option>)}
              </select>
              <ProviderModelSelect
                value={leftLLM}
                onChange={setLeftLLM}
                providers={providers}
                compact
              />
              <Button size="sm" onClick={left.streaming ? () => stopQA('left') : () => startQA('left', leftMode)}
                icon={left.streaming ? <Pause size={11}/> : <Search size={11}/>}>
                {left.streaming ? '停止' : '搜索'}
              </Button>
            </div>
            <QAPanel state={left} mode={leftMode}
              onToggleSources={() => setLeft(s => ({ ...s, showSources: !s.showSources }))} />
          </div>
          {/* Right column */}
          <div>
            <div className="flex items-center justify-between gap-2 mb-2 flex-wrap">
              <select value={rightMode} onChange={e => setRightMode(e.target.value)}
                className="text-xs border border-gray-300 rounded px-2 py-1">
                {modes.map(m => <option key={m.value} value={m.value}>{m.label}</option>)}
              </select>
              <ProviderModelSelect
                value={rightLLM}
                onChange={setRightLLM}
                providers={providers}
                compact
              />
              <Button size="sm" onClick={right.streaming ? () => stopQA('right') : () => startQA('right', rightMode)}
                icon={right.streaming ? <Pause size={11}/> : <Search size={11}/>}>
                {right.streaming ? '停止' : '搜索'}
              </Button>
            </div>
            <QAPanel state={right} mode={rightMode}
              onToggleSources={() => setRight(s => ({ ...s, showSources: !s.showSources }))} />
          </div>
        </div>
      ) : (
        <QAPanel state={left} mode={leftMode}
          onToggleSources={() => setLeft(s => ({ ...s, showSources: !s.showSources }))} />
      )}
    </div>
  )
}

// ─── Main Detail Page ──────────────────────────────────────────

const TABS = [
  { id: 'files', label: '文件管理', icon: <FolderOpen size={14}/> },
  { id: 'index', label: '索引构建', icon: <Database size={14}/> },
  { id: 'wiki', label: 'Wiki目录', icon: <BookOpen size={14}/> },
  { id: 'search', label: '搜索测试', icon: <Search size={14}/> },
]

export default function KnowledgeDetail() {
  const { kbId } = useParams<{ kbId: string }>()
  const navigate = useNavigate()
  const [tab, setTab] = useState('files')

  const { data: kb, isLoading } = useQuery({
    queryKey: ['knowledge', kbId],
    queryFn: async () => {
      const r = await knowledgeApi.list()
      return r.items.find(k => k.id === kbId) ?? null
    },
    enabled: !!kbId,
  })

  if (isLoading) return <div className="flex items-center justify-center h-40"><Spinner /></div>
  if (!kb) return <div className="p-6 text-red-600">知识体不存在</div>

  return (
    <div className="p-6">
      {/* Header */}
      <div className="flex items-center gap-3 mb-6">
        <button onClick={() => navigate('/knowledge')}
          className="text-gray-400 hover:text-gray-700 transition-colors p-1">
          <ArrowLeft size={18}/>
        </button>
        <div>
          <h1 className="text-lg font-bold text-gray-900">{kb.name}</h1>
          <p className="text-xs text-gray-400 font-mono mt-0.5">{kb.root_dir}</p>
        </div>
      </div>

      {/* Tabs */}
      <div className="flex border-b border-gray-200 mb-6">
        {TABS.map(t => (
          <button
            key={t.id}
            onClick={() => setTab(t.id)}
            className={`flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors ${
              tab === t.id
                ? 'border-indigo-500 text-indigo-600'
                : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
            }`}
          >
            {t.icon}
            {t.label}
          </button>
        ))}
      </div>

      {/* Tab content */}
      {tab === 'files' && <FileManager kbId={kbId!}/>}
      {tab === 'index' && <IndexBuilder kbId={kbId!}/>}
      {tab === 'wiki' && <WikiBrowser kbId={kbId!}/>}
      {tab === 'search' && <SearchTest kbId={kbId!}/>}
    </div>
  )
}
