import { useState, useEffect, useRef, useCallback, useMemo } from 'react'
import {
  CloseOutlined, MessageOutlined, SendOutlined,
  StarFilled, MinusOutlined, PictureOutlined, CustomerServiceOutlined,
} from '@ant-design/icons'
import EmojiPicker, { insertAtCursor } from '@/components/common/EmojiPicker'
import { ChatImage } from '@/components/common/ImagePreview'
import MarkdownBody from '@/components/common/MarkdownBody'
import SafeHtml, { looksLikeHtml } from '@/components/common/SafeHtml'

interface Message {
  id: number
  sender: 'visitor' | 'agent'
  content: string
  time: string
  type?: 'text' | 'image'
  /** 服务端单调序号，用于增量同步 */
  seq?: number
}

const defaultQuickQuestions = ['查询订单状态', '退换货政策', '配送时效说明']

type WelcomeSegment = { type: 'text' | 'image'; content: string }

const defaultWelcomeSegments: WelcomeSegment[] = [
  { type: 'text', content: '您好！欢迎咨询，请问有什么可以帮您？' },
]

function parseWelcomeSegments(data: {
  welcome_messages?: unknown
  welcome_message?: string
}): WelcomeSegment[] {
  if (Array.isArray(data.welcome_messages) && data.welcome_messages.length > 0) {
    const segs: WelcomeSegment[] = []
    for (const raw of data.welcome_messages) {
      if (!raw || typeof raw !== 'object') continue
      const item = raw as { type?: string; content?: string }
      const content = typeof item.content === 'string' ? item.content.trim() : ''
      if (!content) continue
      if (item.type === 'image') segs.push({ type: 'image', content })
      else segs.push({ type: 'text', content })
    }
    if (segs.length > 0) return segs
  }
  if (typeof data.welcome_message === 'string' && data.welcome_message.trim()) {
    return [{ type: 'text', content: data.welcome_message.trim() }]
  }
  return defaultWelcomeSegments
}

type LayoutMode = 'floating' | 'fill'

export type HostPageInfo = { url?: string; title?: string; referrer?: string }

interface VisitorChatProps {
  defaultOpen?: boolean
  channelKey?: string
  /** 是否在 iframe 嵌入模式（关闭/最小化会 postMessage 给宿主） */
  embedded?: boolean
  layout?: LayoutMode
  /** 宿主页初始 URL（由 widget.js 经 query 传入） */
  initialPage?: HostPageInfo
}

