import { useState, useEffect, useRef, useCallback } from 'react'
import { Button, Checkbox, Dropdown, Input, Modal, Radio, Select, Spin, message as antMsg, Popover } from 'antd'
import {
  CheckCircleOutlined, FileTextOutlined, FlagOutlined, PaperClipOutlined,
  SearchOutlined, SendOutlined, SwapOutlined, FilterOutlined,
  ExportOutlined, BookOutlined, PictureOutlined, ThunderboltOutlined,
  StopOutlined,
} from '@ant-design/icons'
import EmojiPicker, { insertAtCursor } from '@/components/common/EmojiPicker'
import { ChatImage } from '@/components/common/ImagePreview'
import MarkdownBody, { stripMarkdown } from '@/components/common/MarkdownBody'
import { useAuth } from '@/stores/auth'
import {
  addSessionNote, claimSession, createBlacklist, endSession, getAvailableAgents, getCustomer, getCustomers, getKnowledgeEntries,
  getQuickReplies, getSession, getSessionMessages, getSessions, markSessionRead, sendSessionMessage,
  suggestQuickReplies, transferSession, updateCustomer, updateSessionPriority, uploadImage, useQuickReply,
  type AvailableAgent, type BlacklistDuration, type Customer, type CustomerContact, type KnowledgeEntry, type Message, type QuickReply,
  type Session, type SessionEvent, type VisitorPageView,
} from '@/services/api'

type CustomerEditableField = 'phone' | 'email' | 'wechat' | 'qq'

/** 客户侧栏基本信息：点击输入，失焦/回车保存 */
function CustomerInfoField({
  label,
  value,
  placeholder,
  disabled,
  saving,
  onSave,
}: {
  label: string
  value: string
  placeholder?: string
  disabled?: boolean
  saving?: boolean
  onSave: (next: string) => Promise<void> | void
}) {
  const [editing, setEditing] = useState(false)
  const [draft, setDraft] = useState(value)
  const inputRef = useRef<HTMLInputElement>(null)

  useEffect(() => {
    if (!editing) setDraft(value)
  }, [value, editing])

  useEffect(() => {
    if (editing) {
      requestAnimationFrame(() => {
        inputRef.current?.focus()
        inputRef.current?.select()
      })
    }
  }, [editing])

  const commit = async () => {
    const next = draft.trim()
    setEditing(false)
    if (next === (value || '').trim()) return
    await onSave(next)
  }

  return (
    <div className="flex items-center gap-2 min-h-[28px]">
      <span className="shrink-0 text-neutral-400 min-w-12 leading-7">{label}</span>
      {editing ? (
        <input
          ref={inputRef}
          className="min-w-0 flex-1 h-7 px-2 rounded border border-[#2563eb] bg-white text-[13px] text-neutral-800 outline-none"
          value={draft}
          disabled={saving || disabled}
          placeholder={placeholder || '点击填写'}
          onChange={e => setDraft(e.target.value)}
          onBlur={() => { void commit() }}
          onKeyDown={e => {
            if (e.key === 'Enter') {
              e.preventDefault()
              void commit()
            }
            if (e.key === 'Escape') {
              setDraft(value)
              setEditing(false)
            }
          }}
        />
      ) : (
        <button
          type="button"
          disabled={disabled || saving}
          title="点击编辑"
          onClick={() => !disabled && setEditing(true)}
          className={`min-w-0 flex-1 text-left h-7 px-1 -mx-1 rounded truncate text-[13px] leading-7 ${
            value
              ? 'text-neutral-800 hover:bg-neutral-100'
              : 'text-neutral-400 hover:bg-neutral-100'
          } disabled:cursor-default disabled:hover:bg-transparent`}
        >
          {value || placeholder || '点击填写'}
        </button>
      )}
    </div>
  )
}

/** 按 id 合并消息，再按 seq / id 排序 */
function mergeMessagesBySeq(existing: Message[], incoming: Message[]): Message[] {
  const map = new Map<number, Message>()
  for (const item of existing) map.set(item.id, item)
  for (const item of incoming) map.set(item.id, item)
  return Array.from(map.values()).sort((a, b) => {
    const seqDiff = (a.seq || 0) - (b.seq || 0)
    return seqDiff !== 0 ? seqDiff : a.id - b.id
  })
}

function maxMessageSeq(messages: Message[]): number {
  return messages.reduce((acc, item) => Math.max(acc, item.seq || 0), 0)
}

function normalizeMessage(raw: Partial<Message> & { id?: number }, sessionId: number): Message | null {
  if (!raw?.id) return null
  return {
    id: Number(raw.id),
    session_id: Number(raw.session_id ?? sessionId),
    sender_type: raw.sender_type === 'agent' ? 'agent' : 'visitor',
    sender_id: raw.sender_id ?? null,
    content: String(raw.content ?? ''),
    type: raw.type === 'image' ? 'image' : 'text',
    seq: Number(raw.seq ?? 0),
    sent_at: raw.sent_at || new Date().toISOString(),
  }
}

const endReasons = [
  { value: 'resolved', label: '已解决' },
  { value: 'no_response', label: '无人回复' },
  { value: 'visitor_left', label: '访客离开' },
  { value: 'transferred', label: '已转接' },
  { value: 'other', label: '其他' },
]
/** 列表项展示：紧急 / 等待中 / 进行中 */
function listStatusMeta(session: Session) {
  if (session.priority === 'urgent') {
    return { label: '紧急', bar: '#dc2626', avatarBg: '#fef2f2', avatarColor: '#dc2626', dot: '#dc2626' }
  }
  if (session.status === 'waiting') {
    return { label: '等待中', bar: '#d97706', avatarBg: '#fffbeb', avatarColor: '#d97706', dot: '#d97706' }
  }
  return { label: '进行中', bar: '#2563eb', avatarBg: '#eff6ff', avatarColor: '#2563eb', dot: '#2563eb' }
}

/** 坐席在线状态（转接列表） */
function agentStatusMeta(status: string) {
  switch (status) {
    case 'online':
      return { text: '在线', dot: '#16a34a', bg: '#ecfdf5', color: '#15803d' }
    case 'busy':
      return { text: '忙碌', dot: '#d97706', bg: '#fffbeb', color: '#b45309' }
    case 'disabled':
      return { text: '停用', dot: '#94a3b8', bg: '#f1f5f9', color: '#64748b' }
    default:
      return { text: '离线', dot: '#94a3b8', bg: '#f1f5f9', color: '#64748b' }
  }
}

function relativeTime(iso?: string | null) {
  if (!iso) return ''
  const diff = Date.now() - new Date(iso).getTime()
  const mins = Math.floor(diff / 60000)
  if (mins < 1) return '刚刚'
  if (mins < 60) return `${mins}分钟前`
  const hours = Math.floor(mins / 60)
  if (hours < 24) return `${hours}小时前`
  const days = Math.floor(hours / 24)
  if (days < 7) return `${days}天前`
  return new Date(iso).toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })
}

function parseTags(tagsStr: string): string[] {
  try {
    const parsed = JSON.parse(tagsStr)
    return Array.isArray(parsed) ? parsed : []
  } catch {
    return tagsStr ? tagsStr.split(',').map(t => t.trim()).filter(Boolean) : []
  }
}

function channelLabel(source?: string) {
  if (!source) return '网页'
  if (source.includes('微信') || source.toLowerCase().includes('wechat')) return '微信'
  if (source.toLowerCase().includes('app')) return 'APP'
  if (source.includes('电话')) return '电话'
  return source.length > 6 ? '网页' : source
}

interface SessionDetail {
  messages: Message[]
  events: SessionEvent[]
  pageViews: VisitorPageView[]
  pendingCount: number
}

function formatOnlineDuration(startIso?: string | null, endIso?: string | null, nowMs?: number): string {
  if (!startIso) return '—'
  const start = new Date(startIso).getTime()
  if (!Number.isFinite(start)) return '—'
  const end = endIso ? new Date(endIso).getTime() : (nowMs ?? Date.now())
  let sec = Math.max(0, Math.floor((end - start) / 1000))
  const h = Math.floor(sec / 3600)
  sec %= 3600
  const m = Math.floor(sec / 60)
  const s = sec % 60
  if (h > 0) return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
  return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
}

function shortPagePath(url?: string): string {
  if (!url) return '—'
  try {
    const u = new URL(url)
    const path = u.pathname + (u.search || '') + (u.hash || '')
    return path.length > 56 ? `${path.slice(0, 54)}…` : path || '/'
  } catch {
    return url.length > 56 ? `${url.slice(0, 54)}…` : url
  }
}

