import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { motion, AnimatePresence } from 'framer-motion'
import { Trash2, RefreshCw, Star, X, Search, LayoutGrid, List, Settings2, Plus, Check, Filter, Eye, EyeOff, Minus, ChevronsUp } from 'lucide-react'
import { api, type KlineRow } from '@/lib/api'
import { QK } from '@/lib/queryKeys'
import { storage } from '@/lib/storage'
import { fmtPrice, fmtPct, fmtBigNum, priceColorClass } from '@/lib/format'
import { PageHeader } from '@/components/PageHeader'
import { EmptyState } from '@/components/EmptyState'
import { StockPreviewDialog } from '@/components/StockPreviewDialog'
import { ColumnCustomizer } from '@/components/ColumnCustomizer'
import { StockDataTable } from '@/components/stock-table/StockDataTable'
import { useTableSort } from '@/components/stock-table/useTableSort'
import { MiniCandlestick } from '@/components/stock-table/MiniCandlestick'
import { boardTag, renderBuiltinDataCell } from '@/components/stock-table/primitives'
import { getSignals, signalCls, getSortValue, UNSORTABLE_KEYS } from '@/lib/stock-table'
import { resolveCandleConfig } from '@/lib/list-columns'
import { useQuoteStatus } from '@/lib/useSharedQueries'
import {
  type ColumnConfig,
  BUILTIN_COLUMNS,
  COLUMN_GROUPS,
  loadColumnConfig,
  saveColumnConfig,
  buildExtColumnsParam,
} from '@/lib/watchlist-columns'

// ===== 板块标识（筛选/卡片用） =====
// 注: boardTag（创/科/北 标签）已移至共享 @/components/stock-table/primitives

const BOARDS = ['沪主板', '深主板', '创业板', '科创板', '北交所'] as const
type BoardType = typeof BOARDS[number]

function getBoardType(symbol: string): BoardType | null {
  if (/^(300|301)/.test(symbol)) return '创业板'
  if (/^688/.test(symbol))       return '科创板'
  if (/\.BJ$/.test(symbol))      return '北交所'
  if (/^60[0135]/.test(symbol))  return '沪主板'
  if (/^00[012]/.test(symbol))   return '深主板'
  return null
}

// ===== 换手率分档色（卡片/表格用） =====

function turnoverColor(rate: number | null | undefined): string {
  if (rate == null || Number.isNaN(rate)) return 'text-[#888]'
  if (rate < 5)   return 'text-[#888]'
  if (rate < 10)  return 'text-[#d4a800]'
  if (rate < 20)  return 'text-[#f97316]'
  if (rate < 35)  return 'text-[#d94a3d]'
  return 'text-[#b84a8a]'
}

// ===== 动态列渲染 =====
// 表头/单元格渲染已共享化：纯数据列由 @/components/stock-table/primitives 的
// renderBuiltinDataCell 处理；symbol/signals/candle/ext 等需上下文的列由下方
// 表格 renderCell 回调处理。表格骨架使用 StockDataTable。

/** 渲染扩展数据列的值（含分隔/标签/展开配置） */
function renderExtValue(
  val: any,
  col: ColumnConfig,
  expanded: boolean,
  onToggle: () => void,
  inline?: boolean,
): React.ReactNode {
  if (val == null || Number.isNaN(val)) return <span className="text-muted">—</span>
  if (typeof val === 'number') {
    // int 类型不显示小数
    const displayVal = Number.isInteger(val) ? fmtPrice(val, 0) : fmtPrice(val)
    return <span className="tabular-nums">{displayVal}</span>
  }
  if (typeof val === 'boolean') {
    return <span className={val ? 'text-bull' : 'text-muted'}>{val ? '是' : '否'}</span>
  }

  // String — 按 extDisplay 配置渲染
  const cfg = col.extDisplay
  const str = String(val)

  // 纯文本模式
  if (cfg?.displayMode === 'text') {
    return <span className="text-foreground">{str}</span>
  }

  // 标签模式（默认）
  const separator = cfg?.separator?.trim() || null
  const tags = separator
    ? str.split(separator).map(s => s.trim()).filter(Boolean)
    : str.split(/[、,，;；\-]/).map(s => s.trim()).filter(Boolean)

  if (tags.length === 0) return <span className="text-muted">—</span>

  const maxTags = cfg?.maxTags ?? 0
  const showAll = maxTags <= 0 || expanded || tags.length <= maxTags
  const sliced = showAll ? tags : tags.slice(0, maxTags)
  const hiddenIndices = maxTags > 0 ? cfg?.hiddenIndices : undefined
  const visibleTags = hiddenIndices?.length
    ? sliced.filter((_, i) => !hiddenIndices.includes(i))
    : sliced
  const hiddenCount = tags.length - visibleTags.length

  // 竖向排列：仅在表格视图、收起状态、设定了显示上限时生效
  const isVertical = !inline && cfg?.tagLayout === 'vertical' && !expanded

  const tagEls = (
    <>
      {visibleTags.map((tag, i) => (
        <span key={i} className="inline-block px-1.5 py-px rounded text-[10px] font-medium leading-tight text-yellow-500 bg-yellow-500/10">
          {tag}
        </span>
      ))}
      {!showAll && hiddenCount > 0 && (
        <button
          onClick={onToggle}
          className="inline-block px-1.5 py-px rounded text-[10px] font-medium leading-tight text-accent bg-accent/10 hover:bg-accent/20 transition-colors"
        >
          +{hiddenCount}
        </button>
      )}
      {showAll && maxTags > 0 && tags.length > maxTags && (
        <button
          onClick={onToggle}
          className="inline-block px-1.5 py-px rounded text-[10px] font-medium leading-tight text-muted hover:text-foreground transition-colors"
        >
          收起
        </button>
      )}
    </>
  )

  if (inline) {
    // 卡片视图：返回 inline 片段
    return tagEls
  }
  // 表格视图：用 <div> 包裹
  return <div className={isVertical ? 'flex flex-col items-start gap-0.5' : 'flex flex-wrap gap-0.5'}>{tagEls}</div>
}