const VisitorChat = ({
  defaultOpen = false,
  channelKey = 'WK_8a3f2e',
  embedded = false,
  layout = 'floating',
  initialPage,
}: VisitorChatProps) => {
  const storageKey = useMemo(() => `kefu_widget_session_${channelKey}`, [channelKey])
  const tokenKey = useMemo(() => `kefu_widget_visitor_token_${channelKey}`, [channelKey])
  const msgsKey = useMemo(() => `${storageKey}_msgs`, [storageKey])

  const [open, setOpen] = useState(defaultOpen || layout === 'fill')
  const [sessionId, setSessionId] = useState<number | null>(() => {
    const saved = localStorage.getItem(storageKey)
    return saved ? Number(saved) : null
  })
  const [visitorToken, setVisitorToken] = useState<string>(() => localStorage.getItem(tokenKey) || '')
  const [messages, setMessages] = useState<Message[]>(() => {
    const saved = localStorage.getItem(msgsKey)
    return saved ? JSON.parse(saved) : []
  })
  const [input, setInput] = useState('')
  const [sending, setSending] = useState(false)
  const [showRating, setShowRating] = useState(false)
  const [rated, setRated] = useState(false)
  const [sessionEnded, setSessionEnded] = useState(false)
  const [agentTyping, setAgentTyping] = useState(false)
  const [ratingScore, setRatingScore] = useState(0)
  const [ratingText, setRatingText] = useState('')
  const [hoverStar, setHoverStar] = useState(0)
  const [ratingSubmitting, setRatingSubmitting] = useState(false)

  /** 评价快捷文案：按星级展示更贴切的选项 */
  const ratingPresets = useMemo(() => {
    if (ratingScore >= 5) {
      return ['服务很棒，非常感谢！', '回复及时，专业耐心', '问题已顺利解决', '下次还会咨询']
    }
    if (ratingScore === 4) {
      return ['整体不错，体验良好', '基本解决了问题', '态度很好，继续加油', '回复比较及时']
    }
    if (ratingScore >= 1 && ratingScore <= 3) {
      return ['等待时间偏长', '问题未完全解决', '希望回复更清晰一些', '需要再跟进一下']
    }
    return ['服务很好，感谢！', '回复及时，很专业', '问题已解决', '等待时间稍长']
  }, [ratingScore])
  const [pendingImage, setPendingImage] = useState<{ file: File; preview: string } | null>(null)
  const [sendError, setSendError] = useState('')
  const [agentsOnline, setAgentsOnline] = useState(true)
  const [offlinePrompt, setOfflinePrompt] = useState('当前无客服在线，请留言并留下联系方式，我们上线后会尽快回复您。')
  const [welcomeSegments, setWelcomeSegments] = useState<WelcomeSegment[]>(defaultWelcomeSegments)
  const [displayName, setDisplayName] = useState('在线客服')
  const [agentName, setAgentName] = useState('')
  const [leaveName, setLeaveName] = useState('')
  const [leavePhone, setLeavePhone] = useState('')
  const [leaveEmail, setLeaveEmail] = useState('')
  const [leaveSent, setLeaveSent] = useState(false)
  const pollRef = useRef<number | null>(null)
  const initRef = useRef(false)
  const typingTimerRef = useRef<number | null>(null)
  const chatEndRef = useRef<HTMLDivElement>(null)
  const fileInputRef = useRef<HTMLInputElement>(null)
  const textInputRef = useRef<HTMLInputElement>(null)
  const socketRef = useRef<WebSocket | null>(null)
  const lastTypingAt = useRef(0)
  const sessionIdRef = useRef<number | null>(sessionId)
  const visitorTokenRef = useRef(visitorToken)
  const agentNameRef = useRef(agentName)
  const loadMessagesRef = useRef<(sid?: number, token?: string, opts?: { afterSeq?: number; full?: boolean }) => Promise<void>>(async () => {})
  const msgsKeyRef = useRef(msgsKey)
  /** 宿主页信息（嵌入时由 postMessage / 初始 query 更新） */
  const hostPageRef = useRef<HostPageInfo>({
    url: initialPage?.url || (typeof window !== 'undefined' ? window.location.href : ''),
    title: initialPage?.title || (typeof document !== 'undefined' ? document.title : ''),
    referrer: initialPage?.referrer || (typeof document !== 'undefined' ? document.referrer : ''),
  })
  const lastPageKeyRef = useRef('') // url + title，避免同 URL 标题更新被吞
  /** 本地已同步到的最大 seq（从缓存消息初始化） */
  const lastSeqRef = useRef((() => {
    try {
      const saved = localStorage.getItem(msgsKey)
      if (!saved) return 0
      const cached = JSON.parse(saved) as Message[]
      return cached.reduce((acc, m) => Math.max(acc, m.seq || 0), 0)
    } catch {
      return 0
    }
  })())

  const insertEmoji = (emoji: string) => {
    const { next, cursor } = insertAtCursor(textInputRef.current, input, emoji)
    setInput(next)
    if (agentsOnline) emitTyping()
    requestAnimationFrame(() => {
      const el = textInputRef.current
      if (!el) return
      el.focus()
      el.setSelectionRange(cursor, cursor)
    })
  }

  const mapServerMessage = (m: {
    id: number
    sender_type?: string
    content?: string
    type?: string
    sent_at?: string
    seq?: number
  }): Message => ({
    id: Number(m.id),
    sender: m.sender_type === 'agent' ? 'agent' : 'visitor',
    content: String(m.content ?? ''),
    type: m.type === 'image' ? 'image' : 'text',
    seq: typeof m.seq === 'number' ? m.seq : Number(m.seq || 0) || undefined,
    time: m.sent_at
      ? new Date(m.sent_at).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
      : new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
  })

  const rememberSeq = (seq?: number) => {
    if (seq && seq > lastSeqRef.current) lastSeqRef.current = seq
  }

  const mergeVisitorMessages = (existing: Message[], incoming: Message[]): Message[] => {
    const map = new Map<number, Message>()
    for (const item of existing) {
      if (item.id > 0) map.set(item.id, item)
    }
    // 保留未确认的乐观气泡
    const temps = existing.filter(item => item.id < 0)
    for (const item of incoming) map.set(item.id, item)
    const merged = Array.from(map.values())
    // 去掉已被服务端确认的乐观气泡
    const remainingTemps = temps.filter(temp =>
      !incoming.some(srv =>
        srv.sender === 'visitor'
        && srv.content === temp.content
        && (srv.type || 'text') === (temp.type || 'text'),
      ),
    )
    return [...merged, ...remainingTemps].sort((a, b) => {
      const as = a.seq || 0
      const bs = b.seq || 0
      if (as && bs && as !== bs) return as - bs
      if (a.id > 0 && b.id > 0) return a.id - b.id
      return a.id - b.id
    })
  }

  const applySessionStatus = useCallback((status?: string, satisfactionScore?: number | null) => {
    if (!status) return
    if (status === 'ended' || status === 'archived') {
      setSessionEnded(true)
      const alreadyRated = satisfactionScore != null && Number(satisfactionScore) > 0
      if (alreadyRated) {
        setRated(true)
        setShowRating(false)
      } else {
        setRated(false)
        setShowRating(true)
      }
    } else if (status === 'active') {
      setAgentsOnline(true)
    }
  }, [])

  const loadMessages = useCallback(async (
    sid?: number,
    token?: string,
    opts?: { afterSeq?: number; full?: boolean },
  ) => {
    const s = sid || sessionIdRef.current || sessionId
    const visitorCredential = token || visitorTokenRef.current || visitorToken || localStorage.getItem(tokenKey) || ''
    if (!s || !visitorCredential) return
    const full = opts?.full === true || (opts?.afterSeq == null && lastSeqRef.current <= 0)
    const afterSeq = full ? 0 : (opts?.afterSeq ?? lastSeqRef.current)
    try {
      const qs = new URLSearchParams({ session_id: String(s) })
      if (afterSeq > 0) qs.set('after_seq', String(afterSeq))
      const res = await fetch(`/api/widget/messages?${qs}`, {
        headers: { 'X-Visitor-Token': visitorCredential },
      })
      const json = await res.json()
      if (json.code !== 0 || !json.data) return

      // 兼容旧版 data 为数组的响应
      const payload = json.data
      const list: any[] = Array.isArray(payload)
        ? payload
        : (Array.isArray(payload.messages) ? payload.messages : [])
      const maxSeq = Array.isArray(payload)
        ? list.reduce((acc, m) => Math.max(acc, Number(m.seq || 0)), 0)
        : Number(payload.max_seq || 0)
      const hasMore = !Array.isArray(payload) && Boolean(payload.has_more)

      if (!Array.isArray(payload)) {
        applySessionStatus(payload.session_status, payload.satisfaction_score)
      }

      if (list.length === 0) {
        if (maxSeq > 0) rememberSeq(maxSeq)
        return
      }

      const mapped = list.map((m: any) => mapServerMessage(m))
      setMessages(prev => {
        const next = afterSeq > 0 ? mergeVisitorMessages(prev, mapped) : mapped
        localStorage.setItem(msgsKeyRef.current, JSON.stringify(next.filter(m => m.id > 0)))
        return next
      })
      const localMax = mapped.reduce((acc, m) => Math.max(acc, m.seq || 0), 0)
      rememberSeq(Math.max(maxSeq, localMax))

      // 一次未拉完则继续增量
      if (hasMore && localMax > afterSeq) {
        await loadMessages(s, visitorCredential, { afterSeq: localMax })
      }
    } catch { /* ignore */ }
  }, [sessionId, visitorToken, tokenKey, applySessionStatus])

  const appendPushedMessage = useCallback((raw: {
    id?: number
    sender_type?: string
    content?: string
    type?: string
    sent_at?: string
    seq?: number
  }) => {
    if (!raw?.id) return false
    const msg = mapServerMessage(raw as {
      id: number
      sender_type?: string
      content?: string
      type?: string
      sent_at?: string
      seq?: number
    })
    const known = lastSeqRef.current
    // seq 空洞 → 走增量拉取补齐
    if (known > 0 && msg.seq && msg.seq > known + 1) {
      void loadMessagesRef.current(undefined, undefined, { afterSeq: known })
      return true
    }
    setMessages(prev => {
      if (prev.some(item => item.id === msg.id)) return prev
      const next = mergeVisitorMessages(prev, [msg])
      localStorage.setItem(msgsKeyRef.current, JSON.stringify(next.filter(m => m.id > 0)))
      return next
    })
    if (msg.seq) rememberSeq(msg.seq)
    return true
  }, [])

  const reportPageView = useCallback(async (url: string, title: string) => {
    const sid = sessionIdRef.current
    const token = visitorTokenRef.current
    if (!url) return
    // 会话尚未 Init 完成：只缓存宿主页，避免丢掉打开前/初始化中的换页
    if (!sid || !token) {
      hostPageRef.current = {
        ...hostPageRef.current,
        url,
        title: title || hostPageRef.current.title,
      }
      return
    }
    if (sessionEnded) return
    const key = `${url}\0${title || ''}`
    // 同 URL 但标题变了仍要上报（服务端会更新 current_title；新 URL 会插轨迹）
    if (key === lastPageKeyRef.current) return
    lastPageKeyRef.current = key
    try {
      const res = await fetch('/api/widget/pageview', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-Visitor-Token': token,
        },
        body: JSON.stringify({
          session_id: sid,
          visitor_token: token,
          page_url: url,
          page_title: title || '',
        }),
      })
      if (!res.ok) {
        lastPageKeyRef.current = ''
      }
    } catch {
      lastPageKeyRef.current = ''
    }
  }, [sessionEnded])

  /** 调用 Init 创建新会话并写入本地状态（不复用已结束会话） */
  const bootstrapNewSession = useCallback(async () => {
    const page = hostPageRef.current
    const res = await fetch('/api/widget/init', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        channel_key: channelKey,
        visitor_name: '访客',
        page_url: page.url || '',
        page_title: page.title || '',
        referrer: page.referrer || '',
      }),
    })
    const json = await res.json()
    if (json.code !== 0) {
      throw new Error(json.message || '初始化会话失败')
    }
    const data = json.data
    const sid = data.session_id as number
    const token = data.visitor_token as string
    setSessionId(sid)
    setVisitorToken(token)
    sessionIdRef.current = sid
    visitorTokenRef.current = token
    setAgentsOnline(Boolean(data.agents_online))
    if (data.offline_prompt) setOfflinePrompt(data.offline_prompt)
    setWelcomeSegments(parseWelcomeSegments(data))
    if (data.display_name) setDisplayName(data.display_name)
    if (data.agent_name) setAgentName(data.agent_name)
    else if (data.agent_nickname) setAgentName(data.agent_nickname)
    else setAgentName('')
    setSessionEnded(false)
    setRated(false)
    setShowRating(false)
    setLeaveSent(false)
    setMessages([])
    localStorage.setItem(storageKey, String(sid))
    localStorage.setItem(tokenKey, token)
    localStorage.removeItem(msgsKey)
    lastSeqRef.current = 0
    // init 已写入落地页；标记已上报，避免立刻重复；宿主若已换页则补报
    const latest = hostPageRef.current
    if (page.url) lastPageKeyRef.current = `${page.url}\0${page.title || ''}`
    await loadMessages(sid, token, { full: true })
    if (latest.url && (latest.url !== page.url || (latest.title || '') !== (page.title || ''))) {
      lastPageKeyRef.current = ''
      void reportPageView(latest.url, latest.title || '')
    }
  }, [channelKey, loadMessages, storageKey, tokenKey, msgsKey, reportPageView])

  const closeSocket = useCallback(() => {
    const sock = socketRef.current
    if (!sock) return
    try {
      sock.onclose = null
      sock.close()
    } catch { /* ignore */ }
    socketRef.current = null
  }, [])

  /** 会话结束后重新咨询：清本地凭证并 Init 新会话 */
  const startNewSession = useCallback(async () => {
    if (sending) return
    setSending(true)
    setSendError('')
    setShowRating(false)
    closeSocket()
    localStorage.removeItem(storageKey)
    localStorage.removeItem(tokenKey)
    localStorage.removeItem(msgsKey)
    setSessionId(null)
    setVisitorToken('')
    sessionIdRef.current = null
    visitorTokenRef.current = ''
    setMessages([])
    setInput('')
    setSessionEnded(false)
    setRated(false)
    setRatingScore(0)
    setRatingText('')
    setHoverStar(0)
    setAgentTyping(false)
    setAgentName('')
    setLeaveSent(false)
    setPendingImage(prev => {
      if (prev?.preview) URL.revokeObjectURL(prev.preview)
      return null
    })
    lastSeqRef.current = 0
    initRef.current = true
    try {
      await bootstrapNewSession()
    } catch (e) {
      console.error('Start new session failed:', e)
      setSendError(e instanceof Error ? e.message : '发起新咨询失败，请稍后重试')
      initRef.current = false
    } finally {
      setSending(false)
    }
  }, [sending, closeSocket, storageKey, tokenKey, msgsKey, bootstrapNewSession])

  const initSession = useCallback(async () => {
    if (sessionId && visitorToken) {
      // 本地有 seq 游标时增量补齐，否则全量（若已结束会由 loadMessages 带回状态）
      const after = lastSeqRef.current
      await loadMessages(sessionId, visitorToken, after > 0 ? { afterSeq: after } : { full: true })
      return
    }
    if (initRef.current) return
    initRef.current = true
    try {
      await bootstrapNewSession()
    } catch (e) {
      console.error('Init failed:', e)
      setSendError(e instanceof Error ? e.message : '连接客服失败，请稍后重试')
      initRef.current = false
    }
  }, [sessionId, visitorToken, loadMessages, bootstrapNewSession])

  useEffect(() => {
    sessionIdRef.current = sessionId
    visitorTokenRef.current = visitorToken
    agentNameRef.current = agentName
    loadMessagesRef.current = loadMessages
    msgsKeyRef.current = msgsKey
  }, [sessionId, visitorToken, agentName, loadMessages, msgsKey])

  useEffect(() => {
    if (open) initSession()
  }, [open, initSession])

  // 嵌入模式：通知宿主就绪，并接收宿主页 URL 变更
  useEffect(() => {
    if (!embedded) return
    try {
      window.parent.postMessage({ type: 'kefu-widget-ready' }, '*')
    } catch { /* ignore */ }
    const onMsg = (event: MessageEvent) => {
      const data = event?.data
      if (!data || data.type !== 'kefu-host-page') return
      const url = typeof data.url === 'string' ? data.url : ''
      const title = typeof data.title === 'string' ? data.title : ''
      const referrer = typeof data.referrer === 'string' ? data.referrer : hostPageRef.current.referrer
      if (!url) return
      hostPageRef.current = { url, title, referrer }
      void reportPageView(url, title)
    }
    window.addEventListener('message', onMsg)
    return () => window.removeEventListener('message', onMsg)
  }, [embedded, reportPageView])

  // 非嵌入预览：用当前页作为来源
  useEffect(() => {
    if (embedded || !sessionId || !visitorToken || sessionEnded) return
    const url = window.location.href
    const title = document.title
    hostPageRef.current = {
      url,
      title,
      referrer: document.referrer || hostPageRef.current.referrer,
    }
    void reportPageView(url, title)
  }, [embedded, sessionId, visitorToken, sessionEnded, reportPageView])

  // 心跳：刷新 last_seen_at，供客服端在线读秒
  useEffect(() => {
    if (!sessionId || !visitorToken || sessionEnded || !open) return
    const beat = () => {
      void fetch('/api/widget/heartbeat', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-Visitor-Token': visitorToken,
        },
        body: JSON.stringify({ session_id: sessionId, visitor_token: visitorToken }),
      }).catch(() => {})
    }
    beat()
    const t = window.setInterval(beat, 20000)
    return () => clearInterval(t)
  }, [sessionId, visitorToken, sessionEnded, open])

  // 兜底轮询：按 after_seq 增量对齐
  useEffect(() => {
    if (sessionId && visitorToken && open) {
      pollRef.current = window.setInterval(() => {
        const after = lastSeqRef.current
        void loadMessages(undefined, undefined, after > 0 ? { afterSeq: after } : { full: true })
      }, 8000)
      return () => { if (pollRef.current) clearInterval(pollRef.current) }
    }
  }, [sessionId, visitorToken, open, loadMessages])

  // 访客 WS：session / token 就绪后建连；断线指数退避重连；消息直接追加
  useEffect(() => {
    if (!sessionId || !visitorToken || !open) 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 sid = sessionIdRef.current
      const token = visitorTokenRef.current
      if (!sid || !token) return

      const scheme = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
      const socket = new WebSocket(
        `${scheme}//${window.location.host}/api/widget/ws?session_id=${sid}`,
        ['kefu-visitor-v1', token],
      )
      socketRef.current = socket

      socket.onopen = () => {
        attempt = 0
        // 重连后按 seq 增量 catch-up
        const after = lastSeqRef.current
        void loadMessagesRef.current(sid, token, after > 0 ? { afterSeq: after } : { full: true })
      }

      socket.onmessage = (event) => {
        try {
          const payload = JSON.parse(event.data) as {
            type?: string
            session_id?: number | string
            data?: {
              id?: number
              sender_type?: string
              content?: string
              type?: string
              sent_at?: string
              from?: string
              status?: string
              agent_name?: string
            }
          }
          const eventSid = Number(payload.session_id)
          const currentSid = Number(sessionIdRef.current)
          if (!Number.isFinite(eventSid) || eventSid !== currentSid) return

          if (payload.type === 'typing') {
            if (payload.data?.from && payload.data.from !== 'agent') return
            setAgentTyping(true)
            if (typingTimerRef.current) clearTimeout(typingTimerRef.current)
            typingTimerRef.current = window.setTimeout(() => setAgentTyping(false), 1800)
            return
          }

          if (payload.type === 'session_updated') {
            const status = payload.data?.status
            if (status === 'ended' || status === 'archived') {
              setSessionEnded(true)
              setShowRating(true)
              setAgentTyping(false)
              return
            }
            if (status === 'active') {
              setAgentsOnline(true)
              setAgentName(payload.data?.agent_name || agentNameRef.current)
            }
            setAgentTyping(false)
            const after = lastSeqRef.current
            void loadMessagesRef.current(
              currentSid,
              visitorTokenRef.current,
              after > 0 ? { afterSeq: after } : { full: true },
            )
            return
          }

          if (payload.type === 'message') {
            setAgentTyping(false)
            const ok = appendPushedMessage(payload.data || {})
            if (!ok) {
              const after = lastSeqRef.current
              void loadMessagesRef.current(
                currentSid,
                visitorTokenRef.current,
                after > 0 ? { afterSeq: after } : { full: true },
              )
            }
          }
        } catch {
          // ignore
        }
      }

      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 = () => {
        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 (typingTimerRef.current) clearTimeout(typingTimerRef.current)
    }
  }, [sessionId, visitorToken, open, appendPushedMessage])

  useEffect(() => {
    chatEndRef.current?.scrollIntoView({ behavior: 'smooth' })
  }, [messages, agentTyping, open, pendingImage])

  const notifyHost = (type: 'kefu-widget-close' | 'kefu-widget-minimize') => {
    if (embedded && window.parent && window.parent !== window) {
      window.parent.postMessage({ type }, '*')
    }
  }

  const emitTyping = () => {
    if (!sessionId || sessionEnded || !agentsOnline) 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: sessionId }))
  }

  const postMessage = async (content: string, type: 'text' | 'image') => {
    if (!sessionId || !visitorToken) throw new Error('会话未就绪')
    const res = await fetch('/api/widget/message', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'X-Visitor-Token': visitorToken },
      body: JSON.stringify({ session_id: sessionId, content, type }),
    })
    const json = await res.json()
    if (json.code !== 0) {
      throw new Error(json.message || '发送失败')
    }
    return json.data
  }

  const uploadVisitorImage = async (file: File) => {
    if (!sessionId || !visitorToken) throw new Error('会话未就绪')
    const form = new FormData()
    form.append('file', file)
    form.append('session_id', String(sessionId))
    form.append('visitor_token', visitorToken)
    const res = await fetch('/api/widget/upload', {
      method: 'POST',
      headers: { 'X-Visitor-Token': visitorToken },
      body: form,
    })
    const json = await res.json()
    if (json.code !== 0) throw new Error(json.message || '上传失败')
    return json.data as { url: string; thumb_url: string }
  }

  const sendMessage = async (text: string) => {
    if (!text.trim() || sending || sessionEnded) return
    if (!agentsOnline) {
      setSendError('当前无客服在线，请使用下方留言表单')
      return
    }
    const content = text.trim()
    setInput('')
    setSendError('')
    setSending(true)

    const localMsg: Message = {
      id: -Date.now(),
      sender: 'visitor',
      content,
      type: 'text',
      time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
    }
    setMessages(prev => [...prev, localMsg])

    try {
      await postMessage(content, 'text')
      await loadMessages(sessionId || undefined)
    } catch (e) {
      setSendError(e instanceof Error ? e.message : '发送失败')
      setMessages(prev => prev.filter(m => m.id !== localMsg.id))
    } finally {
      setSending(false)
    }
  }

  const submitLeaveMessage = async () => {
    if (!sessionId || !visitorToken || sending || leaveSent) return
    const content = input.trim() || '请尽快与我联系，谢谢。'
    if (!leavePhone.trim() && !leaveEmail.trim()) {
      setSendError('请至少填写手机号或邮箱')
      return
    }
    setSending(true)
    setSendError('')
    try {
      const res = await fetch('/api/widget/leave-message', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Visitor-Token': visitorToken },
        body: JSON.stringify({
          session_id: sessionId,
          content,
          name: leaveName.trim(),
          phone: leavePhone.trim(),
          email: leaveEmail.trim(),
        }),
      })
      const json = await res.json()
      if (json.code !== 0) throw new Error(json.message || '留言失败')
      setLeaveSent(true)
      setInput('')
      if (json.data?.agents_online) setAgentsOnline(true)
      await loadMessages(sessionId, visitorToken)
    } catch (e) {
      setSendError(e instanceof Error ? e.message : '留言失败')
    } finally {
      setSending(false)
    }
  }

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

  const sendImage = async () => {
    if (!pendingImage || sending || sessionEnded || !agentsOnline) return
    setSending(true)
    setSendError('')
    try {
      const uploaded = await uploadVisitorImage(pendingImage.file)
      const localMsg: Message = {
        id: -Date.now(),
        sender: 'visitor',
        content: uploaded.url,
        type: 'image',
        time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
      }
      setMessages(prev => [...prev, localMsg])
      URL.revokeObjectURL(pendingImage.preview)
      setPendingImage(null)
      await postMessage(uploaded.url, 'image')
      await loadMessages(sessionId || undefined)
    } catch (e) {
      setSendError(e instanceof Error ? e.message : '图片发送失败')
    } finally {
      setSending(false)
    }
  }

  const handleOpen = () => {
    setOpen(true)
    setShowRating(sessionEnded && !rated)
  }

  const handleClose = () => {
    setOpen(false)
    notifyHost('kefu-widget-close')
    if (sessionEnded && !rated) setShowRating(true)
  }

  const handleMinimize = () => {
    setOpen(false)
    notifyHost('kefu-widget-minimize')
  }

  const submitRating = async () => {
    if (!sessionId || !visitorToken || rated || ratingSubmitting) return
    if (ratingScore < 1 || ratingScore > 5) {
      setSendError('请先点击星星选择评分')
      return
    }
    setRatingSubmitting(true)
    setSendError('')
    try {
      const res = await fetch('/api/widget/rating', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Visitor-Token': visitorToken },
        body: JSON.stringify({
          session_id: sessionId,
          score: ratingScore,
          text: ratingText.trim(),
        }),
      })
      const json = await res.json()
      if (json.code === 0) {
        setRated(true)
        setShowRating(false)
        setRatingScore(0)
        setRatingText('')
        setSendError('')
      } else {
        setSendError(json.message || '评价提交失败')
      }
    } catch {
      setSendError('评价提交失败，请稍后重试')
    } finally {
      setRatingSubmitting(false)
    }
  }

  const showWelcome = messages.length === 0 && !leaveSent
  const showQuick = messages.length <= 1 && !sessionEnded && agentsOnline
  const isFill = layout === 'fill'
  const shellClass = isFill
    ? 'relative w-full h-full flex flex-col bg-white overflow-hidden'
    : 'fixed z-50 right-6 bottom-6 w-[400px] h-[600px] max-w-[calc(100vw-32px)] max-h-[calc(100vh-32px)] flex flex-col rounded-xl overflow-hidden bg-white'

  const panel = open && (
    <div className={shellClass} style={isFill ? undefined : { boxShadow: 'var(--shadow-floating)' }}>
      <header className="shrink-0 px-4 py-4 bg-[#2563eb] text-white flex items-center gap-3">
        <div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center shrink-0">
          <CustomerServiceOutlined className="text-lg text-white" />
        </div>
        <div className="flex-1 min-w-0">
          <h1 className="m-0 text-[15px] font-semibold leading-tight text-white">{displayName || '在线客服'}</h1>
          <p className="m-0 text-xs leading-normal text-white/80 flex items-center gap-1 mt-0.5">
            <span
              className="inline-block w-1.5 h-1.5 rounded-full"
              style={{ background: sessionEnded ? '#94a3b8' : agentsOnline ? '#16a34a' : '#d97706' }}
            />
            {sessionEnded
              ? '会话已结束'
              : agentsOnline
                ? (agentName ? `${agentName} 为您服务` : '正在为您服务')
                : '暂不可即时接待 · 可留言'}
          </p>
        </div>
        <div className="flex items-center gap-1 shrink-0">
          {!isFill && (
            <button
              type="button"
              onClick={handleMinimize}
              className="w-7 h-7 rounded bg-white/15 hover:bg-white/25 border-0 cursor-pointer flex items-center justify-center text-white"
              aria-label="最小化"
            >
              <MinusOutlined className="text-xs" />
            </button>
          )}
          <button
            type="button"
            onClick={handleClose}
            className="w-7 h-7 rounded bg-white/15 hover:bg-white/25 border-0 cursor-pointer flex items-center justify-center text-white"
            aria-label="关闭"
          >
            <CloseOutlined className="text-xs" />
          </button>
        </div>
      </header>

      <section className="flex-1 overflow-y-auto p-4 flex flex-col gap-3 bg-neutral-50 no-scrollbar">
        {showWelcome && (
          <div className="flex flex-col items-start gap-2 max-w-[90%]">
            {agentsOnline ? (
              welcomeSegments.map((seg, i) =>
                seg.type === 'image' ? (
                  <div
                    key={`w-${i}`}
                    className="rounded-xl bg-white border border-neutral-200 p-1.5"
                  >
                    <ChatImage
                      src={seg.content}
                      alt="欢迎图"
                      className="max-w-[220px] max-h-[220px]"
                    />
                  </div>
                ) : (
                  <div
                    key={`w-${i}`}
                    className="px-4 py-2.5 rounded-xl bg-white border border-neutral-200 text-[13px] text-neutral-700 leading-normal"
                  >
                    {looksLikeHtml(seg.content) ? (
                      <SafeHtml html={seg.content} />
                    ) : (
                      <MarkdownBody>{seg.content}</MarkdownBody>
                    )}
                  </div>
                ),
              )
            ) : (
              <div className="px-4 py-2.5 rounded-xl bg-white border border-neutral-200 text-[13px] text-neutral-600 leading-normal">
                {looksLikeHtml(offlinePrompt) ? (
                  <SafeHtml html={offlinePrompt} />
                ) : (
                  <MarkdownBody>{offlinePrompt}</MarkdownBody>
                )}
              </div>
            )}
          </div>
        )}

        {showQuick && (
          <div className="flex flex-wrap gap-2 justify-center">
            {defaultQuickQuestions.map((q, i) => (
              <button
                key={i}
                type="button"
                onClick={() => sendMessage(q)}
                disabled={!sessionId || sending || sessionEnded}
                className="px-3 py-1 rounded-full border border-[#2563eb] bg-white text-[#2563eb] text-xs cursor-pointer whitespace-nowrap hover:bg-[#eff6ff] disabled:opacity-50"
              >
                {q}
              </button>
            ))}
          </div>
        )}

        {messages.map(msg => (
          msg.sender === 'visitor' ? (
            <div key={msg.id} className="flex flex-col items-end max-w-[85%] ml-auto gap-1">
              <div className={`rounded-xl bg-[#2563eb] text-white text-[13px] leading-normal ${msg.type === 'image' ? 'p-1.5' : 'px-4 py-3'}`}>
                {msg.type === 'image' ? (
                  <ChatImage src={msg.content} alt="图片" className="max-w-[180px] max-h-[180px]" />
                ) : (
                  <MarkdownBody tone="inverse">{msg.content}</MarkdownBody>
                )}
              </div>
              {msg.time && (
                <span className="text-[11px] text-neutral-400 pr-0.5">{msg.time}</span>
              )}
            </div>
          ) : (
            <div key={msg.id} className="flex gap-2 max-w-[85%]">
              <div className="w-8 h-8 rounded-full bg-neutral-200 flex items-center justify-center shrink-0 mt-0.5">
                <CustomerServiceOutlined className="text-xs text-neutral-500" />
              </div>
              <div className="min-w-0 flex flex-col gap-1">
                <div className={`rounded-xl bg-white border border-neutral-200 text-[13px] text-neutral-800 leading-normal ${msg.type === 'image' ? 'p-1.5' : 'px-4 py-3'}`}>
                  {msg.type === 'image' ? (
                    <ChatImage src={msg.content} alt="图片" className="max-w-[180px] max-h-[180px]" />
                  ) : (
                    <MarkdownBody>{msg.content}</MarkdownBody>
                  )}
                </div>
                {msg.time && (
                  <span className="text-[11px] text-neutral-400 pl-0.5">{msg.time}</span>
                )}
              </div>
            </div>
          )
        ))}

        {agentTyping && (
          <>
            <div className="flex gap-2 max-w-[85%]">
              <div className="w-8 h-8 rounded-full bg-neutral-200 flex items-center justify-center shrink-0 mt-0.5">
                <CustomerServiceOutlined className="text-xs text-neutral-500" />
              </div>
              <div className="px-4 py-3 rounded-xl bg-white border border-neutral-200 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>
            <p className="text-left text-xs text-neutral-400 m-0 pl-[42px]">客服正在输入...</p>
          </>
        )}
        <div ref={chatEndRef} />
      </section>

      <footer className="shrink-0 px-4 py-3 bg-white border-t border-neutral-200">
        {sendError && <div className="mb-2 text-xs text-red-500">{sendError}</div>}

        {sessionEnded && (
          <div className="mb-3 space-y-2">
            {!rated && (
              <div className="rounded-lg bg-amber-50 border border-amber-100 px-3 py-2 flex items-center justify-between gap-2">
                <span className="text-xs text-amber-800">会话已结束，欢迎为本次服务打分</span>
                <button
                  type="button"
                  onClick={() => setShowRating(true)}
                  className="shrink-0 text-xs font-medium text-amber-900 bg-amber-100 hover:bg-amber-200 border-0 rounded-md px-2 py-1 cursor-pointer"
                >
                  去评价
                </button>
              </div>
            )}
            <button
              type="button"
              onClick={() => { void startNewSession() }}
              disabled={sending}
              className="w-full h-10 rounded-xl border-0 bg-[#2563eb] hover:bg-[#1d4ed8] disabled:opacity-50 cursor-pointer text-white text-sm font-medium"
            >
              {sending ? '正在接入...' : '重新咨询'}
            </button>
            <p className="m-0 text-center text-[11px] text-neutral-400">将开启新会话，历史消息仅保留在本页展示到重新接入前</p>
          </div>
        )}

        {!agentsOnline && !sessionEnded && (
          <div className="mb-3 p-3 rounded-lg border border-amber-100 bg-amber-50 space-y-2">
            {leaveSent ? (
              <div className="text-xs text-amber-800 leading-relaxed">
                留言已提交，客服上线后会尽快联系您。您也可继续补充留言内容。
              </div>
            ) : (
              <>
                <div className="text-xs text-amber-800 font-medium">离线留言</div>
                <input
                  className="w-full h-8 px-2 rounded-md border border-neutral-200 bg-white text-xs outline-none"
                  placeholder="您的姓名（可选）"
                  value={leaveName}
                  onChange={e => setLeaveName(e.target.value)}
                />
                <input
                  className="w-full h-8 px-2 rounded-md border border-neutral-200 bg-white text-xs outline-none"
                  placeholder="手机号"
                  value={leavePhone}
                  onChange={e => setLeavePhone(e.target.value)}
                />
                <input
                  className="w-full h-8 px-2 rounded-md border border-neutral-200 bg-white text-xs outline-none"
                  placeholder="邮箱"
                  value={leaveEmail}
                  onChange={e => setLeaveEmail(e.target.value)}
                />
              </>
            )}
          </div>
        )}

        {!sessionEnded && pendingImage && agentsOnline && (
          <div className="mb-2 p-2 rounded-lg border border-neutral-200 bg-neutral-50 flex items-center gap-2">
            <img src={pendingImage.preview} alt="预览" className="w-14 h-14 object-cover rounded" />
            <div className="flex-1 min-w-0 text-xs text-neutral-500">确认上传并发送？（自动转 WebP）</div>
            <button
              type="button"
              className="text-xs text-neutral-400 border-0 bg-transparent cursor-pointer"
              onClick={() => {
                URL.revokeObjectURL(pendingImage.preview)
                setPendingImage(null)
              }}
            >
              取消
            </button>
            <button type="button" disabled={sending} className="text-xs px-2 py-1 rounded-md bg-[#2563eb] text-white border-0 cursor-pointer disabled:opacity-50" onClick={sendImage}>发送</button>
          </div>
        )}

        {!sessionEnded && agentsOnline && (
          <div className="flex items-center gap-2 mb-2">
            <button
              type="button"
              className="w-8 h-8 rounded-md border-0 bg-transparent cursor-pointer flex items-center justify-center text-neutral-400 hover:bg-neutral-50 disabled:opacity-40"
              aria-label="发送图片"
              onClick={() => fileInputRef.current?.click()}
              disabled={!sessionId || sending}
            >
              <PictureOutlined className="text-base" />
            </button>
            <EmojiPicker
              onSelect={insertEmoji}
              disabled={!sessionId || sending}
              className="w-8 h-8 rounded-md border-0 bg-transparent cursor-pointer flex items-center justify-center text-neutral-400 hover:bg-neutral-50 disabled:opacity-40"
              placement="topLeft"
            />
            <input
              ref={fileInputRef}
              type="file"
              accept="image/jpeg,image/png,image/gif,image/webp"
              className="hidden"
              onChange={e => { handleImageFile(e.target.files?.[0]); e.currentTarget.value = '' }}
            />
          </div>
        )}

        {!sessionEnded && (
          <div className="flex items-center gap-2">
            <input
              ref={textInputRef}
              className="flex-1 min-w-0 h-10 px-3 rounded-xl border border-neutral-200 bg-neutral-50 text-[13px] text-neutral-800 outline-none focus:border-[#2563eb]"
              placeholder={
                !sessionId
                  ? '正在连接...'
                  : agentsOnline
                    ? '输入消息...'
                    : '描述您的问题（留言）'
              }
              value={input}
              onChange={e => { setInput(e.target.value); if (agentsOnline) emitTyping() }}
              onPaste={e => {
                if (!agentsOnline) return
                const item = Array.from(e.clipboardData.items).find(i => i.type.startsWith('image/'))
                if (item) {
                  e.preventDefault()
                  handleImageFile(item.getAsFile())
                }
              }}
              onKeyDown={e => {
                if (e.key === 'Enter') {
                  if (agentsOnline) sendMessage(input)
                }
              }}
              disabled={sending || !sessionId}
            />
            {agentsOnline ? (
              <button
                type="button"
                onClick={() => sendMessage(input)}
                disabled={!input.trim() || sending || !sessionId}
                className="w-10 h-10 rounded-full border-0 bg-[#2563eb] hover:bg-[#1d4ed8] disabled:opacity-40 cursor-pointer flex items-center justify-center shrink-0"
                aria-label="发送"
              >
                <SendOutlined className="text-white text-sm" />
              </button>
            ) : (
              <button
                type="button"
                onClick={submitLeaveMessage}
                disabled={sending || !sessionId}
                className="h-10 px-3 rounded-full border-0 bg-[#d97706] hover:bg-[#b45309] disabled:opacity-40 cursor-pointer text-white text-xs shrink-0"
              >
                {leaveSent ? '再留言' : '提交留言'}
              </button>
            )}
          </div>
        )}
      </footer>

      {showRating && (
        <div className="absolute inset-0 bg-black/40 flex items-center justify-center z-10 p-4">
          <div className="bg-white rounded-xl p-5 w-full max-w-xs text-center shadow-lg">
            <div className="text-lg font-semibold text-neutral-800 mb-1">本次服务如何？</div>
            <div className="text-sm text-neutral-400 mb-3">先点亮星级，可补充感受后再提交</div>
            <div className="flex justify-center gap-1.5 mb-1">
              {[1, 2, 3, 4, 5].map(star => {
                const active = star <= (hoverStar || ratingScore)
                return (
                  <StarFilled
                    key={star}
                    className="text-2xl cursor-pointer transition-colors"
                    style={{ color: active ? '#facc15' : '#e2e8f0' }}
                    onMouseEnter={() => setHoverStar(star)}
                    onMouseLeave={() => setHoverStar(0)}
                    onClick={() => {
                      setRatingScore(star)
                      setSendError('')
                    }}
                  />
                )
              })}
            </div>
            <div className="text-xs text-neutral-400 mb-3 h-4">
              {ratingScore > 0 ? `${ratingScore} 星` : '请选择 1～5 星'}
            </div>
            <div className="flex flex-wrap gap-1.5 justify-center mb-3">
              {ratingPresets.map(preset => {
                const selected = ratingText === preset
                return (
                  <button
                    key={preset}
                    type="button"
                    onClick={() => setRatingText(prev => (prev === preset ? '' : preset))}
                    className={`px-2 py-1 rounded-full text-[11px] border cursor-pointer transition-colors ${
                      selected
                        ? 'bg-[#2563eb] border-[#2563eb] text-white'
                        : 'bg-neutral-50 border-neutral-200 text-neutral-600 hover:border-blue-300 hover:text-blue-600'
                    }`}
                  >
                    {preset}
                  </button>
                )
              })}
            </div>
            <textarea
              className="w-full rounded-lg border border-neutral-200 p-2 text-sm outline-none focus:border-blue-400 mb-3 resize-none text-left"
              rows={3}
              maxLength={500}
              value={ratingText}
              onChange={e => setRatingText(e.target.value)}
              placeholder="可选：点击上方标签快速填入，或自行输入"
            />
            <button
              type="button"
              disabled={ratingSubmitting || ratingScore < 1}
              onClick={() => { void submitRating() }}
              className="w-full h-10 rounded-xl border-0 bg-[#2563eb] hover:bg-[#1d4ed8] disabled:opacity-40 cursor-pointer text-white text-sm font-medium mb-2"
            >
              {ratingSubmitting ? '提交中...' : '提交评价'}
            </button>
            <div className="flex flex-col gap-1.5">
              <button
                type="button"
                onClick={() => {
                  setShowRating(false)
                  setRatingScore(0)
                  setHoverStar(0)
                }}
                className="text-sm text-neutral-400 hover:text-neutral-600 border-0 bg-transparent cursor-pointer"
              >
                跳过
              </button>
              <button
                type="button"
                disabled={sending || ratingSubmitting}
                onClick={() => {
                  setShowRating(false)
                  setRatingScore(0)
                  setHoverStar(0)
                  void startNewSession()
                }}
                className="text-sm text-[#2563eb] hover:text-[#1d4ed8] border-0 bg-transparent cursor-pointer disabled:opacity-50"
              >
                跳过并重新咨询
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  )

  return (
    <>
      {!open && layout === 'floating' && (
        <button
          type="button"
          onClick={handleOpen}
          className="fixed right-6 bottom-6 w-14 h-14 rounded-full bg-[#2563eb] hover:bg-[#1d4ed8] text-white flex items-center justify-center z-50 transition-transform hover:scale-105"
          style={{ boxShadow: 'var(--shadow-floating)' }}
          aria-label="打开在线客服"
        >
          <MessageOutlined className="text-xl" />
        </button>
      )}
      {panel}
    </>
  )
}

export default VisitorChat