const Dashboard = () => {
  const { user } = useAuth()
  const [sessions, setSessions] = useState<Session[]>([])
  const [customers, setCustomers] = useState<Record<number, Customer>>({})
  const [loading, setLoading] = useState(true)
  const [selectedId, setSelectedId] = useState<number | null>(null)
  const [detail, setDetail] = useState<SessionDetail | null>(null)
  const [detailLoading, setDetailLoading] = useState(false)
  const [messageInput, setMessageInput] = useState('')
  const [search, setSearch] = useState('')
  const [sending, setSending] = useState(false)
  const [transferOpen, setTransferOpen] = useState(false)
  const [availableAgents, setAvailableAgents] = useState<AvailableAgent[]>([])
  const [targetAgentID, setTargetAgentID] = useState<number>()
  const [blacklistOpen, setBlacklistOpen] = useState(false)
  const [blacklistKind, setBlacklistKind] = useState<'ip' | 'device'>('ip')
  const [blacklistDuration, setBlacklistDuration] = useState<BlacklistDuration>('7d')
  const [blacklistReason, setBlacklistReason] = useState('')
  const [blacklistEndSession, setBlacklistEndSession] = useState(true)
  const [blacklistSaving, setBlacklistSaving] = useState(false)
  const [endingOpen, setEndingOpen] = useState(false)
  const [endReason, setEndReason] = useState('resolved')
  const [knowledgeOpen, setKnowledgeOpen] = useState(false)
  const [knowledgeKeyword, setKnowledgeKeyword] = useState('')
  const [knowledgeEntries, setKnowledgeEntries] = useState<KnowledgeEntry[]>([])
  const [knowledgeLoading, setKnowledgeLoading] = useState(false)
  const [quickOpen, setQuickOpen] = useState(false)
  const [quickKeyword, setQuickKeyword] = useState('')
  const [quickList, setQuickList] = useState<QuickReply[]>([])
  const [quickLoading, setQuickLoading] = useState(false)
  /** 右侧栏快捷回复：团队 / 个人 */
  const [sidebarQuickScope, setSidebarQuickScope] = useState<'team' | 'personal'>('team')
  const [sidebarQuickKeyword, setSidebarQuickKeyword] = useState('')
  const [sidebarQuickList, setSidebarQuickList] = useState<QuickReply[]>([])
  const [sidebarQuickLoading, setSidebarQuickLoading] = useState(false)
  /** 输入框快捷回复建议：slash=/ 输入码；keyword=正文关键字 */
  const [suggestOpen, setSuggestOpen] = useState(false)
  const [suggestMode, setSuggestMode] = useState<'slash' | 'keyword'>('slash')
  const [slashPrefix, setSlashPrefix] = useState('')
  const [keywordQuery, setKeywordQuery] = useState('')
  const [suggestItems, setSuggestItems] = useState<QuickReply[]>([])
  const [suggestIndex, setSuggestIndex] = useState(0)
  const keywordTimer = useRef<number | null>(null)
  /** 避免唯一匹配自动填入后立刻再次触发 */
  const autoAppliedRef = useRef('')
  const [pendingImage, setPendingImage] = useState<{ file: File; preview: string } | null>(null)
  const [noteInput, setNoteInput] = useState('')
  const [savingNote, setSavingNote] = useState(false)
  const [customerHistory, setCustomerHistory] = useState<Session[]>([])
  const [statusFilter, setStatusFilter] = useState<'all' | 'waiting' | 'active'>('all')
  const [priorityFilter, setPriorityFilter] = useState<'all' | 'urgent'>('all')
  const [filterOpen, setFilterOpen] = useState(false)
  const [visitorTyping, setVisitorTyping] = useState(false)
  /** 访客输入框未发送草稿（实时） */
  const [visitorDraft, setVisitorDraft] = useState('')
  const [customerContacts, setCustomerContacts] = useState<CustomerContact[]>([])
  const [savingCustomerField, setSavingCustomerField] = useState<CustomerEditableField | null>(null)
  /** 驱动在线读秒每秒刷新 */
  const [clockTick, setClockTick] = useState(0)
  const initialLoad = useRef(true)
  const chatEndRef = useRef<HTMLDivElement>(null)
  const fileInputRef = useRef<HTMLInputElement>(null)
  const messageInputRef = useRef<HTMLTextAreaElement>(null)
  const socketRef = useRef<WebSocket | null>(null)
  const lastTypingAt = useRef(0)
  const visitorTypingTimer = useRef<number | null>(null)
  /** 当前选中会话，供稳定 WS 闭包读取，避免切会话时重连 */
  const selectedIdRef = useRef<number | null>(null)
  /** 各会话本地已同步到的最大 seq，用于重连增量 catch-up */
  const lastSeqBySessionRef = useRef<Record<number, number>>({})
  const loadAllRef = useRef<(opts?: { silent?: boolean }) => Promise<void>>(async () => {})
  const loadDetailRef = useRef<(id: number, markRead?: boolean, opts?: { silent?: boolean }) => Promise<void>>(async () => {})
  const syncAfterSeqRef = useRef<(id: number, opts?: { markRead?: boolean }) => Promise<void>>(async () => {})

  const rememberSeq = useCallback((sessionId: number, seq: number) => {
    if (!sessionId || !seq || seq <= 0) return
    const prev = lastSeqBySessionRef.current[sessionId] || 0
    if (seq > prev) lastSeqBySessionRef.current[sessionId] = seq
  }, [])

  const rememberMessagesSeq = useCallback((sessionId: number, messages: Message[]) => {
    rememberSeq(sessionId, maxMessageSeq(messages))
  }, [rememberSeq])

  const insertEmoji = (emoji: string) => {
    const { next, cursor } = insertAtCursor(messageInputRef.current, messageInput, emoji)
    setMessageInput(next)
    emitTyping()
    requestAnimationFrame(() => {
      const el = messageInputRef.current
      if (!el) return
      el.focus()
      el.setSelectionRange(cursor, cursor)
    })
  }

  const isManager = user?.role === 'admin' || user?.role === 'supervisor'

  const loadAll = useCallback(async (opts?: { silent?: boolean }) => {
    if (!opts?.silent) setLoading(true)
    try {
      const [sessionRes, customerRes] = await Promise.all([
        getSessions({ page: 1, pageSize: 100 }),
        getCustomers({ page: 1, pageSize: 100 }),
      ])
      const sessionList = Array.isArray(sessionRes.list) ? sessionRes.list : []
      const customerList = Array.isArray(customerRes.list) ? customerRes.list : []
      setSessions(sessionList)
      setCustomers(Object.fromEntries(customerList.map(customer => [customer.id, customer])))
      if (sessionList.length > 0 && initialLoad.current) {
        const preferred = sessionList.find(session => session.status === 'active')
          || sessionList.find(session => session.priority === 'urgent')
          || sessionList[0]
        setSelectedId(preferred.id)
        initialLoad.current = false
      }
    } catch {
      if (!opts?.silent) antMsg.error('加载会话失败')
      if (!opts?.silent) {
        setSessions([])
        setCustomers({})
      }
    } finally {
      if (!opts?.silent) setLoading(false)
    }
  }, [])

  const loadDetail = useCallback(async (id: number, markRead = true, opts?: { silent?: boolean }) => {
    if (!opts?.silent) setDetailLoading(true)
    try {
      const response = await getSession(id)
      const data = response.data
      const messages = data.messages || []
      setDetail({
        messages,
        events: data.events || [],
        // 接口已 desc；再保险按时间新→旧
        pageViews: (Array.isArray(data.page_views) ? data.page_views : [])
          .slice()
          .sort((a, b) => new Date(b.entered_at).getTime() - new Date(a.entered_at).getTime()),
        pendingCount: data.pending_count || 0,
      })
      rememberMessagesSeq(id, messages)
      if (typeof data.max_seq === 'number') rememberSeq(id, data.max_seq)
      // 用详情接口的完整会话字段（含 IP/地区）回填列表，保证顶栏展示准确
      if (data.session) {
        setSessions(previous => previous.map(session =>
          session.id === id
            ? {
                ...session,
                ...data.session,
                unread_count: markRead ? 0 : session.unread_count,
                last_message: data.session.last_message ?? session.last_message,
                last_message_at: data.session.last_message_at ?? session.last_message_at,
              }
            : session,
        ))
        // 仅在主动打开会话时恢复草稿；静默轮询不覆盖 WS 实时值
        if (!opts?.silent && selectedIdRef.current === id) {
          setVisitorDraft(data.session.draft_text || '')
        }
      } else if (markRead) {
        setSessions(previous => previous.map(session => session.id === id ? { ...session, unread_count: 0 } : session))
      }
      if (markRead) {
        await markSessionRead(id)
      }
    } catch {
      if (!opts?.silent) antMsg.error('加载消息失败')
    } finally {
      if (!opts?.silent) setDetailLoading(false)
    }
    setTimeout(() => chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }), 80)
  }, [rememberMessagesSeq, rememberSeq])

  /** 重连 / 发现 seq 空洞时：只拉 after_seq 之后的消息并合并 */
  const syncAfterSeq = useCallback(async (id: number, opts?: { markRead?: boolean }) => {
    const afterSeq = lastSeqBySessionRef.current[id] || 0
    // 本地尚无游标：退回全量详情
    if (afterSeq <= 0) {
      await loadDetail(id, opts?.markRead ?? true, { silent: true })
      return
    }
    try {
      let cursor = afterSeq
      let guard = 0
      let latest: Message | null = null
      while (guard < 10) {
        guard += 1
        const response = await getSessionMessages(id, cursor, 200)
        const batch = response.data.messages || []
        const maxSeq = Number(response.data.max_seq || cursor)
        if (batch.length > 0) {
          latest = batch[batch.length - 1]
          setDetail(previous => {
            if (selectedIdRef.current !== id) return previous
            if (!previous) return { messages: batch, events: [], pageViews: [], pendingCount: 0 }
            return { ...previous, messages: mergeMessagesBySeq(previous.messages, batch) }
          })
          rememberMessagesSeq(id, batch)
        }
        rememberSeq(id, maxSeq)
        const lastBatchSeq = batch.length ? maxMessageSeq(batch) : cursor
        if (!response.data.has_more || lastBatchSeq >= maxSeq || batch.length === 0) break
        cursor = lastBatchSeq
      }
      if (latest && selectedIdRef.current === id) {
        const preview = latest.type === 'image' ? '[图片]' : stripMarkdown(latest.content)
        setSessions(previous => previous.map(session =>
          session.id === id
            ? {
                ...session,
                last_message: preview,
                last_message_at: latest!.sent_at,
                unread_count: opts?.markRead === false ? session.unread_count : 0,
              }
            : session,
        ))
        setTimeout(() => chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }), 40)
      }
      if (opts?.markRead !== false) {
        await markSessionRead(id).catch(() => {})
      }
    } catch {
      await loadDetail(id, opts?.markRead ?? true, { silent: true })
    }
  }, [loadDetail, rememberMessagesSeq, rememberSeq])

  /** 将 WS 推送的消息直接追加；若 seq 有空洞则触发增量同步 */
  const appendPushedMessage = useCallback((sessionId: number, raw: Partial<Message> & { id?: number }) => {
    const msg = normalizeMessage(raw, sessionId)
    if (!msg) return false

    const known = lastSeqBySessionRef.current[sessionId] || 0
    // 已知游标且出现空洞（漏消息）→ 走 after_seq 增量补齐
    if (known > 0 && msg.seq > 0 && msg.seq > known + 1) {
      void syncAfterSeqRef.current(sessionId, { markRead: true })
      return true
    }

    const preview = msg.type === 'image' ? '[图片]' : stripMarkdown(msg.content)
    let appended = false
    setDetail(previous => {
      if (selectedIdRef.current !== sessionId) return previous
      if (!previous) {
        appended = true
        return { messages: [msg], events: [], pageViews: [], pendingCount: 0 }
      }
      if (previous.messages.some(item => item.id === msg.id)) {
        appended = true
        return previous
      }
      appended = true
      return { ...previous, messages: mergeMessagesBySeq(previous.messages, [msg]) }
    })
    setSessions(previous => previous.map(session => {
      if (session.id !== sessionId) return session
      const viewing = selectedIdRef.current === sessionId
      return {
        ...session,
        last_message: preview,
        last_message_at: msg.sent_at,
        unread_count: viewing ? 0 : (session.unread_count || 0) + (msg.sender_type === 'visitor' ? 1 : 0),
      }
    }))
    if (msg.seq > 0) rememberSeq(sessionId, msg.seq)
    if (selectedIdRef.current === sessionId) {
      setTimeout(() => chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }), 40)
      void markSessionRead(sessionId).catch(() => {})
    }
    return appended
  }, [rememberSeq])

  useEffect(() => {
    selectedIdRef.current = selectedId
  }, [selectedId])

  useEffect(() => {
    loadAllRef.current = loadAll
    loadDetailRef.current = loadDetail
    syncAfterSeqRef.current = syncAfterSeq
  }, [loadAll, loadDetail, syncAfterSeq])

  useEffect(() => { void loadAll() }, [loadAll])

  // 稳定 WebSocket：仅 token 变化时建连；切会话不重连；断线自动重连（ws / wss）
  useEffect(() => {
    if (!user?.token) return
    let disposed = false
    let reconnectTimer: number | null = null
    let attempt = 0

    const clearReconnect = () => {
      if (reconnectTimer != null) {
        window.clearTimeout(reconnectTimer)
        reconnectTimer = null
      }
    }

    const connect = () => {
      if (disposed) return
      const scheme = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
      const socket = new WebSocket(`${scheme}//${window.location.host}/api/ws`, ['kefu-v1', user.token])
      socketRef.current = socket

      socket.onopen = () => {
        attempt = 0
        // 重连后：列表静默刷新 + 当前会话按 seq 增量 catch-up
        void loadAllRef.current({ silent: true })
        const sid = selectedIdRef.current
        if (sid != null) void syncAfterSeqRef.current(sid, { markRead: true })
      }

      socket.onmessage = event => {
        try {
          const payload = JSON.parse(event.data) as {
            type?: string
            session_id?: number | string
            data?: Partial<Message> & { from?: string; status?: string; agent_name?: string }
          }
          const sid = Number(payload.session_id)
          const current = selectedIdRef.current
          const sameSession = Number.isFinite(sid) && current != null && sid === Number(current)

          if (payload.type === 'typing' && sameSession && payload.data?.from === 'visitor') {
            setVisitorTyping(true)
            if (visitorTypingTimer.current) clearTimeout(visitorTypingTimer.current)
            visitorTypingTimer.current = window.setTimeout(() => setVisitorTyping(false), 1800)
            return
          }

          if (payload.type === 'input_draft' && sameSession) {
            const text = typeof (payload.data as { text?: string })?.text === 'string'
              ? String((payload.data as { text?: string }).text)
              : ''
            setVisitorDraft(text)
            setVisitorTyping(text.length > 0)
            if (visitorTypingTimer.current) clearTimeout(visitorTypingTimer.current)
            if (text) {
              visitorTypingTimer.current = window.setTimeout(() => setVisitorTyping(false), 2500)
            }
            return
          }

          if (payload.type === 'customer_updated' && sameSession && payload.data) {
            const d = payload.data as {
              customer?: Customer
              contacts?: CustomerContact[]
            }
            if (d.customer) {
              setCustomers(prev => ({ ...prev, [d.customer!.id]: { ...prev[d.customer!.id], ...d.customer! } }))
            }
            if (Array.isArray(d.contacts)) {
              setCustomerContacts(d.contacts)
            }
            return
          }

          if (payload.type === 'message') {
            if (sameSession) {
              setVisitorTyping(false)
              // 访客发出消息后草稿应清空（后端也会推 input_draft 空串，这里兜底）
              const sender = (payload.data as { sender_type?: string })?.sender_type
              if (sender === 'visitor') setVisitorDraft('')
              const ok = appendPushedMessage(sid, payload.data || {})
              if (!ok) void syncAfterSeqRef.current(sid, { markRead: true })
            }
            void loadAllRef.current({ silent: true })
            return
          }

          if (payload.type === 'session_created' || payload.type === 'session_updated') {
            void loadAllRef.current({ silent: true })
            // 转接/分配/结束等会写 SessionEvent，需全量刷新详情（含 events）
            if (sameSession) void loadDetailRef.current(sid, false, { silent: true })
          }

          if (payload.type === 'page_view' && sameSession && payload.data) {
            const raw = payload.data as unknown as VisitorPageView & { title_fix?: boolean }
            if (raw?.url) {
              setDetail(prev => {
                if (!prev) return prev
                const idx = prev.pageViews.findIndex(p => p.id === raw.id)
                if (idx >= 0) {
                  // 标题纠正：更新已有条目，并移到最前（仍是当前页）
                  const next = prev.pageViews.slice()
                  const item = { ...next[idx], title: raw.title || next[idx].title, url: raw.url }
                  next.splice(idx, 1)
                  return { ...prev, pageViews: [item, ...next] }
                }
                if (prev.pageViews.some(p => p.url === raw.url && !raw.id)) return prev
                // 最新轨迹插到最上方
                return { ...prev, pageViews: [raw, ...prev.pageViews] }
              })
              setSessions(previous => previous.map(session =>
                session.id === sid
                  ? {
                      ...session,
                      current_url: raw.url,
                      current_title: raw.title || session.current_title,
                      last_seen_at: raw.entered_at || session.last_seen_at,
                    }
                  : session,
              ))
            }
          }
        } catch {
          // ignore malformed frames
        }
      }

      socket.onclose = () => {
        if (socketRef.current === socket) socketRef.current = null
        if (disposed) return
        const delay = Math.min(1000 * (2 ** attempt), 15000)
        attempt += 1
        clearReconnect()
        reconnectTimer = window.setTimeout(connect, delay)
      }

      socket.onerror = () => {
        // onclose 会负责重连
        try { socket.close() } catch { /* ignore */ }
      }
    }

    connect()

    return () => {
      disposed = true
      clearReconnect()
      const sock = socketRef.current
      socketRef.current = null
      if (sock) {
        sock.onclose = null
        sock.close()
      }
      if (visitorTypingTimer.current) clearTimeout(visitorTypingTimer.current)
    }
  }, [user?.token, appendPushedMessage])

  useEffect(() => {
    setVisitorTyping(false)
    setVisitorDraft('')
    setCustomerContacts([])
  }, [selectedId])

  useEffect(() => {
    if (visitorTyping) {
      setTimeout(() => chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }), 40)
    }
  }, [visitorTyping])

  useEffect(() => {
    if (visitorDraft.trim()) {
      setTimeout(() => chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }), 40)
    }
  }, [visitorDraft])

  useEffect(() => {
    if (selectedId) loadDetail(selectedId)
  }, [selectedId, loadDetail])

  useEffect(() => {
    if (!knowledgeOpen) return
    setKnowledgeLoading(true)
    getKnowledgeEntries({ search: knowledgeKeyword, page: 1 }).then(response => {
      setKnowledgeEntries(Array.isArray(response.list) ? response.list : [])
    }).catch(() => setKnowledgeEntries([])).finally(() => setKnowledgeLoading(false))
  }, [knowledgeOpen, knowledgeKeyword])

  useEffect(() => {
    if (!quickOpen) return
    setQuickLoading(true)
    getQuickReplies({
      scope: 'all',
      q: quickKeyword.trim() || undefined,
      page: 1,
      pageSize: 50,
    }).then(response => {
      setQuickList(Array.isArray(response.list) ? response.list : [])
    }).catch(() => setQuickList([])).finally(() => setQuickLoading(false))
  }, [quickOpen, quickKeyword])

  // 右侧栏快捷回复列表（始终可用）
  useEffect(() => {
    let cancelled = false
    setSidebarQuickLoading(true)
    getQuickReplies({
      scope: sidebarQuickScope,
      // 团队仅展示已发布；个人展示自己的全部可用
      status: sidebarQuickScope === 'team' ? 'published' : undefined,
      q: sidebarQuickKeyword.trim() || undefined,
      page: 1,
      pageSize: 50,
    }).then(response => {
      if (cancelled) return
      setSidebarQuickList(Array.isArray(response.list) ? response.list : [])
    }).catch(() => {
      if (!cancelled) setSidebarQuickList([])
    }).finally(() => {
      if (!cancelled) setSidebarQuickLoading(false)
    })
    return () => { cancelled = true }
  }, [sidebarQuickScope, sidebarQuickKeyword])

  const closeSuggest = useCallback(() => {
    setSuggestOpen(false)
    setSuggestItems([])
    setSuggestIndex(0)
    setSlashPrefix('')
    setKeywordQuery('')
  }, [])

  // / 模式：拉输入码建议（按个人调用频次）
  useEffect(() => {
    if (!suggestOpen || suggestMode !== 'slash') return
    let cancelled = false
    suggestQuickReplies({ mode: 'slash', prefix: slashPrefix }).then(res => {
      if (cancelled) return
      const list = Array.isArray(res.data) ? res.data : []
      setSuggestItems(list)
      setSuggestIndex(0)
      // 输入码细化后仅剩 1 条 → 自动填入（纯 / 不自动，避免误触）
      if (list.length === 1 && slashPrefix.length > 0) {
        const only = list[0]
        const key = `slash:${slashPrefix}:${only.id}`
        if (autoAppliedRef.current !== key) {
          autoAppliedRef.current = key
          // 延迟到下一 tick，避免与 onChange 竞态
          window.setTimeout(() => {
            void applyQuickReplyRef.current(only, 'slash')
          }, 0)
        }
      }
    }).catch(() => {
      if (!cancelled) setSuggestItems([])
    })
    return () => { cancelled = true }
  }, [suggestOpen, suggestMode, slashPrefix])

  // 关键字模式：防抖搜索标题/内容
  useEffect(() => {
    if (!suggestOpen || suggestMode !== 'keyword') return
    const q = keywordQuery.trim()
    if (q.length < 2) {
      setSuggestItems([])
      return
    }
    let cancelled = false
    suggestQuickReplies({ mode: 'keyword', q }).then(res => {
      if (cancelled) return
      const list = Array.isArray(res.data) ? res.data : []
      setSuggestItems(list)
      setSuggestIndex(0)
    }).catch(() => {
      if (!cancelled) setSuggestItems([])
    })
    return () => { cancelled = true }
  }, [suggestOpen, suggestMode, keywordQuery])

  const applyQuickReplyRef = useRef<(item: QuickReply, mode?: 'slash' | 'keyword' | 'panel') => Promise<void>>(async () => {})

  const applyQuickReply = useCallback(async (item: QuickReply, mode: 'slash' | 'keyword' | 'panel' = 'panel') => {
    if (mode === 'slash') {
      // 只替换末尾 /输入码，保留前文
      setMessageInput(prev => prev.replace(/(^|[\s\n])\/([a-zA-Z0-9_-]*)$/, `$1${item.content}`))
    } else if (mode === 'keyword') {
      // 关键字触发：用话术替换当前输入（模板式回复）
      setMessageInput(item.content)
    } else {
      setMessageInput(item.content)
    }
    setQuickOpen(false)
    closeSuggest()
    try {
      await useQuickReply(item.id)
    } catch { /* 计数失败可忽略 */ }
    requestAnimationFrame(() => {
      const el = messageInputRef.current
      if (el) {
        el.focus()
        const len = el.value.length
        el.setSelectionRange(len, len)
      }
    })
  }, [closeSuggest])

  applyQuickReplyRef.current = applyQuickReply

  /** 解析输入：优先 / 输入码；否则关键字联想 */
  const syncSuggestFromInput = useCallback((value: string) => {
    // 末尾 /xxx（行首或空白后）
    const slashMatch = /(^|[\s\n])\/([a-zA-Z0-9_-]*)$/.exec(value)
    if (slashMatch) {
      if (keywordTimer.current) {
        window.clearTimeout(keywordTimer.current)
        keywordTimer.current = null
      }
      setSuggestMode('slash')
      setSuggestOpen(true)
      setSlashPrefix(slashMatch[2] || '')
      setKeywordQuery('')
      return
    }

    // 无 / 时：取最后一段非空白作关键字（至少 2 字）
    const trimmed = value.trim()
    if (!trimmed) {
      closeSuggest()
      return
    }
    // 取末行最后一词/整段
    const lastLine = trimmed.split(/\n/).pop() || trimmed
    const token = lastLine.trim()
    if (token.length < 2) {
      if (keywordTimer.current) {
        window.clearTimeout(keywordTimer.current)
        keywordTimer.current = null
      }
      closeSuggest()
      return
    }
    if (keywordTimer.current) window.clearTimeout(keywordTimer.current)
    keywordTimer.current = window.setTimeout(() => {
      setSuggestMode('keyword')
      setSuggestOpen(true)
      setKeywordQuery(token)
      setSlashPrefix('')
    }, 220)
  }, [closeSuggest])

  const selected = sessions.find(session => session.id === selectedId)
  const selectedCustomer = selected ? customers[selected.customer_id] : null
  const canOperate = Boolean(selected && (isManager || selected.agent_id === user?.user_id) && selected.status === 'active')

  // 进行中会话：在线时长每秒刷新；轨迹每 8s 静默拉一次（防 WS 漏推）
  useEffect(() => {
    if (!selected || selected.status === 'ended' || selected.status === 'archived') return
    const t = window.setInterval(() => setClockTick(n => n + 1), 1000)
    const poll = window.setInterval(() => {
      if (selectedIdRef.current != null) {
        void loadDetailRef.current(selectedIdRef.current, false, { silent: true })
      }
    }, 8000)
    return () => {
      clearInterval(t)
      clearInterval(poll)
    }
  }, [selected?.id, selected?.status])

  useEffect(() => {
    if (!selectedCustomer) {
      setCustomerHistory([])
      return
    }
    getSessions({ page: 1, pageSize: 50 }).then(res => {
      const list = Array.isArray(res.list) ? res.list : []
      setCustomerHistory(
        list
          .filter(s => s.customer_id === selectedCustomer.id && s.id !== selectedId)
          .slice(0, 5),
      )
    }).catch(() => setCustomerHistory([]))
  }, [selectedCustomer?.id, selectedId])

  // 拉取客户详情中的多条联系方式（输入识别沉淀）
  useEffect(() => {
    if (!selectedCustomer?.id) return
    let cancelled = false
    getCustomer(selectedCustomer.id)
      .then(res => {
        if (cancelled) return
        const data = res.data
        if (data?.customer) {
          setCustomers(prev => ({
            ...prev,
            [data.customer.id]: { ...prev[data.customer.id], ...data.customer },
          }))
        }
        if (Array.isArray(data?.contacts)) {
          setCustomerContacts(data.contacts)
        }
      })
      .catch(() => { /* 侧栏增强信息失败不影响会话 */ })
    return () => { cancelled = true }
  }, [selectedCustomer?.id, selectedId])

  const saveCustomerField = useCallback(async (customerId: number, field: CustomerEditableField, value: string) => {
    setSavingCustomerField(field)
    try {
      const res = await updateCustomer(customerId, { [field]: value })
      const updated = res.data
      setCustomers(prev => ({
        ...prev,
        [customerId]: { ...prev[customerId], ...updated },
      }))
    } catch (e) {
      antMsg.error(e instanceof Error ? e.message : '保存失败')
    } finally {
      setSavingCustomerField(null)
    }
  }, [])

  const filterActive = statusFilter !== 'all' || priorityFilter !== 'all'
  const filteredSessions = sessions
    .filter(session => {
      if (session.status === 'ended') return false
      if (statusFilter !== 'all' && session.status !== statusFilter) return false
      if (priorityFilter === 'urgent' && session.priority !== 'urgent') return false
      const customer = customers[session.customer_id]
      const keyword = search.trim().toLowerCase()
      return !keyword
        || customer?.name.toLowerCase().includes(keyword)
        || String(session.id).includes(keyword)
        || (session.last_message || '').toLowerCase().includes(keyword)
    })
    .slice()
    .sort((a, b) => {
      const rank = (s: Session) => (s.priority === 'urgent' ? 0 : s.status === 'waiting' ? 1 : 2)
      const r = rank(a) - rank(b)
      if (r !== 0) return r
      const ta = new Date(a.last_message_at || a.created_at).getTime()
      const tb = new Date(b.last_message_at || b.created_at).getTime()
      return tb - ta
    })

  const timelineEvents = (detail?.events || []).filter(event =>
    event.action === 'transfer'
    || event.action === 'assign'
    || event.action === 'auto_assign'
    || event.action === 'end'
    || event.action === 'offline_leave'
    || event.action === 'blacklist',
  )
  const notes = detail?.events
    .filter(event =>
      event.action === 'note'
      || event.action === 'offline_leave'
      || event.action === 'auto_assign'
      || event.action === 'assign'
      || event.action === 'transfer'
      || event.action === 'blacklist'
      || event.action === 'end',
    )
    .slice()
    .reverse() || []

  type ChatTimelineItem =
    | { kind: 'message'; at: number; message: Message }
    | { kind: 'event'; at: number; event: SessionEvent }

  const chatTimeline: ChatTimelineItem[] = (() => {
    const items: ChatTimelineItem[] = []
    for (const message of detail?.messages || []) {
      items.push({ kind: 'message', at: new Date(message.sent_at).getTime(), message })
    }
    for (const event of timelineEvents) {
      items.push({ kind: 'event', at: new Date(event.created_at).getTime(), event })
    }
    items.sort((a, b) => a.at - b.at || (a.kind === 'event' ? -1 : 1))
    return items
  })()

  const eventLabel = (action: string) => {
    switch (action) {
      case 'transfer': return '会话转接'
      case 'blacklist': return '加入黑名单'
      case 'assign': return '人工分配'
      case 'auto_assign': return '自动分配'
      case 'end': return '结束会话'
      case 'offline_leave': return '离线留言'
      default: return '系统记录'
    }
  }

  const emitTyping = () => {
    if (!selectedId || !canOperate) return
    const now = Date.now()
    if (now - lastTypingAt.current < 1200 || socketRef.current?.readyState !== WebSocket.OPEN) return
    lastTypingAt.current = now
    socketRef.current.send(JSON.stringify({ type: 'typing', session_id: selectedId }))
  }

  const sendMessage = async (content: string, type: 'text' | 'image' = 'text') => {
    if (!selectedId || !canOperate || sending || (type === 'text' && !content.trim())) return
    setSending(true)
    try {
      const response = await sendSessionMessage(selectedId, content, type)
      if (type === 'text') setMessageInput('')
      // 发送成功后优先用返回体追加并推进 seq，失败再静默全量
      if (response.data?.id) {
        appendPushedMessage(selectedId, response.data)
        void loadAll({ silent: true })
      } else {
        await syncAfterSeq(selectedId, { markRead: true })
        await loadAll({ silent: true })
      }
    } catch (error) {
      antMsg.error(error instanceof Error ? error.message : '发送失败')
    } finally {
      setSending(false)
    }
  }

  const handleImage = (file?: File) => {
    if (!file) return
    const okTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
    if (!okTypes.includes(file.type)) {
      antMsg.error('仅支持 jpg、png、gif、webp 图片')
      return
    }
    if (file.size > 10 * 1024 * 1024) {
      antMsg.error('图片不能超过 10 MB')
      return
    }
    if (pendingImage?.preview) URL.revokeObjectURL(pendingImage.preview)
    setPendingImage({ file, preview: URL.createObjectURL(file) })
  }

  const confirmSendImage = async () => {
    if (!pendingImage || !selectedId || !canOperate) return
    setSending(true)
    try {
      const uploaded = await uploadImage(pendingImage.file)
      const response = await sendSessionMessage(selectedId, uploaded.data.url, 'image')
      URL.revokeObjectURL(pendingImage.preview)
      setPendingImage(null)
      if (response.data?.id) {
        appendPushedMessage(selectedId, response.data)
        void loadAll({ silent: true })
      } else {
        await syncAfterSeq(selectedId, { markRead: true })
        await loadAll({ silent: true })
      }
    } catch (error) {
      antMsg.error(error instanceof Error ? error.message : '图片发送失败')
    } finally {
      setSending(false)
    }
  }

  const handleClaim = async (sessionID: number) => {
    try {
      await claimSession(sessionID)
      antMsg.success('已领取会话')
      setSelectedId(sessionID)
      await loadAll()
    } catch (error) {
      antMsg.error(error instanceof Error ? error.message : '领取失败')
    }
  }

  const openTransfer = async () => {
    if (!selected) return
    try {
      // 默认返回在线/忙碌坐席（含 status）
      const response = await getAvailableAgents()
      setAvailableAgents((response.data || []).filter(agent => agent.id !== user?.user_id))
      setTargetAgentID(undefined)
      setTransferOpen(true)
    } catch {
      antMsg.error('加载可转接坐席失败')
    }
  }

  const handleTransfer = async () => {
    if (!selected || !targetAgentID) return
    const target = availableAgents.find(a => a.id === targetAgentID)
    if (target && target.status !== 'online' && target.status !== 'busy') {
      antMsg.warning('只能转接给在线或忙碌的坐席')
      return
    }
    try {
      await transferSession(selected.id, targetAgentID)
      antMsg.success('会话已转接')
      setTransferOpen(false)
      setSelectedId(null)
      setDetail(null)
      await loadAll()
    } catch (error) {
      antMsg.error(error instanceof Error ? error.message : '转接失败')
    }
  }

  const openBlacklist = () => {
    if (!selected) return
    setBlacklistKind(selected.visitor_ip ? 'ip' : 'device')
    setBlacklistDuration('7d')
    setBlacklistReason('')
    setBlacklistEndSession(selected.status === 'active' || selected.status === 'waiting')
    setBlacklistOpen(true)
  }

  const handleBlacklist = async () => {
    if (!selected) return
    const reason = blacklistReason.trim()
    if (!reason) {
      antMsg.warning('请填写拉黑原因')
      return
    }
    if (blacklistKind === 'ip' && !selected.visitor_ip) {
      antMsg.warning('该会话无有效 IP，请改选「设备」')
      return
    }
    if (blacklistKind === 'device' && !selected.device_key && !selected.user_agent) {
      antMsg.warning('该会话无设备标识，请改选「IP」')
      return
    }
    setBlacklistSaving(true)
    try {
      await createBlacklist({
        session_id: selected.id,
        kind: blacklistKind,
        duration: blacklistDuration,
        reason,
        end_session: blacklistEndSession,
      })
      antMsg.success(blacklistKind === 'ip' ? '已拉黑该 IP' : '已拉黑该设备')
      setBlacklistOpen(false)
      if (blacklistEndSession) {
        setSelectedId(null)
        setDetail(null)
      } else if (selectedId) {
        await loadDetail(selectedId, false)
      }
      await loadAll()
    } catch (error) {
      antMsg.error(error instanceof Error ? error.message : '拉黑失败')
    } finally {
      setBlacklistSaving(false)
    }
  }

  const handlePriority = async (priority: 'normal' | 'urgent') => {
    if (!selected) return
    try {
      await updateSessionPriority(selected.id, priority)
      antMsg.success('优先级已更新')
      await loadAll()
    } catch (error) {
      antMsg.error(error instanceof Error ? error.message : '更新失败')
    }
  }

  const handleEnd = async () => {
    if (!selected) return
    try {
      await endSession(selected.id, endReason)
      antMsg.success('会话已结束')
      setEndingOpen(false)
      setSelectedId(null)
      setDetail(null)
      await loadAll()
    } catch (error) {
      antMsg.error(error instanceof Error ? error.message : '结束会话失败')
    }
  }

  const handleAddNote = async () => {
    if (!selected || !noteInput.trim()) return
    setSavingNote(true)
    try {
      await addSessionNote(selected.id, noteInput)
      setNoteInput('')
      await loadDetail(selected.id, false)
    } catch (error) {
      antMsg.error(error instanceof Error ? error.message : '保存备注失败')
    } finally {
      setSavingNote(false)
    }
  }

  const agentInitial = (user?.nickname || '客').slice(0, 1)

  if (loading) {
    return <div className="h-full flex items-center justify-center"><Spin size="large" /></div>
  }

  return (
    <div className="h-full flex overflow-hidden">
      <input
        ref={fileInputRef}
        type="file"
        accept="image/jpeg,image/png,image/gif,image/webp"
        className="hidden"
        onChange={event => { handleImage(event.target.files?.[0]); event.currentTarget.value = '' }}
      />

      {/* 会话列表面板 320px — 顶栏固定 56px，与中/右对齐 */}
      <section className="w-[320px] shrink-0 flex flex-col h-full border-r border-neutral-200 bg-white">
        <div
          className="shrink-0 px-3 flex items-center gap-2 border-b border-neutral-200"
          style={{ height: 'var(--header-height)' }}
        >
          <div className="flex items-center flex-1 min-w-0 rounded-lg px-2.5 h-8 bg-neutral-100 border border-neutral-200">
            <SearchOutlined className="text-neutral-400 text-xs mr-1.5 shrink-0" />
            <input
              className="bg-transparent border-none outline-none flex-1 min-w-0 text-sm text-neutral-900 placeholder:text-neutral-400"
              placeholder="搜索访客或会话"
              value={search}
              onChange={e => setSearch(e.target.value)}
            />
          </div>
          <span
            className="inline-flex items-center h-8 px-2 rounded-md text-xs font-medium bg-[#dbeafe] text-[#2563eb] whitespace-nowrap shrink-0"
            title={`当前会话 ${filteredSessions.length} 个`}
          >
            {filteredSessions.length}
          </span>
          <Popover
            open={filterOpen}
            onOpenChange={setFilterOpen}
            trigger="click"
            placement="bottomRight"
            content={(
              <div className="w-52 space-y-3">
                <div>
                  <div className="text-xs text-neutral-400 mb-1.5">会话状态</div>
                  <div className="flex flex-wrap gap-1.5">
                    {([
                      { key: 'all', label: '全部' },
                      { key: 'waiting', label: '等待中' },
                      { key: 'active', label: '进行中' },
                    ] as const).map(item => (
                      <button
                        key={item.key}
                        type="button"
                        onClick={() => setStatusFilter(item.key)}
                        className={`px-2 py-0.5 rounded-md text-xs border ${
                          statusFilter === item.key
                            ? 'bg-[#dbeafe] border-[#2563eb] text-[#2563eb]'
                            : 'bg-white border-neutral-200 text-neutral-600'
                        }`}
                      >
                        {item.label}
                      </button>
                    ))}
                  </div>
                </div>
                <div>
                  <div className="text-xs text-neutral-400 mb-1.5">优先级</div>
                  <div className="flex flex-wrap gap-1.5">
                    {([
                      { key: 'all', label: '全部' },
                      { key: 'urgent', label: '仅紧急' },
                    ] as const).map(item => (
                      <button
                        key={item.key}
                        type="button"
                        onClick={() => setPriorityFilter(item.key)}
                        className={`px-2 py-0.5 rounded-md text-xs border ${
                          priorityFilter === item.key
                            ? 'bg-[#dbeafe] border-[#2563eb] text-[#2563eb]'
                            : 'bg-white border-neutral-200 text-neutral-600'
                        }`}
                      >
                        {item.label}
                      </button>
                    ))}
                  </div>
                </div>
                {filterActive && (
                  <button
                    type="button"
                    className="text-xs text-neutral-500 hover:text-neutral-700"
                    onClick={() => { setStatusFilter('all'); setPriorityFilter('all') }}
                  >
                    清除筛选
                  </button>
                )}
              </div>
            )}
          >
            <button
              type="button"
              className={`w-8 h-8 rounded-lg border flex items-center justify-center shrink-0 relative ${
                filterActive
                  ? 'bg-[#dbeafe] border-[#2563eb] text-[#2563eb]'
                  : 'bg-neutral-100 border-neutral-200 text-neutral-500'
              }`}
              title="筛选"
            >
              <FilterOutlined className="text-xs" />
              {filterActive && <span className="absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full bg-[#2563eb]" />}
            </button>
          </Popover>
          <a
            href="/widget/preview"
            target="_blank"
            rel="noreferrer"
            className="w-8 h-8 rounded-lg border border-neutral-200 bg-neutral-100 text-[#2563eb] hover:bg-[#eff6ff] flex items-center justify-center shrink-0"
            title="预览访客窗口"
          >
            <ExportOutlined className="text-xs" />
          </a>
        </div>

        <div className="flex-1 overflow-y-auto no-scrollbar">
          {filteredSessions.length === 0 ? (
            <div className="text-center text-sm text-neutral-400 py-16">暂无待处理会话</div>
          ) : filteredSessions.map(session => {
            const customer = customers[session.customer_id]
            const name = customer?.name || `客户${session.customer_id}`
            const meta = listStatusMeta(session)
            const selected = selectedId === session.id
            return (
              <button
                key={session.id}
                type="button"
                onClick={() => setSelectedId(session.id)}
                className={`w-full flex items-stretch text-left border-b border-neutral-100 transition-colors ${
                  selected ? 'bg-[#eff6ff]' : 'hover:bg-neutral-50'
                }`}
              >
                <div className="w-[3px] shrink-0" style={{ background: selected || session.priority === 'urgent' || session.status === 'waiting' ? meta.bar : 'transparent' }} />
                <div className="flex-1 min-w-0 px-3 py-3">
                  <div className="flex items-center justify-between mb-1">
                    <div className="flex items-center gap-2 min-w-0">
                      <div
                        className="w-9 h-9 rounded-full flex items-center justify-center shrink-0 text-sm font-semibold"
                        style={{ background: meta.avatarBg, color: meta.avatarColor }}
                      >
                        {name.slice(0, 1)}
                      </div>
                      <span className="truncate font-medium text-sm text-neutral-800">{name}</span>
                    </div>
                    <div className="flex items-center gap-1.5 shrink-0 ml-2">
                      <span className="whitespace-nowrap text-xs text-neutral-400">
                        {relativeTime(session.last_message_at || session.created_at)}
                      </span>
                      {session.unread_count > 0 && (
                        <span className="min-w-[18px] h-[18px] px-[5px] rounded-full bg-[#dc2626] text-white text-[11px] font-semibold flex items-center justify-center">
                          {session.unread_count > 99 ? '99+' : session.unread_count}
                        </span>
                      )}
                    </div>
                  </div>
                  <div className="flex items-center justify-between pl-0">
                    <p className="truncate flex-1 min-w-0 mr-2 text-xs text-neutral-500">
                      {session.last_message || '暂无消息'}
                    </p>
                    <span className="flex items-center gap-1 shrink-0 text-xs text-neutral-500">
                      <span className="inline-block w-1.5 h-1.5 rounded-full" style={{ background: meta.dot }} />
                      {meta.label}
                    </span>
                  </div>
                  {session.status === 'waiting' && user?.role === 'agent' && (
                    <div className="mt-1.5 pl-11">
                      <span
                        role="button"
                        tabIndex={0}
                        className="text-xs text-[#2563eb] hover:underline"
                        onClick={e => { e.stopPropagation(); handleClaim(session.id) }}
                        onKeyDown={e => { if (e.key === 'Enter') { e.stopPropagation(); handleClaim(session.id) } }}
                      >
                        领取会话
                      </span>
                    </div>
                  )}
                </div>
              </button>
            )
          })}
        </div>
      </section>

      {/* 主聊天区 */}
      <section className="flex-1 flex flex-col min-w-0 h-full bg-neutral-50">
        {selected && selectedCustomer ? (
          <>
            <div
              className="shrink-0 flex items-center justify-between px-5 bg-white border-b border-neutral-200"
              style={{ height: 'var(--header-height)' }}
            >
              <div className="flex items-center gap-3 min-w-0">
                <div
                  className="w-9 h-9 rounded-full flex items-center justify-center shrink-0 text-sm font-semibold"
                  style={{
                    background: listStatusMeta(selected).avatarBg,
                    color: listStatusMeta(selected).avatarColor,
                  }}
                >
                  {selectedCustomer.name.slice(0, 1)}
                </div>
                <div className="min-w-0 leading-tight">
                  <div className="flex items-center gap-2">
                    <span className="truncate font-semibold text-[15px] text-neutral-900">{selectedCustomer.name}</span>
                    <span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-[#ecfeff] text-[#0891b2]">
                      {channelLabel(selectedCustomer.source)}
                    </span>
                    <span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs bg-[#f0fdf4] text-[#16a34a]">
                      <span className="inline-block w-1.5 h-1.5 rounded-full bg-[#16a34a]" />
                      {selectedCustomer.status === 'online' ? '在线' : selectedCustomer.status === 'busy' ? '忙碌' : '离线'}
                    </span>
                  </div>
                  {/* IP | 地区 — 压缩行高，保证三栏顶栏等高 */}
                  <div className="flex items-center gap-1.5 mt-0.5 text-[11px] text-neutral-400 min-w-0">
                    <span className="truncate">
                      IP: {selected.visitor_ip || '—'}
                    </span>
                    <span className="shrink-0 text-neutral-300">|</span>
                    <span className="truncate">
                      {selected.visitor_region || '未知'}
                    </span>
                    <span className="shrink-0 text-neutral-300">|</span>
                    <span className="tabular-nums shrink-0" title="访客在线时长（自进线起）">
                      在线 {formatOnlineDuration(
                        selected.created_at,
                        selected.status === 'ended' || selected.status === 'archived' ? selected.ended_at : null,
                        clockTick >= 0 ? Date.now() : Date.now(),
                      )}
                    </span>
                  </div>
                </div>
              </div>
              <div className="flex items-center gap-1.5 shrink-0 ml-4">
                <Dropdown
                  menu={{
                    items: [
                      { key: 'urgent', label: '标记紧急', onClick: () => handlePriority('urgent') },
                      { key: 'normal', label: '取消紧急', onClick: () => handlePriority('normal') },
                    ],
                  }}
                  disabled={!canOperate}
                >
                  <button type="button" className="w-8 h-8 rounded-lg flex items-center justify-center text-neutral-500 hover:bg-neutral-100 disabled:opacity-40" title="优先级" disabled={!canOperate}>
                    <FlagOutlined />
                  </button>
                </Dropdown>
                <button type="button" className="w-8 h-8 rounded-lg flex items-center justify-center text-neutral-500 hover:bg-neutral-100 disabled:opacity-40" title="快捷回复" disabled={!canOperate} onClick={() => setQuickOpen(true)}>
                  <ThunderboltOutlined />
                </button>
                <button type="button" className="w-8 h-8 rounded-lg flex items-center justify-center text-neutral-500 hover:bg-neutral-100 disabled:opacity-40" title="知识库" disabled={!canOperate} onClick={() => setKnowledgeOpen(true)}>
                  <BookOutlined />
                </button>
                <button type="button" className="w-8 h-8 rounded-lg flex items-center justify-center text-neutral-500 hover:bg-neutral-100 disabled:opacity-40" title="转接" disabled={!canOperate} onClick={openTransfer}>
                  <SwapOutlined />
                </button>
                <button
                  type="button"
                  className="w-8 h-8 rounded-lg flex items-center justify-center text-neutral-500 hover:bg-red-50 hover:text-red-600 disabled:opacity-40"
                  title="拉黑"
                  disabled={!selected}
                  onClick={openBlacklist}
                >
                  <StopOutlined />
                </button>
                <button type="button" className="w-8 h-8 rounded-lg flex items-center justify-center text-red-500 hover:bg-red-50 disabled:opacity-40" title="结束会话" disabled={!canOperate} onClick={() => setEndingOpen(true)}>
                  <CheckCircleOutlined />
                </button>
              </div>
            </div>

            <div className="flex-1 overflow-y-auto px-5 py-4 min-h-0">
              {detailLoading ? (
                <div className="h-full flex items-center justify-center"><Spin /></div>
              ) : (
                <>
                  <div className="flex justify-center mb-4">
                    <span className="inline-flex items-center px-3 py-1 rounded-lg text-xs text-neutral-400 bg-white border border-neutral-100">
                      会话开始 — {new Date(selected.created_at).toLocaleString('zh-CN', { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
                    </span>
                  </div>
                  {chatTimeline.map(item => {
                    if (item.kind === 'event') {
                      const { event } = item
                      return (
                        <div key={`ev-${event.id}`} className="flex justify-center mb-4">
                          <span className="inline-flex flex-col items-center max-w-[90%] px-3 py-1.5 rounded-lg text-xs text-violet-700 bg-violet-50 border border-violet-100">
                            <span className="font-medium text-violet-800">{eventLabel(event.action)}</span>
                            <span className="text-center leading-relaxed mt-0.5">{event.detail || eventLabel(event.action)}</span>
                            <span className="text-[11px] text-violet-500/80 mt-0.5">
                              {new Date(event.created_at).toLocaleString('zh-CN', {
                                month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit',
                              })}
                            </span>
                          </span>
                        </div>
                      )
                    }
                    const message = item.message
                    const isAgent = message.sender_type === 'agent'
                    const visitorInitial = selectedCustomer.name.slice(0, 1)
                    return (
                      <div
                        key={`msg-${message.id}`}
                        className={`flex items-start gap-2.5 mb-4 ${isAgent ? 'flex-row-reverse' : ''}`}
                      >
                        <div
                          className="w-9 h-9 rounded-full flex items-center justify-center shrink-0 text-sm font-semibold"
                          style={isAgent
                            ? { background: '#2563eb', color: '#fff' }
                            : { background: listStatusMeta(selected).avatarBg, color: listStatusMeta(selected).avatarColor }}
                        >
                          {isAgent ? agentInitial : visitorInitial}
                        </div>
                        <div className={`min-w-0 max-w-[65%] ${isAgent ? 'items-end' : ''}`}>
                          <div
                            className={`px-3.5 py-2.5 shadow-sm ${
                              isAgent
                                ? 'rounded-xl rounded-tr-sm bg-[#2563eb] text-white'
                                : 'rounded-xl rounded-tl-sm bg-white text-neutral-800 border border-neutral-100'
                            }`}
                          >
                            {message.type === 'image' ? (
                              <ChatImage src={message.content} alt="聊天图片" className="max-w-64 max-h-64" />
                            ) : (
                              <MarkdownBody
                                tone={isAgent ? 'inverse' : 'default'}
                                className="text-sm"
                              >
                                {message.content}
                              </MarkdownBody>
                            )}
                          </div>
                          <div className={`mt-1 text-xs text-neutral-400 ${isAgent ? 'text-right' : ''}`}>
                            {new Date(message.sent_at).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
                          </div>
                        </div>
                      </div>
                    )
                  })}
                  {chatTimeline.length === 0 && (
                    <div className="text-center text-sm text-neutral-400 py-10">暂无消息，开始对话吧</div>
                  )}
                  {visitorTyping && selectedCustomer && (
                    <div className="flex items-start gap-2.5 mb-2">
                      <div
                        className="w-9 h-9 rounded-full flex items-center justify-center shrink-0 text-sm font-semibold"
                        style={{
                          background: listStatusMeta(selected).avatarBg,
                          color: listStatusMeta(selected).avatarColor,
                        }}
                      >
                        {selectedCustomer.name.slice(0, 1)}
                      </div>
                      <div>
                        <div className="px-3.5 py-2.5 rounded-xl rounded-tl-sm bg-white border border-neutral-100 inline-flex items-center gap-1">
                          <span className="typing-dot" style={{ animationDelay: '0s' }} />
                          <span className="typing-dot" style={{ animationDelay: '0.2s' }} />
                          <span className="typing-dot" style={{ animationDelay: '0.4s' }} />
                        </div>
                        <div className="mt-1 text-xs text-neutral-400">访客正在输入…</div>
                      </div>
                    </div>
                  )}
                  <div ref={chatEndRef} />
                </>
              )}
            </div>

            <div className="shrink-0 bg-white border-t border-neutral-200">
              {/* 访客实时输入草稿：展示在工具栏/输入框上方 */}
              {(visitorDraft.trim() || visitorTyping) && (
                <div className="mx-4 mt-2 mb-0 rounded-md border border-amber-200/80 bg-amber-50 px-3 py-2">
                  <div className="flex items-start gap-1.5 min-w-0">
                    <span className="shrink-0 text-[12px] font-medium text-amber-700 leading-5">
                      对方正在输入
                      {visitorTyping && (
                        <span className="inline-flex items-center gap-0.5 ml-1 align-middle">
                          <span className="typing-dot" style={{ animationDelay: '0s', width: 3, height: 3 }} />
                          <span className="typing-dot" style={{ animationDelay: '0.2s', width: 3, height: 3 }} />
                          <span className="typing-dot" style={{ animationDelay: '0.4s', width: 3, height: 3 }} />
                        </span>
                      )}
                      {visitorDraft.trim() ? '：' : ''}
                    </span>
                    {visitorDraft.trim() ? (
                      <span className="min-w-0 flex-1 text-[13px] text-neutral-800 whitespace-pre-wrap break-words max-h-20 overflow-y-auto leading-5">
                        {visitorDraft.trim()}
                      </span>
                    ) : (
                      <span className="text-[12px] text-amber-600/70 leading-5">…</span>
                    )}
                  </div>
                </div>
              )}
              {selected.status !== 'active' || !canOperate ? (
                <div className="text-center text-sm text-neutral-400 py-4">
                  {selected.status === 'waiting' ? '领取会话后即可回复' : '会话已结束，无法继续发送消息'}
                </div>
              ) : (
                <>
                  <div className="flex items-center gap-1 px-4 pt-2.5 pb-1">
                    <EmojiPicker onSelect={insertEmoji} disabled={!canOperate} />
                    <button type="button" className="w-8 h-8 rounded-md flex items-center justify-center text-neutral-500 hover:bg-neutral-100" title="图片" onClick={() => fileInputRef.current?.click()}>
                      <PictureOutlined />
                    </button>
                    <button type="button" className="w-8 h-8 rounded-md flex items-center justify-center text-neutral-500 hover:bg-neutral-100" title="附件" onClick={() => fileInputRef.current?.click()}>
                      <PaperClipOutlined />
                    </button>
                    <div className="w-px h-5 bg-neutral-200 mx-1" />
                    <button
                      type="button"
                      className="h-8 px-2 rounded-md text-xs text-neutral-500 hover:bg-neutral-100 flex items-center gap-1"
                      onClick={() => setQuickOpen(true)}
                    >
                      <ThunderboltOutlined />
                      快捷回复
                    </button>
                    <button type="button" className="h-8 px-2 rounded-md text-xs text-neutral-500 hover:bg-neutral-100 flex items-center gap-1" onClick={() => setKnowledgeOpen(true)}>
                      <FileTextOutlined />
                      知识库
                    </button>
                    <span className="text-[11px] text-neutral-400 ml-1">输入 / 调用话术</span>
                  </div>
                  <div className="relative flex items-end gap-2.5 px-4 pb-3 pt-1">
                    {suggestOpen && (suggestMode === 'slash' || keywordQuery.trim().length >= 2) && (
                      <div className="absolute bottom-full left-4 right-16 mb-1 z-20 max-h-56 overflow-auto rounded-lg border border-neutral-200 bg-white shadow-lg">
                        <div className="px-3 py-1.5 text-[11px] text-neutral-400 border-b border-neutral-100 flex items-center justify-between">
                          <span>
                            {suggestMode === 'slash'
                              ? (slashPrefix ? `输入码 /${slashPrefix}` : '常用话术（按你的调用频率）')
                              : `关键字「${keywordQuery}」`}
                          </span>
                          <span>↑↓ 选择 · Enter 填入 · Esc 关闭</span>
                        </div>
                        {suggestItems.length === 0 ? (
                          <div className="px-3 py-2 text-xs text-neutral-400">无匹配话术</div>
                        ) : (
                          suggestItems.map((item, idx) => (
                            <button
                              key={item.id}
                              type="button"
                              className={`w-full text-left px-3 py-2 border-0 cursor-pointer ${
                                idx === suggestIndex ? 'bg-blue-50' : 'bg-white hover:bg-neutral-50'
                              }`}
                              onMouseDown={e => {
                                e.preventDefault()
                                void applyQuickReply(item, suggestMode)
                              }}
                            >
                              <div className="flex items-center gap-2 min-w-0">
                                <span className="text-sm font-medium text-neutral-800 truncate">{item.title}</span>
                                {item.shortcut && (
                                  <code className="text-[11px] text-blue-600 bg-blue-50 px-1 rounded shrink-0">/{item.shortcut}</code>
                                )}
                                <span className="text-[10px] text-neutral-400 shrink-0">
                                  {item.scope === 'team' ? '团队' : '个人'}
                                </span>
                                {(item.my_usage_count || 0) > 0 && (
                                  <span className="text-[10px] text-neutral-400 shrink-0 ml-auto">用过 {item.my_usage_count} 次</span>
                                )}
                              </div>
                              <div className="text-xs text-neutral-500 line-clamp-1 mt-0.5">{item.content}</div>
                            </button>
                          ))
                        )}
                      </div>
                    )}
                    <textarea
                      ref={messageInputRef}
                      rows={3}
                      className="flex-1 min-w-0 min-h-[88px] max-h-[160px] rounded-xl px-3.5 py-3 bg-neutral-50 border border-neutral-200 text-sm leading-6 text-neutral-800 placeholder:text-neutral-400 outline-none resize-y focus:border-[#2563eb] transition-colors"
                      placeholder="输入文字联想话术，/ 调输入码… Enter 发送"
                      value={messageInput}
                      onChange={event => {
                        const v = event.target.value
                        setMessageInput(v)
                        syncSuggestFromInput(v)
                        emitTyping()
                      }}
                      onPaste={event => {
                        const image = Array.from(event.clipboardData.items).find(item => item.type.startsWith('image/'))
                        if (image) {
                          event.preventDefault()
                          handleImage(image.getAsFile() || undefined)
                        }
                      }}
                      onKeyDown={event => {
                        if (suggestOpen && suggestItems.length > 0) {
                          if (event.key === 'ArrowDown') {
                            event.preventDefault()
                            setSuggestIndex(i => (i + 1) % suggestItems.length)
                            return
                          }
                          if (event.key === 'ArrowUp') {
                            event.preventDefault()
                            setSuggestIndex(i => (i - 1 + suggestItems.length) % suggestItems.length)
                            return
                          }
                          if (event.key === 'Enter' && !event.shiftKey) {
                            event.preventDefault()
                            void applyQuickReply(suggestItems[suggestIndex] || suggestItems[0], suggestMode)
                            return
                          }
                          if (event.key === 'Escape') {
                            event.preventDefault()
                            closeSuggest()
                            return
                          }
                          if (event.key === 'Tab') {
                            event.preventDefault()
                            void applyQuickReply(suggestItems[suggestIndex] || suggestItems[0], suggestMode)
                            return
                          }
                        }
                        if (event.key === 'Enter' && !event.shiftKey) {
                          event.preventDefault()
                          sendMessage(messageInput)
                        }
                      }}
                    />
                    <button
                      type="button"
                      disabled={!messageInput.trim() || sending}
                      onClick={() => sendMessage(messageInput)}
                      className="w-11 h-11 rounded-xl bg-[#2563eb] hover:bg-[#1d4ed8] disabled:opacity-40 text-white flex items-center justify-center shrink-0 shadow-sm"
                    >
                      {sending ? <Spin size="small" /> : <SendOutlined className="text-base" />}
                    </button>
                  </div>
                </>
              )}
            </div>
          </>
        ) : (
          <div className="flex-1 flex flex-col items-center justify-center text-neutral-400 gap-2">
            <div className="w-16 h-16 rounded-2xl bg-white border border-neutral-200 flex items-center justify-center text-2xl text-neutral-300">
              <SearchOutlined />
            </div>
            <div className="text-sm">选择一个会话开始处理</div>
          </div>
        )}
      </section>

      {/* 右侧：上客户信息 + 下快捷回复 */}
      <aside className="w-[320px] shrink-0 flex flex-col h-full border-l border-neutral-200 bg-white min-h-0">
        {selectedCustomer ? (
          <>
            {/* —— 上：客户信息（精简） —— */}
            <div
              className="shrink-0 flex items-center justify-between px-4 border-b border-neutral-200"
              style={{ height: 'var(--header-height)' }}
            >
              <span className="font-semibold text-base text-neutral-800">客户信息</span>
            </div>
            <div className="shrink-0 max-h-[42%] overflow-y-auto px-4 py-3 no-scrollbar border-b border-neutral-100">
              <div className="flex items-center gap-3 mb-3">
                <div
                  className="w-10 h-10 rounded-full flex items-center justify-center text-[16px] font-semibold shrink-0"
                  style={{
                    background: selected ? listStatusMeta(selected).avatarBg : '#eff6ff',
                    color: selected ? listStatusMeta(selected).avatarColor : '#2563eb',
                  }}
                >
                  {selectedCustomer.name.slice(0, 1)}
                </div>
                <div className="min-w-0">
                  <div className="font-semibold text-[15px] text-neutral-900 truncate">{selectedCustomer.name}</div>
                  <div className="flex items-center gap-1 mt-0.5 text-[11px] text-neutral-400">
                    <span
                      className="inline-block w-1.5 h-1.5 rounded-full"
                      style={{ background: selectedCustomer.status === 'online' ? '#16a34a' : '#94a3b8' }}
                    />
                    {selectedCustomer.status === 'online' ? '在线' : selectedCustomer.status === 'busy' ? '忙碌' : '离线'}
                    {selectedCustomer.source ? (
                      <span className="text-neutral-300">· {selectedCustomer.source}</span>
                    ) : null}
                  </div>
                </div>
              </div>

              <div className="flex flex-col gap-1 text-sm">
                <CustomerInfoField
                  label="手机"
                  value={selectedCustomer.phone || ''}
                  placeholder="点击填写"
                  saving={savingCustomerField === 'phone'}
                  onSave={v => saveCustomerField(selectedCustomer.id, 'phone', v)}
                />
                <CustomerInfoField
                  label="邮箱"
                  value={selectedCustomer.email || ''}
                  placeholder="点击填写"
                  saving={savingCustomerField === 'email'}
                  onSave={v => saveCustomerField(selectedCustomer.id, 'email', v)}
                />
                <CustomerInfoField
                  label="微信"
                  value={selectedCustomer.wechat || ''}
                  placeholder="点击填写"
                  saving={savingCustomerField === 'wechat'}
                  onSave={v => saveCustomerField(selectedCustomer.id, 'wechat', v)}
                />
                <CustomerInfoField
                  label="QQ"
                  value={selectedCustomer.qq || ''}
                  placeholder="点击填写"
                  saving={savingCustomerField === 'qq'}
                  onSave={v => saveCustomerField(selectedCustomer.id, 'qq', v)}
                />
                {(() => {
                  const kindLabel = (k: string) =>
                    k === 'phone' ? '手机' : k === 'wechat' ? '微信' : k === 'email' ? '邮箱' : k === 'qq' ? 'QQ' : k
                  const extra = customerContacts.filter(c => {
                    if (c.kind === 'phone' && selectedCustomer.phone && c.value === selectedCustomer.phone) return false
                    if (c.kind === 'email' && selectedCustomer.email && c.value === selectedCustomer.email) return false
                    if (c.kind === 'wechat' && selectedCustomer.wechat && c.value === selectedCustomer.wechat) return false
                    if (c.kind === 'qq' && selectedCustomer.qq && c.value === selectedCustomer.qq) return false
                    return true
                  })
                  if (extra.length === 0) return null
                  return (
                    <div className="flex items-start gap-2 pt-0.5">
                      <span className="shrink-0 text-neutral-400 min-w-12 leading-5 text-sm">更多</span>
                      <div className="flex flex-col gap-0.5 min-w-0">
                        {extra.map(c => (
                          <span key={c.id || `${c.kind}-${c.value}`} className="text-neutral-800 text-xs break-all">
                            <span className="text-neutral-400 mr-1">{kindLabel(c.kind)}</span>
                            {c.value}
                          </span>
                        ))}
                      </div>
                    </div>
                  )
                })()}
              </div>

              <div className="mt-3">
                <div className="mb-1.5 text-[11px] font-semibold text-neutral-500">标签</div>
                <div className="flex flex-wrap gap-1">
                  {parseTags(selectedCustomer.tags).length === 0 ? (
                    <span className="text-xs text-neutral-400">暂无</span>
                  ) : parseTags(selectedCustomer.tags).map(tag => (
                    <span key={tag} className="text-[11px] px-1.5 py-0.5 rounded-md bg-[#dbeafe] text-[#2563eb] font-medium">
                      {tag}
                    </span>
                  ))}
                </div>
              </div>

              <div className="mt-4 mb-3">
                <div className="mb-1.5 text-[11px] font-semibold text-neutral-500">浏览轨迹</div>
                {!detail?.pageViews?.length ? (
                  <div className="text-xs text-neutral-400">暂无页面记录（需嵌入 widget 并打开会话）</div>
                ) : (
                  <div className="flex flex-col gap-0 max-h-36 overflow-y-auto rounded-lg border border-neutral-100">
                    {detail.pageViews.map((pv, idx) => (
                      <div
                        key={pv.id || `${pv.url}-${pv.entered_at}`}
                        className={`px-2.5 py-2 text-xs ${idx > 0 ? 'border-t border-neutral-50' : ''} ${
                          idx === 0 ? 'bg-blue-50/50' : 'bg-white'
                        }`}
                      >
                        <div className="flex items-center justify-between gap-2 mb-0.5">
                          <span className="text-neutral-400 tabular-nums shrink-0">
                            {new Date(pv.entered_at).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
                          </span>
                          {idx === 0 && (
                            <span className="text-[10px] text-[#2563eb] font-medium">当前</span>
                          )}
                        </div>
                        <a
                          href={pv.url}
                          target="_blank"
                          rel="noreferrer"
                          className="text-neutral-800 hover:text-[#2563eb] break-all leading-snug"
                          title={pv.url}
                        >
                          {pv.title || shortPagePath(pv.url)}
                        </a>
                      </div>
                    ))}
                  </div>
                )}
              </div>

              <div className="mb-3">
                <div className="mb-1.5 text-[11px] font-semibold text-neutral-500">历史会话</div>
                <div className="flex flex-col gap-1.5">
                  {customerHistory.length === 0 ? (
                    <div className="text-xs text-neutral-400">暂无其他会话</div>
                  ) : customerHistory.map(s => (
                    <button
                      key={s.id}
                      type="button"
                      onClick={() => setSelectedId(s.id)}
                      className="rounded-lg px-2.5 py-2 text-left bg-neutral-50 border border-neutral-100 hover:border-blue-200"
                    >
                      <div className="flex items-center justify-between mb-0.5">
                        <span className="truncate text-sm font-medium text-neutral-700">会话 #{s.id}</span>
                        <span className="whitespace-nowrap text-xs text-neutral-400">
                          {new Date(s.created_at).toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })}
                        </span>
                      </div>
                      <p className="line-clamp-1 text-xs text-neutral-400 m-0">
                        {s.last_message || (s.status === 'ended' ? '已结束' : '进行中')}
                      </p>
                    </button>
                  ))}
                </div>
              </div>

              <div className="mb-1">
                <div className="mb-1.5 text-[11px] font-semibold text-neutral-500">内部备注</div>
                <div className="space-y-1.5 max-h-32 overflow-auto">
                  {notes.length === 0 ? (
                    <div className="text-xs text-neutral-400">暂无内部备注</div>
                  ) : notes.map(note => {
                    const isLeave = note.action === 'offline_leave'
                    const isAuto = note.action === 'auto_assign'
                    const isTransfer = note.action === 'transfer'
                    const isAssign = note.action === 'assign'
                    const isEnd = note.action === 'end'
                    const box = isLeave
                      ? 'bg-orange-50 text-orange-900 border-orange-100'
                      : isTransfer
                        ? 'bg-violet-50 text-violet-900 border-violet-100'
                        : (isAuto || isAssign)
                          ? 'bg-blue-50 text-blue-900 border-blue-100'
                          : isEnd
                            ? 'bg-neutral-100 text-neutral-700 border-neutral-200'
                            : 'bg-amber-50 text-amber-900 border-amber-100'
                    const timeCls = isLeave
                      ? 'text-orange-600/70'
                      : isTransfer
                        ? 'text-violet-600/70'
                        : (isAuto || isAssign)
                          ? 'text-blue-600/70'
                          : isEnd
                            ? 'text-neutral-500'
                            : 'text-amber-600/70'
                    const title = isLeave
                      ? '离线留言'
                      : isTransfer
                        ? '会话转接'
                        : isAuto
                          ? '自动分配'
                          : isAssign
                            ? '人工分配'
                            : isEnd
                              ? '结束会话'
                              : null
                    return (
                      <div key={note.id} className={`rounded-lg p-2 text-xs whitespace-pre-wrap border ${box}`}>
                        {title && <div className="font-medium mb-0.5">{title}</div>}
                        {note.detail}
                        <div className={`mt-1 ${timeCls}`}>{new Date(note.created_at).toLocaleString('zh-CN')}</div>
                      </div>
                    )
                  })}
                </div>
                {canOperate && (
                  <div className="mt-2 flex gap-1">
                    <Input
                      size="small"
                      maxLength={500}
                      placeholder="添加仅客服可见的备注"
                      value={noteInput}
                      onChange={e => setNoteInput(e.target.value)}
                      onPressEnter={handleAddNote}
                    />
                    <Button size="small" loading={savingNote} onClick={handleAddNote}>保存</Button>
                  </div>
                )}
              </div>
            </div>

            {/* —— 下：快捷回复（团队 / 个人） —— */}
            <div className="flex-1 min-h-0 flex flex-col">
              <div className="shrink-0 px-3 pt-2.5 pb-2 border-b border-neutral-100">
                <div className="flex items-center justify-between gap-2 mb-2">
                  <span className="text-sm font-semibold text-neutral-800">快捷回复</span>
                  <div className="flex items-center gap-0.5 p-0.5 rounded-md bg-neutral-100">
                    {([
                      { key: 'team' as const, label: '团队' },
                      { key: 'personal' as const, label: '个人' },
                    ]).map(tab => (
                      <button
                        key={tab.key}
                        type="button"
                        onClick={() => setSidebarQuickScope(tab.key)}
                        className={`h-6 px-2 rounded text-xs border-0 cursor-pointer transition-colors ${
                          sidebarQuickScope === tab.key
                            ? 'bg-white text-[#2563eb] font-medium shadow-sm'
                            : 'bg-transparent text-neutral-500 hover:text-neutral-700'
                        }`}
                      >
                        {tab.label}
                      </button>
                    ))}
                  </div>
                </div>
                <Input
                  size="small"
                  allowClear
                  prefix={<SearchOutlined className="text-neutral-400" />}
                  placeholder="搜索标题、内容或输入码"
                  value={sidebarQuickKeyword}
                  onChange={e => setSidebarQuickKeyword(e.target.value)}
                />
              </div>
              <div className="flex-1 min-h-0 overflow-y-auto px-2 py-2 no-scrollbar">
                {sidebarQuickLoading ? (
                  <div className="py-8 text-center"><Spin size="small" /></div>
                ) : sidebarQuickList.length === 0 ? (
                  <div className="py-8 text-center text-xs text-neutral-400">
                    {sidebarQuickScope === 'team' ? '暂无已发布的团队话术' : '暂无个人快捷回复'}
                  </div>
                ) : (
                  <div className="flex flex-col gap-1">
                    {sidebarQuickList.map(item => (
                      <button
                        key={item.id}
                        type="button"
                        disabled={!canOperate}
                        title={canOperate ? '点击填入输入框' : '领取会话后可用'}
                        onClick={() => void applyQuickReply(item, 'panel')}
                        className="w-full text-left rounded-lg px-2.5 py-2 border-0 bg-neutral-50 hover:bg-blue-50 hover:ring-1 hover:ring-blue-200 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
                      >
                        <div className="flex items-center gap-1.5 min-w-0">
                          <span className="text-[13px] font-medium text-neutral-800 truncate">{item.title}</span>
                          {item.shortcut && (
                            <code className="text-[10px] text-blue-600 bg-blue-50 px-1 rounded shrink-0">/{item.shortcut}</code>
                          )}
                        </div>
                        <div className="text-[11px] text-neutral-500 line-clamp-2 mt-0.5 leading-snug">{item.content}</div>
                      </button>
                    ))}
                  </div>
                )}
              </div>
            </div>
          </>
        ) : (
          <div className="flex-1 flex items-center justify-center text-sm text-neutral-400">选择会话查看客户信息</div>
        )}
      </aside>

      <Modal
        title="转接会话"
        open={transferOpen}
        onCancel={() => setTransferOpen(false)}
        onOk={handleTransfer}
        okButtonProps={{
          disabled: !targetAgentID || (() => {
            const t = availableAgents.find(a => a.id === targetAgentID)
            return Boolean(t && t.status !== 'online' && t.status !== 'busy')
          })(),
        }}
      >
        <p className="text-sm text-neutral-500 mb-3">请选择接手坐席（绿色在线 / 橙色忙碌）。</p>
        {availableAgents.length === 0 ? (
          <div className="text-sm text-neutral-400 py-6 text-center border border-dashed border-neutral-200 rounded-lg">
            当前没有其他可转接坐席
          </div>
        ) : (
          <Select
            className="w-full"
            placeholder="选择坐席"
            value={targetAgentID}
            onChange={setTargetAgentID}
            optionLabelProp="label"
            options={availableAgents.map(agent => {
              const st = agentStatusMeta(agent.status)
              return {
                value: agent.id,
                // 选中后输入框展示：昵称 + 状态
                label: `${agent.nickname}（${st.text}）`,
                disabled: agent.status !== 'online' && agent.status !== 'busy',
              }
            })}
            optionRender={option => {
              const agent = availableAgents.find(a => a.id === option.value)
              const st = agentStatusMeta(agent?.status || 'offline')
              return (
                <div className="flex items-center gap-2 py-0.5 min-w-0">
                  <span
                    className="inline-block w-2 h-2 rounded-full shrink-0"
                    style={{ background: st.dot }}
                    title={st.text}
                  />
                  <span className="truncate text-neutral-800 flex-1 min-w-0">
                    {agent?.nickname || option.label}
                  </span>
                  <span
                    className="shrink-0 text-[11px] px-1.5 py-0 rounded font-medium"
                    style={{ background: st.bg, color: st.color }}
                  >
                    {st.text}
                  </span>
                </div>
              )
            }}
          />
        )}
      </Modal>
      <Modal title="结束会话" open={endingOpen} onCancel={() => setEndingOpen(false)} onOk={handleEnd} okText="确认结束">
        <p className="text-sm text-neutral-500 mb-3">请选择本次会话的结束原因。</p>
        <Select className="w-full" value={endReason} onChange={setEndReason} options={endReasons} />
      </Modal>
      <Modal
        title="拉黑访客"
        open={blacklistOpen}
        onCancel={() => setBlacklistOpen(false)}
        onOk={handleBlacklist}
        okText="确认拉黑"
        okButtonProps={{ danger: true, loading: blacklistSaving, disabled: !blacklistReason.trim() }}
        cancelButtonProps={{ disabled: blacklistSaving }}
        destroyOnClose
      >
        <div className="flex flex-col gap-3.5 pt-1">
          <div>
            <div className="text-sm text-neutral-600 mb-2">拉黑类型</div>
            <Radio.Group
              value={blacklistKind}
              onChange={e => setBlacklistKind(e.target.value)}
              optionType="button"
              buttonStyle="solid"
              options={[
                {
                  value: 'ip',
                  label: selected?.visitor_ip ? `IP（${selected.visitor_ip}）` : 'IP（无）',
                  disabled: !selected?.visitor_ip,
                },
                {
                  value: 'device',
                  label: selected?.device_key || selected?.user_agent
                    ? '设备'
                    : '设备（无）',
                  disabled: !selected?.device_key && !selected?.user_agent,
                },
              ]}
            />
            {blacklistKind === 'device' && (
              <div className="mt-1.5 text-xs text-neutral-400 truncate" title={selected?.device_key || selected?.user_agent}>
                {selected?.device_key
                  ? `设备标识：${selected.device_key.slice(0, 12)}…`
                  : selected?.user_agent
                    ? `将按浏览器指纹：${selected.user_agent.slice(0, 48)}…`
                    : ''}
              </div>
            )}
          </div>
          <div>
            <div className="text-sm text-neutral-600 mb-2">释放时间</div>
            <Select
              className="w-full"
              value={blacklistDuration}
              onChange={v => setBlacklistDuration(v)}
              options={[
                { value: '1h', label: '1 小时后自动解除' },
                { value: '6h', label: '6 小时后自动解除' },
                { value: '1d', label: '1 天后自动解除' },
                { value: '7d', label: '7 天后自动解除' },
                { value: '30d', label: '30 天后自动解除' },
                { value: 'permanent', label: '长期（不自动解除）' },
              ]}
            />
          </div>
          <div>
            <div className="text-sm text-neutral-600 mb-2">拉黑原因 <span className="text-red-500">*</span></div>
            <Input.TextArea
              value={blacklistReason}
              onChange={e => setBlacklistReason(e.target.value)}
              placeholder="例如：恶意骚扰、发送垃圾信息…"
              maxLength={200}
              showCount
              rows={3}
            />
          </div>
          <Checkbox
            checked={blacklistEndSession}
            onChange={e => setBlacklistEndSession(e.target.checked)}
            disabled={selected?.status === 'ended' || selected?.status === 'archived'}
          >
            拉黑后同时结束当前会话
          </Checkbox>
          <div className="text-xs text-amber-700 bg-amber-50 border border-amber-100 rounded-md px-2.5 py-2 leading-relaxed">
            拉黑后，该{blacklistKind === 'ip' ? ' IP ' : '设备'}再次访问在线客服将被拦截，直至到期或手动解除。
          </div>
        </div>
      </Modal>
      <Modal
        title="图片预览"
        open={Boolean(pendingImage)}
        onCancel={() => {
          if (pendingImage?.preview) URL.revokeObjectURL(pendingImage.preview)
          setPendingImage(null)
        }}
        onOk={confirmSendImage}
        okText="上传并发送"
        okButtonProps={{ loading: sending }}
      >
        <div className="flex justify-center">
          <img src={pendingImage?.preview || ''} alt="待发送图片预览" className="max-h-[420px] max-w-full rounded-lg" />
        </div>
        <p className="text-xs text-neutral-400 text-center mt-3 mb-0">将自动压缩并转为 WebP 后上传</p>
      </Modal>
      <Modal title="知识库" open={knowledgeOpen} onCancel={() => setKnowledgeOpen(false)} footer={null} width={640}>
        <Input
          prefix={<SearchOutlined />}
          placeholder="搜索标题或内容"
          value={knowledgeKeyword}
          onChange={event => setKnowledgeKeyword(event.target.value)}
          allowClear
          className="mb-3"
        />
        {knowledgeLoading ? (
          <div className="py-10 text-center"><Spin /></div>
        ) : (
          <div className="space-y-2 max-h-96 overflow-auto">
            {knowledgeEntries.length === 0 ? (
              <div className="text-center text-neutral-400 py-8">未找到可用知识条目</div>
            ) : knowledgeEntries.map(entry => (
              <button
                key={entry.id}
                type="button"
                className="w-full text-left border border-neutral-100 rounded-lg p-3 hover:border-blue-300 hover:bg-blue-50"
                onClick={() => { setMessageInput(entry.content); setKnowledgeOpen(false) }}
              >
                <div className="text-sm font-medium text-neutral-800">{entry.title}</div>
                <div className="text-xs text-neutral-500 mt-1 line-clamp-2">{entry.content}</div>
              </button>
            ))}
          </div>
        )}
      </Modal>
      <Modal title="快捷回复" open={quickOpen} onCancel={() => setQuickOpen(false)} footer={null} width={640}>
        <Input
          prefix={<SearchOutlined />}
          placeholder="搜索标题、内容或输入码"
          value={quickKeyword}
          onChange={event => setQuickKeyword(event.target.value)}
          allowClear
          className="mb-3"
        />
        <p className="text-xs text-neutral-400 mb-2">含团队已发布 + 我的话术。管理请到侧栏「快捷回复」。</p>
        {quickLoading ? (
          <div className="py-10 text-center"><Spin /></div>
        ) : (
          <div className="space-y-2 max-h-96 overflow-auto">
            {quickList.length === 0 ? (
              <div className="text-center text-neutral-400 py-8">暂无快捷回复</div>
            ) : quickList.map(item => (
              <button
                key={item.id}
                type="button"
                className="w-full text-left border border-neutral-100 rounded-lg p-3 hover:border-blue-300 hover:bg-blue-50"
                onClick={() => void applyQuickReply(item)}
              >
                <div className="flex items-center gap-2">
                  <span className="text-sm font-medium text-neutral-800">{item.title}</span>
                  {item.shortcut && (
                    <code className="text-[11px] text-blue-600 bg-blue-50 px-1 rounded">/{item.shortcut}</code>
                  )}
                  <span className="text-[10px] text-neutral-400 ml-auto">
                    {item.scope === 'team' ? '团队' : '个人'}
                  </span>
                </div>
                <div className="text-xs text-neutral-500 mt-1 line-clamp-2">{item.content}</div>
              </button>
            ))}
          </div>
        )}
      </Modal>
    </div>
  )
}

export default Dashboard