/** 渲染扩展数据列的 <td> */
function renderExtCell(
  r: any,
  col: ColumnConfig,
  expandedCells: Set<string>,
  onToggleExpand: (key: string) => void,
): React.ReactNode {
  if (col.source.type !== 'ext') return null
  const { configId, fieldName } = col.source
  const val = r[`${configId}__${fieldName}`]
  const cellKey = `${r.symbol}::${col.id}`
  const expanded = expandedCells.has(cellKey)

  const style: React.CSSProperties = {}
  if (col.extDisplay?.maxWidth) {
    style.maxWidth = col.extDisplay.maxWidth
  }

  // 根据值类型决定 td class
  const tdClass = val == null || Number.isNaN(val)
    ? 'px-2 py-1.5 text-right num tabular-nums text-muted'
    : typeof val === 'number'
      ? 'px-2 py-1.5 text-right num tabular-nums'
      : typeof val === 'boolean'
        ? 'px-2 py-1.5 text-right'
        : 'px-2 py-1.5'

  return (
    <td className={tdClass} style={style}>
      {renderExtValue(val, col, expanded, () => onToggleExpand(cellKey))}
    </td>
  )
}

// ===== 搜索框组件（紧凑内联式）=====

function StockSearchBox({
  onPreview,
  existingSymbols,
  onAdd,
}: {
  onPreview: (symbol: string, name: string) => void
  existingSymbols: string[]
  onAdd: (symbol: string) => void
}) {
  const [query, setQuery] = useState('')
  const [open, setOpen] = useState(false)
  const containerRef = useRef<HTMLDivElement>(null)
  const inputRef = useRef<HTMLInputElement>(null)
  const [activeIdx, setActiveIdx] = useState(-1)

  const search = useQuery({
    queryKey: QK.instrumentSearch(query),
    queryFn: () => api.instrumentSearch(query),
    enabled: query.trim().length > 0,
    staleTime: 30_000,
  })

  const results = search.data?.results ?? []

  useEffect(() => {
    function handleClick(e: MouseEvent) {
      if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
        setOpen(false)
      }
    }
    document.addEventListener('mousedown', handleClick)
    return () => document.removeEventListener('mousedown', handleClick)
  }, [])

  function handleKeyDown(e: React.KeyboardEvent) {
    if (e.key === 'Escape') { setOpen(false); return }
    if (!open || results.length === 0) return
    if (e.key === 'ArrowDown') {
      e.preventDefault()
      setActiveIdx(i => Math.min(i + 1, results.length - 1))
    } else if (e.key === 'ArrowUp') {
      e.preventDefault()
      setActiveIdx(i => Math.max(i - 1, -1))
    } else if (e.key === 'Enter') {
      e.preventDefault()
      if (activeIdx >= 0) handleSelect(results[activeIdx])
      else if (results.length > 0) handleSelect(results[0])
    }
  }

  function handleSelect(r: { symbol: string; name: string }) {
    onPreview(r.symbol, r.name)
    setQuery('')
    setOpen(false)
    setActiveIdx(-1)
  }

  return (
    <div ref={containerRef} className="relative">
      <div className="relative flex items-center">
        <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted pointer-events-none" />
        <input
          ref={inputRef}
          type="text"
          placeholder="搜索…"
          value={query}
          onChange={(e) => { setQuery(e.target.value); setOpen(true); setActiveIdx(-1) }}
          onFocus={() => { if (query.trim()) setOpen(true) }}
          onKeyDown={handleKeyDown}
          className="w-44 h-8 pl-8 pr-2.5 rounded-btn bg-elevated border border-border text-xs text-foreground placeholder:text-muted focus:outline-none focus:border-accent/50 focus:w-56 transition-all duration-200"
        />
      </div>

      <AnimatePresence>
        {open && results.length > 0 && (
          <motion.div
            initial={{ opacity: 0, y: -4 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -4 }}
            transition={{ duration: 0.12, ease: [0.16, 1, 0.3, 1] }}
            className="absolute right-0 top-full mt-1 z-50 w-64 max-h-[320px] overflow-y-auto rounded-card border border-border bg-base shadow-xl"
          >
            {results.map((r, i) => {
              const inWatchlist = existingSymbols.includes(r.symbol)
              return (
                <div
                  key={r.symbol}
                  className={`flex items-center gap-2.5 px-3 py-2 text-xs transition-colors duration-100 ${
                    i === activeIdx ? 'bg-accent/10 text-accent' : 'hover:bg-elevated text-foreground'
                  }`}
                >
                  <button
                    type="button"
                    onClick={() => handleSelect(r)}
                    className="flex items-center gap-2.5 flex-1 min-w-0 text-left"
                  >
                    <span className="font-mono shrink-0 w-[80px]">{r.symbol}</span>
                    <span className="truncate text-secondary flex-1">{r.name}</span>
                  </button>
                  <button
                    type="button"
                    onClick={e => { e.stopPropagation(); onAdd(r.symbol) }}
                    disabled={inWatchlist}
                    className={`shrink-0 p-1 rounded transition-colors ${
                      inWatchlist
                        ? 'text-accent bg-accent/10 cursor-default'
                        : 'text-muted hover:text-accent hover:bg-accent/10'
                    }`}
                    title={inWatchlist ? '已加自选' : '加入自选'}
                  >
                    {inWatchlist ? <Check className="h-3.5 w-3.5" /> : <Plus className="h-3.5 w-3.5" />}
                  </button>
                </div>
              )
            })}
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  )
}

// ===== 实时监控圆点 =====
// 自选页 symbol 列代码后的小圆点, 标识该标的正在被实时行情监控 (Free/低档按自选监控模式)。
// 视觉: 内圈实心点 + 外圈 animate-ping 扩散晕, 语义=「在线/活动」。
// 配色用 accent (电光蓝) 而非绿/红: 项目设计规范规定红绿仅用于价格/K线,
// UI 状态用 accent, 避免与 A 股涨跌色混淆。
// 全市场模式 (Starter+) 不显示 —— 全部都在监控, 标记无信息量。
function RealtimeDot({ title = '实时监控中' }: { title?: string }) {
  return (
    <span
      title={title}
      className="relative inline-flex h-2 w-2 shrink-0"
      aria-label={title}
    >
      {/* 外圈: 扩散晕 (ping 动画) */}
      <span className="absolute inline-flex h-full w-full rounded-full bg-accent/60 animate-ping motion-reduce:hidden" />
      {/* 内圈: 实心点 + 微辉光 */}
      <span className="relative inline-flex rounded-full h-2 w-2 bg-accent shadow-[0_0_5px_rgba(61,214,140,0.6)]" />
    </span>
  )
}

// ===== 卡片组件 =====

function StockCard({
  r,
  candleRows,
  showCandle,
  onPreview,
  onConfirmRemove,
  onCancelRemove,
  onRequestRemove,
  confirmRemove,
  extCols,
  expandedCells,
  onToggleExpand,
  isMonitored,
}: {
  r: any
  candleRows: KlineRow[]
  showCandle: boolean
  onPreview: (symbol: string, name: string) => void
  onConfirmRemove: (symbol: string) => void
  onCancelRemove: () => void
  onRequestRemove: (symbol: string) => void
  confirmRemove: string | null
  extCols: ColumnConfig[]
  expandedCells: Set<string>
  onToggleExpand: (key: string) => void
  isMonitored?: boolean
}) {
  const board = boardTag(r.symbol)
  const price = r.rt_price ?? r.close
  const pct = r.rt_pct ?? r.change_pct
  const name = r.rt_name ?? r.name
  const signals = getSignals(r)
  const isUp = (pct ?? 0) > 0
  const isDown = (pct ?? 0) < 0

  // 动态背景渐变: 涨=红底, 跌=绿底, 平=无色
  const bgGlow = isUp
    ? 'bg-gradient-to-br from-bull/[0.06] via-transparent to-bull/[0.02]'
    : isDown
      ? 'bg-gradient-to-br from-bear/[0.06] via-transparent to-bear/[0.02]'
      : ''
  // 左侧指示条颜色
  const barColor = isUp ? 'bg-bull/70' : isDown ? 'bg-bear/70' : 'bg-muted/30'
  // 涨跌幅标签背景
  const pctBg = isUp ? 'bg-bull/12 text-bull' : isDown ? 'bg-bear/12 text-bear' : 'bg-elevated text-secondary'

  return (
    <div
      className={`relative rounded-lg border border-border bg-surface hover:border-border/80 transition-all duration-200 group cursor-pointer overflow-hidden ${bgGlow}`}
      onClick={() => onPreview(r.symbol, name ?? '')}
    >
      {/* 左侧彩色指示条 */}
      <div className={`absolute left-0 top-0 bottom-0 w-[3px] rounded-l-lg ${barColor}`} />

      {/* 删除按钮 / 确认区 */}
      <div className="absolute top-1.5 right-1.5 z-10">
        {confirmRemove === r.symbol ? (
          <div className="flex items-center gap-1" onClick={e => e.stopPropagation()}>
            <button
              onClick={() => onConfirmRemove(r.symbol)}
              className="px-1.5 py-0.5 rounded text-[10px] text-danger bg-danger/10 hover:bg-danger/20 transition-colors"
            >
              确认
            </button>
            <button onClick={() => onCancelRemove()} className="p-0.5 text-muted hover:text-foreground transition-colors">
              <X className="h-3 w-3" />
            </button>
          </div>
        ) : (
          <button
            onClick={e => { e.stopPropagation(); onRequestRemove(r.symbol) }}
            className="opacity-0 group-hover:opacity-100 text-muted hover:text-danger transition-all duration-150 p-0.5 rounded hover:bg-elevated"
            aria-label="移除"
          >
            <Trash2 className="h-3.5 w-3.5" />
          </button>
        )}
      </div>

      {/* 卡片内容 */}
      <div className="pl-4 pr-2.5 pt-2.5 pb-0">
        {/* 第一行: 代码 + 名称 + 板块标识 */}
        <div className="flex items-center gap-1.5 min-w-0 mb-2">
          <span className="shrink-0 font-mono text-foreground text-xs tracking-wide">
            {r.symbol}
          </span>
          {name && (
            <span className="text-xs text-secondary truncate">{name}</span>
          )}
          {board && (
            <span className={`shrink-0 inline-flex items-center justify-center px-1 h-[16px] rounded text-[9px] font-bold leading-none ${board.color}`}>
              {board.label}
            </span>
          )}
          {r.consecutive_limit_ups > 0 && (
            <span className="shrink-0 inline-flex items-center justify-center px-1 h-[16px] rounded bg-danger/15 text-danger text-[9px] font-bold tabular-nums">
              {r.consecutive_limit_ups === 1 ? '首板' : `${r.consecutive_limit_ups}连`}
            </span>
          )}
          {isMonitored && <span className="ml-auto"><RealtimeDot /></span>}
        </div>

        {/* 第二行: 大价格 + 涨跌幅胶囊 */}
        <div className="flex items-end justify-between gap-2 mb-2">
          <span className={`text-xl tabular-nums tracking-tighter leading-none ${priceColorClass(pct)}`}>
            {fmtPrice(price)}
          </span>
          {pct != null && (
            <span className={`shrink-0 inline-flex items-center px-1.5 py-[2px] rounded text-[11px] tabular-nums ${pctBg}`}>
              {isUp ? '+' : ''}{pct.toFixed(2)}%
            </span>
          )}
        </div>

        {/* 第三行: 指标 */}
        <div className="flex flex-wrap items-center gap-x-2.5 gap-y-0.5 text-[10px] text-muted leading-relaxed">
          <span title="换手率">换手<span className={`font-mono ml-0.5 ${turnoverColor(r.turnover_rate)}`}>{r.turnover_rate != null ? `${r.turnover_rate.toFixed(2)}%` : '—'}</span></span>
          <span title="量比">量比<span className="font-mono ml-0.5">{fmtPrice(r.vol_ratio_5d)}</span></span>
          <span title="RSI14">RSI<span className="font-mono ml-0.5">{r.rsi_14 != null ? r.rsi_14.toFixed(1) : '—'}</span></span>
          {/* 扩展数据列展示在卡片中 */}
          {extCols.map(col => {
            if (col.source.type !== 'ext') return null
            const { configId, fieldName } = col.source
            const val = r[`${configId}__${fieldName}`]
            if (val == null) return null

            const cellKey = `${r.symbol}::${col.id}`
            const expanded = expandedCells.has(cellKey)

            return (
              <span key={col.id} title={col.label}>
                <span className="text-secondary">{fieldName}</span>
                <span className="font-mono ml-0.5">
                  {renderExtValue(val, col, expanded, () => onToggleExpand(cellKey), true)}
                </span>
              </span>
            )
          })}
        </div>
      </div>

      {/* 信号标签区 */}
      {signals.length > 0 && (
        <div className="pl-4 pr-2.5 pt-1.5 pb-2 flex flex-wrap gap-1">
          {signals.slice(0, 3).map(s => (
            <span key={s.label} className={`inline-block px-1.5 py-[1px] rounded text-[9px] font-medium leading-tight ${signalCls(s.type)}`}>
              {s.label}
            </span>
          ))}
          {signals.length > 3 && (
            <span className="inline-block px-1 py-[1px] rounded text-[9px] text-muted bg-elevated leading-tight">
              +{signals.length - 3}
            </span>
          )}
        </div>
      )}

      {/* 迷你蜡烛图 */}
      {showCandle && candleRows.length > 0 && (
        <div className="border-t border-border/40 px-3 py-1.5">
          <MiniCandlestick rows={candleRows} height={32} />
        </div>
      )}
    </div>
  )
}

// ===== 主页面 =====

export function Watchlist() {
  const qc = useQueryClient()
  const [viewMode, setViewMode] = useState<'table' | 'card'>(() => {
    return (storage.watchlistView.get('table') as 'table' | 'card')
  })
  const [dailyKChartVisible, setDailyKChartVisible] = useState(() => {
    return storage.watchlistCandle.get(true)
  })

  // 列配置 — 从后端/localStorage 异步加载
  const [columns, setColumns] = useState<ColumnConfig[]>([...BUILTIN_COLUMNS])
  const [customizerOpen, setCustomizerOpen] = useState(false)
  const columnsLoaded = useRef(false)

  useEffect(() => {
    if (columnsLoaded.current) return
    columnsLoaded.current = true
    loadColumnConfig().then(setColumns)
  }, [])

  const handleColumnsChange = useCallback((next: ColumnConfig[]) => {
    setColumns(next)
    saveColumnConfig(next)
  }, [])

  const candleColumn = useMemo(() =>
    columns.find(c => c.source.type === 'builtin' && c.source.key === 'candle' && c.visible),
    [columns],
  )
  const candleColumnEnabled = !!candleColumn
  // 日k列渲染配置（来自列定制，已钳制边界）
  const candleResolved = useMemo(() => resolveCandleConfig(candleColumn?.candleConfig), [candleColumn])
  const candleDays = candleResolved.days
  const candleSize = dailyKChartVisible
    ? { width: candleResolved.enabledWidth, height: candleResolved.enabledHeight }
    : { width: candleResolved.disabledWidth, height: candleResolved.disabledHeight }

  const dailyKVisible = candleColumnEnabled && dailyKChartVisible

  // 计算可见列（列是否出现只由自定义列配置决定）
  const visibleColumns = useMemo(() => {
    return columns.filter(c => c.visible)
  }, [columns])

  // 计算 ext 列参数
  const extColumnsParam = useMemo(() => buildExtColumnsParam(columns), [columns])

  const toggleView = useCallback(() => {
    setViewMode(v => {
      const next = v === 'table' ? 'card' : 'table'
      storage.watchlistView.set(next)
      return next
    })
  }, [])
  const toggleDailyKChart = useCallback(() => {
    setDailyKChartVisible(v => {
      const next = !v
      storage.watchlistCandle.set(next)
      return next
    })
  }, [])
  const [previewSymbol, setPreviewSymbol] = useState<string | null>(null)
  const [previewName, setPreviewName] = useState<string>('')
  const [expandedCells, setExpandedCells] = useState<Set<string>>(new Set())
  const closePreview = useCallback(() => {
    setPreviewSymbol(null)
    setPreviewName('')
  }, [])

  const handleToggleExpand = useCallback((cellKey: string) => {
    setExpandedCells(prev => {
      const next = new Set(prev)
      if (next.has(cellKey)) next.delete(cellKey)
      else next.add(cellKey)
      return next
    })
  }, [])

  const list = useQuery({
    queryKey: QK.watchlist,
    queryFn: api.watchlistList,
  })

  // enriched 数据 — 传入 ext_columns 参数
  const enriched = useQuery({
    queryKey: QK.watchlistEnriched(extColumnsParam),
    queryFn: () => api.watchlistEnriched(extColumnsParam || undefined),
    enabled: (list.data?.symbols.length ?? 0) > 0,
  })

  const symbols = enriched.data?.rows?.map((r: any) => r.symbol) ?? []
  const symbolsKey = symbols.join(',')

  // 批量日k数据 (天数由列配置决定)
  const klineBatch = useQuery({
    queryKey: QK.watchlistKlineBatch(`${symbolsKey}|${candleDays}`),
    queryFn: () => api.klineDailyBatch(symbols, candleDays),
    enabled: dailyKVisible && symbols.length > 0,
    staleTime: 5 * 60_000,  // 5 分钟内不重请求
  })

  const klineData = dailyKVisible ? (klineBatch.data?.data ?? {}) : {}

  const addMutation = useMutation({
    mutationFn: (sym: string) => api.watchlistAdd(sym),
    onSuccess: (data) => {
      qc.setQueryData(QK.watchlist, data)
      qc.invalidateQueries({ queryKey: QK.watchlist })
      qc.invalidateQueries({ queryKey: ['watchlist-enriched'] })
      qc.invalidateQueries({ queryKey: ['watchlist-kline-batch'] })
    },
  })

  const remove = useMutation({
    mutationFn: (sym: string) => api.watchlistRemove(sym),
    onSuccess: (_data, sym) => {
      // 1. 立即从 enriched 缓存中移除该股票，UI 即时更新
      qc.setQueryData(['watchlist-enriched', extColumnsParam], (old: any) => {
        if (!old?.rows) return old
        return { ...old, rows: old.rows.filter((r: any) => r.symbol !== sym) }
      })
      // 2. 清除 list 缓存，触发后台 refetch
      qc.invalidateQueries({ queryKey: QK.watchlist })
      qc.invalidateQueries({ queryKey: QK.watchlistEnriched() })
      qc.invalidateQueries({ queryKey: QK.watchlistKlineBatch('') })
    },
  })

  const moveToTop = useMutation({
    mutationFn: (sym: string) => api.watchlistMoveToTop(sym),
    onSuccess: (data) => {
      qc.setQueryData(QK.watchlist, data)
      qc.invalidateQueries({ queryKey: QK.watchlist })
      qc.invalidateQueries({ queryKey: ['watchlist-enriched'] })
      qc.invalidateQueries({ queryKey: ['watchlist-kline-batch'] })
      qc.invalidateQueries({ queryKey: QK.preferences })
      qc.invalidateQueries({ queryKey: QK.quoteStatus })
    },
  })

  const clearAll = useMutation({
    mutationFn: () => api.watchlistClear(),
    onSuccess: () => {
      setConfirmClear(false)
      // 立即清空 enriched 缓存
      qc.setQueryData(['watchlist-enriched', extColumnsParam], { rows: [], as_of: null, elapsed_ms: 0 })
      qc.invalidateQueries({ queryKey: QK.watchlist })
      qc.invalidateQueries({ queryKey: QK.watchlistEnriched() })
      qc.invalidateQueries({ queryKey: QK.watchlistKlineBatch('') })
    },
  })

  // 二次确认状态
  const [confirmClear, setConfirmClear] = useState(false)
  const [confirmRemove, setConfirmRemove] = useState<string | null>(null)

  const allSymbols = list.data?.symbols?.map(s => s.symbol) ?? []
  const rows = enriched.data?.rows ?? []

  // 实时监控圆点: 仅 Free/低档 "按自选股实时监控" 模式 (mode === 'watchlist') 下显示;
  // Starter+ 全市场模式 (mode === 'full_market') 全部标的都在监控, 标圆点无意义, 故不显示。
  // 后端 Free 档实际只监控自选页前 N 个 (N = watchlist_symbol_count), 顺序与 allSymbols 一致。
  const quoteStatus = useQuoteStatus()
  const realtimeRunning = quoteStatus.data?.running ?? false
  const realtimeMode = quoteStatus.data?.mode
  const watchlistMonitoredCount = quoteStatus.data?.watchlist_symbol_count ?? 0
  const showRealtimeDot = realtimeRunning && realtimeMode === 'watchlist'
  // 真正被监控的标的集合 (自选列表前 watchlistMonitoredCount 个)
  const monitoredSymbols = useMemo(
    () => showRealtimeDot ? new Set(allSymbols.slice(0, watchlistMonitoredCount)) : new Set<string>(),
    [showRealtimeDot, allSymbols, watchlistMonitoredCount],
  )

  // ===== 筛选 =====
  const [filterOpen, setFilterOpen] = useState(false)
  const [filters, setFilters] = useState<Record<string, { min?: string; max?: string; text?: string }>>({})

  // 板块筛选（持久化）
  const [boardFilter, setBoardFilter] = useState<Set<string>>(() => {
    const saved = storage.watchlistBoardFilter.get([])
    return saved.length > 0 ? new Set(saved) : new Set(BOARDS) // 默认全选
  })
  const persistBoardFilter = useCallback((next: Set<string>) => {
    setBoardFilter(next)
    storage.watchlistBoardFilter.set([...next])
  }, [])

  const toggleBoard = useCallback((board: string) => {
    setBoardFilter(prev => {
      const next = new Set(prev)
      if (next.has(board)) next.delete(board)
      else next.add(board)
      persistBoardFilter(next)
      return next
    })
  }, [persistBoardFilter])

  const updateFilter = useCallback((colId: string, patch: { min?: string; max?: string; text?: string }) => {
    setFilters(prev => {
      const next = { ...prev }
      const existing = next[colId] || {}
      const merged = { ...existing, ...patch }
      if (!merged.min && !merged.max && !merged.text) {
        delete next[colId]
      } else {
        next[colId] = merged
      }
      return next
    })
  }, [])

  const clearFilters = useCallback(() => setFilters({}), [])

  // 可筛选的内置列
  const filterableBuiltinCols = useMemo(
    () => columns.filter(c => c.source.type === 'builtin' && !UNSORTABLE_KEYS.has(c.source.key) && c.id !== 'builtin:symbol'),
    [columns],
  )

  // 按类别索引（复用列配置的分组定义）
  const colsByCategory = useMemo(() => {
    const map: Record<string, { id: string; label: string; col: typeof filterableBuiltinCols[number] }[]> = {}
    for (const cat of COLUMN_GROUPS) {
      map[cat.label] = []
      for (const key of cat.keys) {
        const col = filterableBuiltinCols.find(c => c.source.type === 'builtin' && c.source.key === key)
        if (col) map[cat.label].push({ id: col.id, label: col.label, col })
      }
    }
    return map
  }, [filterableBuiltinCols])

  // 筛选 + 排序
  const filteredRows = useMemo(() => {
    // 板块筛选（全选时跳过）
    let result = rows
    if (boardFilter.size > 0 && boardFilter.size < BOARDS.length) {
      result = result.filter(r => {
        const board = getBoardType(r.symbol)
        return board != null && boardFilter.has(board)
      })
    }
    // 数值/文本筛选
    const activeFilters = Object.entries(filters).filter(([, v]) => v.min || v.max || v.text)
    if (activeFilters.length > 0) {
      result = result.filter(r => {
        for (const [colId, f] of activeFilters) {
          const col = columns.find(c => c.id === colId)
          if (!col) continue
          const val = getSortValue(r, col)
          if (val == null) return false
          if (typeof val === 'number') {
            if (f.min && val < Number(f.min)) return false
            if (f.max && val > Number(f.max)) return false
          } else {
            if (f.text && !String(val).includes(f.text)) return false
          }
        }
        return true
      })
    }
    return result
  }, [rows, filters, columns, boardFilter])

  const activeFilterCount = Object.values(filters).filter(v => v.min || v.max || v.text).length

  // 排序（复用共享三态排序 hook）
  const { sort, toggle: handleSortToggle, sortRows } = useTableSort()

  const sortedRows = useMemo(
    () => sortRows(filteredRows, columns),
    [filteredRows, sortRows, columns],
  )

  // 可见的 ext 列（卡片视图使用）
  const visibleExtCols = useMemo(
    () => visibleColumns.filter(c => c.source.type === 'ext'),
    [visibleColumns]
  )

  // 被过滤掉的个股数 (筛选/板块过滤导致的隐藏)
  const hiddenCount = Math.max(0, allSymbols.length - sortedRows.length)

  return (
    <div className="flex flex-col h-full">
      <PageHeader
        title="自选股"
        titleExtra={
          <span className="inline-flex items-center gap-1.5">
            {/* 计数胶囊: 显示数/总数, mono 字体突出数字 */}
            <span className="inline-flex items-baseline gap-0.5 px-2 py-0.5 rounded-md bg-elevated/70 text-[11px]">
              <span className="font-mono font-semibold text-secondary tabular-nums">{sortedRows.length}</span>
              <span className="text-muted/50">/</span>
              <span className="font-mono text-muted tabular-nums">{allSymbols.length}</span>
              <span className="text-muted/60 ml-0.5">只</span>
            </span>
            {/* 过滤提示: 仅在有隐藏时出现, 柔和橙色融入整体 */}
            {hiddenCount > 0 && (
              <span
                className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-md text-[10px] font-medium bg-warning/12 text-warning/90 border border-warning/25 whitespace-nowrap"
                title={`当前有 ${hiddenCount} 只被筛选条件隐藏,清除筛选可查看全部`}
              >
                <Filter className="h-2.5 w-2.5" />
                已过滤 {hiddenCount}
              </span>
            )}
          </span>
        }
        right={
          <div className="flex items-center gap-2">
            {/* 筛选 / 搜索 */}
            <button
              onClick={() => setFilterOpen(v => !v)}
              className={`inline-flex items-center justify-center h-8 w-8 rounded-btn transition-colors duration-150 ease-smooth ${
                filterOpen || activeFilterCount > 0
                  ? 'bg-accent/15 text-accent hover:bg-accent/25'
                  : 'bg-elevated text-secondary hover:bg-elevated/80'
              }`}
              title={`筛选${activeFilterCount > 0 ? ` (${activeFilterCount})` : ''}`}
            >
              <Filter className="h-4 w-4" />
            </button>
            <StockSearchBox
              onPreview={(sym, name) => { setPreviewSymbol(sym); setPreviewName(name) }}
              existingSymbols={allSymbols as string[]}
              onAdd={(sym) => addMutation.mutate(sym)}
            />
            <div className="w-px h-5 bg-border" />
            {/* 视图 */}
            <button
              onClick={toggleView}
              className="inline-flex items-center justify-center h-8 w-8 rounded-btn bg-elevated hover:bg-elevated/80 text-secondary hover:text-foreground transition-colors duration-150 ease-smooth"
              title={viewMode === 'table' ? '卡片视图' : '列表视图'}
            >
              {viewMode === 'table' ? <LayoutGrid className="h-4 w-4" /> : <List className="h-4 w-4" />}
            </button>
            <div className="w-px h-5 bg-border" />
            {/* 自定义列 / 刷新 */}
            <button
              onClick={() => setCustomizerOpen(true)}
              className="inline-flex items-center justify-center h-8 w-8 rounded-btn bg-elevated hover:bg-elevated/80 text-secondary hover:text-foreground transition-colors duration-150 ease-smooth"
              title="自定义列"
            >
              <Settings2 className="h-4 w-4" />
            </button>
            <button
              onClick={() => enriched.refetch()}
              disabled={enriched.isFetching}
              className="inline-flex items-center justify-center h-8 w-8 rounded-btn bg-elevated hover:bg-elevated/80 text-secondary hover:text-foreground transition-colors duration-150 ease-smooth disabled:opacity-50"
              title="刷新"
            >
              <RefreshCw className={`h-4 w-4 ${enriched.isFetching ? 'animate-spin' : ''}`} />
            </button>
            {allSymbols.length > 0 && (
              <>
                <div className="w-px h-5 bg-border" />
                <button
                  onClick={() => setConfirmClear(true)}
                  className="inline-flex items-center justify-center h-8 w-8 rounded-btn bg-danger/10 text-danger hover:bg-danger/20 transition-colors duration-150 ease-smooth"
                  title="清空自选"
                >
                  <Trash2 className="h-4 w-4" />
                </button>
              </>
            )}
          </div>
        }
      />

      {/* 筛选栏 */}
      {filterOpen && (
        <div className="px-5 py-2 border-b border-border bg-surface/50 max-h-[184px] overflow-y-auto">
          {/* 板块筛选 */}
          <div className="mb-2">
            <div className="text-[10px] text-muted uppercase tracking-wider mb-0.5">板块</div>
            <div className="flex flex-wrap gap-1">
              {BOARDS.map(board => {
                const active = boardFilter.has(board)
                return (
                  <button
                    key={board}
                    onClick={() => toggleBoard(board)}
                    className={`px-2 py-0.5 rounded text-[11px] transition-colors ${
                      active
                        ? 'bg-accent/15 text-accent'
                        : 'bg-elevated text-secondary hover:text-foreground hover:bg-elevated/80'
                    }`}
                  >
                    {board}
                  </button>
                )
              })}
            </div>
          </div>
          {COLUMN_GROUPS.map(cat => {
            const items = colsByCategory[cat.label]?.filter(i => i.col)
            if (!items?.length) return null
            return (
              <div key={cat.label} className="mb-1.5 last:mb-0">
                <div className="text-[10px] text-muted uppercase tracking-wider mb-0.5">{cat.label}</div>
                <div className="flex flex-wrap gap-x-2 gap-y-1">
                  {items.map(item => {
                    const f = filters[item.id] || {}
                    const hasFilter = !!f.min || !!f.max || !!f.text
                    return (
                      <div key={item.id} className="flex items-center gap-0.5 text-[11px]">
                        <span className={`whitespace-nowrap ${hasFilter ? 'text-accent' : 'text-secondary'}`}>{item.label}</span>
                        <input
                          type="number"
                          value={f.min ?? ''}
                          onChange={e => updateFilter(item.id, { min: e.target.value })}
                          placeholder="min"
                          className={`w-12 h-5 rounded border text-[10px] px-1 placeholder:text-muted focus:outline-none ${
                            hasFilter ? 'border-accent/30 bg-accent/5' : 'border-border bg-elevated'
                          } text-foreground focus:border-accent/50`}
                        />
                        <span className="text-muted">~</span>
                        <input
                          type="number"
                          value={f.max ?? ''}
                          onChange={e => updateFilter(item.id, { max: e.target.value })}
                          placeholder="max"
                          className={`w-12 h-5 rounded border text-[10px] px-1 placeholder:text-muted focus:outline-none ${
                            hasFilter ? 'border-accent/30 bg-accent/5' : 'border-border bg-elevated'
                          } text-foreground focus:border-accent/50`}
                        />
                      </div>
                    )
                  })}
                </div>
              </div>
            )
          })}
          {activeFilterCount > 0 && (
            <button onClick={clearFilters} className="mt-1 text-[10px] text-danger hover:text-danger/80 transition-colors">
              清除全部筛选
            </button>
          )}
        </div>
      )}

      {/* 可滚动列表区 — 占满剩余高度，内部独立滚动，表头 sticky 固定 */}
      <div className="flex-1 min-h-0 overflow-y-auto">
        <div className="px-5 py-3">
          {/* 列表 */}
          {list.isLoading && <div className="text-sm text-muted">加载中…</div>}
          {list.isError && <div className="text-sm text-danger">读取自选失败</div>}

          {allSymbols.length === 0 ? (
            <EmptyState
              icon={Star}
              title="自选股为空"
              hint="点击右上角搜索按钮查找并预览标的，进入个股详情后可添加到自选。"
            />
          ) : viewMode === 'table' ? (
            <StockDataTable
              columns={visibleColumns}
              rows={sortedRows}
              headerSticky
              sort={sort}
              onSortToggle={handleSortToggle}
              rowKey={(r: any) => r.symbol}
              rowClassName={() => 'border-t border-border hover:bg-elevated/50 transition-colors duration-150 ease-smooth'}
              // 日k列表头：标签 + 显示/隐藏眼睛按钮
              renderHeaderContent={(col) => {
                if (col.source.type === 'builtin' && col.source.key === 'candle') {
                  return (
                    <span className="inline-flex items-center justify-center gap-1.5">
                      <span>{col.label}</span>
                      <button
                        type="button"
                        onClick={(event) => { event.stopPropagation(); toggleDailyKChart() }}
                        className={`inline-flex items-center justify-center w-5 h-5 rounded transition-colors ${
                          dailyKChartVisible
                            ? 'text-accent bg-accent/10 hover:bg-accent/20'
                            : 'text-muted hover:text-foreground hover:bg-elevated'
                        }`}
                        title={dailyKChartVisible ? '隐藏日k蜡烛' : '显示日k蜡烛'}
                        aria-label={dailyKChartVisible ? '隐藏日k蜡烛' : '显示日k蜡烛'}
                      >
                        {dailyKChartVisible ? <Eye className="h-3.5 w-3.5" /> : <EyeOff className="h-3.5 w-3.5" />}
                      </button>
                    </span>
                  )
                }
                return undefined
              }}
              renderCell={(r: any, col: ColumnConfig) => {
                // ext 列
                if (col.source.type === 'ext') {
                  return renderExtCell(r, col, expandedCells, handleToggleExpand)
                }
                const key = col.source.key
                const price = r.rt_price ?? r.close
                const pct = r.rt_pct ?? r.change_pct
                const name = r.rt_name ?? r.name
                // 自选页 symbol 列：预览 + 内嵌删除（减号图标，二次确认）
                if (key === 'symbol') {
                  const board = boardTag(r.symbol)
                  return (
                    <td className="px-1.5 py-1.5">
                      <div className="flex items-center gap-1 w-full">
                        <button
                          type="button"
                          onClick={() => { setPreviewSymbol(r.symbol); setPreviewName(name ?? '') }}
                          className="flex items-center gap-1 text-left min-w-0"
                        >
                          <span className="font-mono text-foreground text-xs group-hover:text-accent transition-colors duration-150">
                            {r.symbol}
                          </span>
                          {name && (
                            <span className="text-xs text-secondary truncate group-hover:text-foreground transition-colors duration-150">
                              {name}
                            </span>
                          )}
                          {board ? (
                            <span className={`shrink-0 inline-flex items-center justify-center w-[18px] h-[18px] rounded text-[9px] font-bold leading-none border ${board.color}`}>
                              {board.label}
                            </span>
                          ) : null}
                          {monitoredSymbols.has(r.symbol) && <span className="ml-2"><RealtimeDot /></span>}
                        </button>
                        {/* 删除入口：默认减号图标，二次确认时替换为确定按钮 */}
                        <div className="ml-auto pl-1 shrink-0">
                          {confirmRemove === r.symbol ? (
                            <div className="flex items-center gap-1">
                              <button
                                onClick={() => { remove.mutate(r.symbol); setConfirmRemove(null) }}
                                className="px-1.5 py-0.5 rounded text-[10px] text-danger bg-danger/10 hover:bg-danger/20 transition-colors"
                              >
                                确认
                              </button>
                              <button
                                onClick={() => setConfirmRemove(null)}
                                className="p-0.5 text-muted hover:text-foreground transition-colors"
                              >
                                <X className="h-3 w-3" />
                              </button>
                            </div>
                          ) : (
                            <div className="flex items-center gap-1">
                              <button
                                onClick={() => setConfirmRemove(r.symbol)}
                                className="p-0.5 text-muted hover:text-danger transition-colors duration-150 ease-smooth"
                                aria-label="移除"
                                title="移除"
                              >
                                <Minus className="h-3.5 w-3.5" />
                              </button>
                              <button
                                onClick={() => moveToTop.mutate(r.symbol)}
                                disabled={moveToTop.isPending || allSymbols[0] === r.symbol}
                                className="p-0.5 text-muted hover:text-accent transition-colors duration-150 ease-smooth disabled:opacity-30 disabled:hover:text-muted"
                                aria-label="移到顶部"
                                title="移到顶部"
                              >
                                <ChevronsUp className="h-3.5 w-3.5" />
                              </button>
                            </div>
                          )}
                        </div>
                      </div>
                    </td>
                  )
                }
                // 实时行情列：price/pct/amount 使用 rt_ 回退（自选页有实时推送）
                const numCls = 'px-2 py-1.5 text-right num tabular-nums'
                if (key === 'price') {
                  return <td className={`${numCls} ${priceColorClass(pct)}`}>{fmtPrice(price)}</td>
                }
                if (key === 'pct') {
                  return <td className={`${numCls} ${priceColorClass(pct)}`}>{fmtPct(pct)}</td>
                }
                if (key === 'amount') {
                  return <td className={`${numCls} text-secondary`}>{fmtBigNum(r.rt_amount ?? r.amount)}</td>
                }
                if (key === 'turnover') {
                  return <td className={`${numCls} ${turnoverColor(r.turnover_rate)}`}>{r.turnover_rate != null ? `${r.turnover_rate.toFixed(2)}%` : '—'}</td>
                }
                // 信号列
                if (key === 'signals') {
                  const signals = getSignals(r)
                  return (
                    <td className="px-2 py-1.5">
                      {signals.length > 0 && (
                        <div className="flex flex-wrap gap-0.5">
                          {signals.slice(0, 3).map((s) => (
                            <span key={s.label} className={`inline-block px-1.5 py-px rounded text-[10px] font-medium leading-tight ${signalCls(s.type)}`}>
                              {s.label}
                            </span>
                          ))}
                          {signals.length > 3 && (
                            <span className="text-[10px] text-muted">+{signals.length - 3}</span>
                          )}
                        </div>
                      )}
                    </td>
                  )
                }
                // 日k列
                if (key === 'candle') {
                  return (
                    <td
                      className="px-2 py-1.5"
                      style={{ width: candleSize.width, minWidth: candleSize.width, maxWidth: candleSize.width, height: candleSize.height }}
                    >
                      <MiniCandlestick rows={klineData[r.symbol] ?? []} width={candleSize.width} height={candleSize.height} />
                    </td>
                  )
                }
                // 其余纯数据列 → 共享原语
                return renderBuiltinDataCell(r, col)
              }}
              className="rounded-card overflow-x-auto"
            />
          ) : (
            <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-3">
              {rows.map((r: any) => (
                <StockCard
                  key={r.symbol}
                  r={r}
                  candleRows={klineData[r.symbol] ?? []}
                  showCandle={dailyKVisible}
                  onPreview={(sym, name) => { setPreviewSymbol(sym); setPreviewName(name) }}
                  onConfirmRemove={(sym) => { remove.mutate(sym); setConfirmRemove(null) }}
                  onCancelRemove={() => setConfirmRemove(null)}
                  onRequestRemove={(sym) => setConfirmRemove(sym)}
                  confirmRemove={confirmRemove}
                  extCols={visibleExtCols}
                  expandedCells={expandedCells}
                  onToggleExpand={handleToggleExpand}
                  isMonitored={monitoredSymbols.has(r.symbol)}
                />
              ))}
            </div>
          )}
        </div>
      </div>

      {/* 清空确认弹窗 */}
      <AnimatePresence>
        {confirmClear && (
          <div className="fixed inset-0 z-50 flex items-center justify-center">
            <motion.div
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
              transition={{ duration: 0.15 }}
              className="absolute inset-0 bg-black/60 backdrop-blur-sm"
              onClick={() => setConfirmClear(false)}
            />
            <motion.div
              initial={{ opacity: 0, scale: 0.95, y: 12 }}
              animate={{ opacity: 1, scale: 1, y: 0 }}
              exit={{ opacity: 0, scale: 0.97, y: 8 }}
              transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
              className="relative w-[90vw] max-w-[380px] rounded-card border border-border bg-base shadow-2xl p-6"
            >
              <h3 className="text-sm font-medium text-foreground mb-2">确认清空自选</h3>
              <p className="text-xs text-secondary mb-5">
                将移除全部 {allSymbols.length} 只自选股，此操作不可恢复。
              </p>
              <div className="flex items-center justify-end gap-2">
                <button
                  onClick={() => setConfirmClear(false)}
                  className="px-3 py-1.5 rounded-btn bg-elevated text-secondary hover:bg-elevated/80 text-sm transition-colors"
                >
                  取消
                </button>
                <button
                  onClick={() => clearAll.mutate()}
                  disabled={clearAll.isPending}
                  className="px-3 py-1.5 rounded-btn bg-danger/15 text-danger hover:bg-danger/25 text-sm font-medium transition-colors disabled:opacity-50"
                >
                  {clearAll.isPending ? '清除中...' : '确认清空'}
                </button>
              </div>
            </motion.div>
          </div>
        )}
      </AnimatePresence>

      {/* 列自定义侧栏 */}
      <ColumnCustomizer
        columns={columns}
        onChange={handleColumnsChange}
        open={customizerOpen}
        onClose={() => setCustomizerOpen(false)}
      />

      <StockPreviewDialog
        symbol={previewSymbol}
        name={previewName}
        onClose={closePreview}
      />
    </div>
  )
}
