'use client'

import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Image from 'next/image'
import Link from 'next/link'
import Lottie from 'lottie-react'
import { FaHeart, FaRegHeart, FaStar, FaUser } from 'react-icons/fa6'
import { FaVolumeDown, FaVolumeMute, FaVolumeUp } from 'react-icons/fa'
import { FiChevronDown, FiChevronUp, FiGift, FiMessageCircle, FiMoreVertical, FiRefreshCw, FiSend, FiStar, FiX } from 'react-icons/fi'
import { HiClock } from 'react-icons/hi2'
import { resolveAvatarFrameAssetUrl } from '@/lib/avatarFrames/resolveUrl'
import VipBadgeLottie from '@/components/VipBadgeLottie'
import RoomVideoPlayer from '@/app/room/_components/RoomVideoPlayer'
import type { RoomStream } from '@/app/room/_components/types'
import type { ChatMessage, EmojiInfo, MentionInfo, ParticipantItem } from '@/app/room/_components/types'
import { getUserRoleBadge, getUserNameColor } from '@/app/room/_components/chat/UserRoleUtils'
import { renderMessageText } from '@/app/room/_components/chat/RenderMessageText'
import GiftChatMessage from '@/app/room/_components/chat/GiftChatMessage'
import { formatLiveDuration } from '@/utils/format'
import { encryptWsPayload } from '@/utils/wsCrypto'
import { buildChatWsUrl, parseChatWsMessage, sendChatJoin } from '@/lib/chat/wsChat'
import { isEnterRoomSystemMessage } from '@/app/room/_components/chat/utils'

type Props = {
    initialRoomId: string
    initialRoomPublicId: string
    initialStream: RoomStream
    initialIsLiveStream: boolean
    initialPlaybackSrc: string
    initialIsLiked: boolean
    initialIsFollowing: boolean
    currentUserId?: string | null
    currentUserDisplayName?: string | null
    currentUserAvatar?: string | null
}

type ViewerListItem = { id: string; name: string; avatar: string | null; badge: string | null }

type HeartParticle = {
    id: string
    x: number
    y: number
    startX: number
    startY: number
    image: HTMLImageElement
    animationType: number
    startTime: number
    duration: number
    type: 'freeLike'
    opacity: number
    scale: number
    verticalProgress: number
}

export default function RoomPageClientMb({
    initialRoomId: roomId,
    initialRoomPublicId,
    initialStream: stream,
    initialIsLiveStream: isLiveStream,
    initialPlaybackSrc: playbackSrc,
    initialIsLiked,
    initialIsFollowing,
    currentUserId,
    currentUserDisplayName,
    currentUserAvatar,
}: Props) {
    const { t } = useTranslation()
    const [isFollowing, setIsFollowing] = useState(initialIsFollowing)
    const [flyingStars, setFlyingStars] = useState<number[]>([])
    const [isLiked, setIsLiked] = useState(initialIsLiked)
    const [likeCount, setLikeCount] = useState(stream.likes)
    const [viewersCount, setViewersCount] = useState(0)
    const [liveNowMs, setLiveNowMs] = useState(() => Date.now())
    const [playerDetectedLive, setPlayerDetectedLive] = useState(false)
    const [viewersListOpen, setViewersListOpen] = useState(false)
    const [viewersListEntering, setViewersListEntering] = useState(false)
    const [viewersListTab, setViewersListTab] = useState<'listeners' | 'fan'>('listeners')
    const [viewersListDragY, setViewersListDragY] = useState(0)
    const [viewersListClosing, setViewersListClosing] = useState(false)
    const [viewersListRefreshing, setViewersListRefreshing] = useState(false)
    const [profileUser, setProfileUser] = useState<{ id: string; name: string; avatar: string | null; badge: string | null } | null>(null)
    const [profileUserGroups, setProfileUserGroups] = useState<{ id: string; name: string }[] | null>(null)
    const [profilePanelEntering, setProfilePanelEntering] = useState(false)
    const [profilePanelClosing, setProfilePanelClosing] = useState(false)
    const [profileOpenedFromViewersList, setProfileOpenedFromViewersList] = useState(false)
    const [messages, setMessages] = useState<ChatMessage[]>([])
    const [viewersList, setViewersList] = useState<ViewerListItem[]>([])
    const [chatDraft, setChatDraft] = useState('')
    const [chatDisabled, setChatDisabled] = useState(false)
    const [giftLottieData, setGiftLottieData] = useState<object | null>(null)
    const [isAtBottom, setIsAtBottom] = useState(true)
    const [hasNewMessagesBelow, setHasNewMessagesBelow] = useState(false)
    const [giftPanelOpen, setGiftPanelOpen] = useState(false)
    const [giftPanelEntering, setGiftPanelEntering] = useState(false)
    const [giftPanelClosing, setGiftPanelClosing] = useState(false)
    const [giftPanelDragY, setGiftPanelDragY] = useState(0)
    const [giftPanelTab, setGiftPanelTab] = useState<'featured' | 'vip' | 'basic' | 'text'>('featured')
    const [selectedGiftId, setSelectedGiftId] = useState<string | null>(null)
    const [avatarFrameLottieData, setAvatarFrameLottieData] = useState<object | null>(null)
    const [profileAvatarFrameLottieData, setProfileAvatarFrameLottieData] = useState<object | null>(null)
    const viewersListDragStart = useRef({ y: 0, t: 0 })
    const giftPanelDragStart = useRef({ y: 0, t: 0 })
    const viewersListSwipeStart = useRef({ x: 0, y: 0 })
    const wsRef = useRef<WebSocket | null>(null)
    const vipBadgeRef = useRef<HTMLSpanElement>(null)
    const messagesContainerRef = useRef<HTMLDivElement | null>(null)
    const lastMessageRef = useRef<HTMLDivElement | null>(null)
    const reconnectTimeoutRef = useRef<number | null>(null)
    const reconnectAttemptsRef = useRef(0)
    const closeWhenOpenRef = useRef(false)
    const modUserIdsRef = useRef<Set<string>>(new Set())
    const superAdminUserIdsRef = useRef<Set<string>>(new Set())
    const [modUserIds, setModUserIds] = useState<Set<string>>(() => new Set())
    const [superAdminUserIds, setSuperAdminUserIds] = useState<Set<string>>(() => new Set())
    const [userProfiles, setUserProfiles] = useState<
        Map<
            string,
            {
                avatar: string | null
                vipLevel?: 'none' | 'vip' | 'vvip' | 'svip' | 'royal'
                avatarFrame?: { id: string; image: string; type: 'LOTTIE' | 'IMG' } | null
            }
        >
    >(() => new Map())
    const heartsCanvasRef = useRef<HTMLCanvasElement | null>(null)
    const heartsCtxRef = useRef<CanvasRenderingContext2D | null>(null)
    const heartImagesRef = useRef<Map<string, HTMLImageElement>>(new Map())
    const heartParticlesRef = useRef<HeartParticle[]>([])
    const heartsAnimFrameRef = useRef<number | null>(null)
    const heartButtonRef = useRef<HTMLButtonElement | null>(null)
    const videoElRef = useRef<HTMLVideoElement | null>(null)
    /** 當後端沒有 startedAt 時，用播放器偵測到 live 當下的時間當作開播起點（僅設一次） */
    const liveStartFallbackMsRef = useRef<number | null>(null)
    const [playerVolume, setPlayerVolume] = useState<number>(1)
    const [playerMuted, setPlayerMuted] = useState<boolean>(false)
    const lastVolumeRef = useRef<number>(1)
    const messageRowRefs = useRef<Map<string, HTMLDivElement>>(new Map())
    const prevMessageIdsRef = useRef<string[]>([])
    const enterBannerRef = useRef<HTMLDivElement | null>(null)
    const lastEnterMessageIdRef = useRef<string | null>(null)
    const [activeEnterMessage, setActiveEnterMessage] = useState<ChatMessage | null>(null)
    const prevEnterBannerRef = useRef<HTMLDivElement | null>(null)
    const [prevEnterMessage, setPrevEnterMessage] = useState<ChatMessage | null>(null)

    const getEffectiveUserType = useCallback(
        (msg: ChatMessage): 'streamer' | 'manager' | 'superAdmin' | undefined => {
            if (msg.userId && msg.userId === stream.userId) return 'streamer'
            const t = msg.userType
            if (t === 'streamer' || t === 'manager' || t === 'superAdmin') return t
            if (msg.userId && superAdminUserIdsRef.current.has(msg.userId)) return 'superAdmin'
            if (msg.userId && modUserIdsRef.current.has(msg.userId)) return 'manager'
            return undefined
        },
        [stream.userId]
    )

    const userProfilesRef = useRef(userProfiles)
    useEffect(() => {
        userProfilesRef.current = userProfiles
    }, [userProfiles])

    // Hearts canvas 初始化尺寸（固定在畫面右下角，直式長條）
    useEffect(() => {
        const canvas = heartsCanvasRef.current
        if (!canvas) return
        const parent = canvas.parentElement
        if (!parent) return

        const resize = () => {
            const parentHeight = parent.clientHeight || (typeof window !== 'undefined' ? window.innerHeight : 0)
            const h = Math.max(0, parentHeight)
            const cw = 250 // 固定寬度，直式條狀
            const ch = Math.max(0, Math.floor(h))
            if (canvas.width === cw && canvas.height === ch) return
            canvas.width = cw
            canvas.height = ch
            canvas.style.width = `${cw}px`
            canvas.style.height = `${ch}px`
        }

        resize()
        const ro = new ResizeObserver(resize)
        ro.observe(parent)

        const ctx = canvas.getContext('2d')
        if (ctx) heartsCtxRef.current = ctx

        return () => {
            ro.disconnect()
            if (heartsAnimFrameRef.current !== null) {
                cancelAnimationFrame(heartsAnimFrameRef.current)
                heartsAnimFrameRef.current = null
            }
        }
    }, [])

    const isLiveStreamUi = isLiveStream || playerDetectedLive

    const displayMessages = useMemo(
        () =>
            messages.map((m) => {
                const effective = getEffectiveUserType(m)
                if (!effective || m.userType === effective) return m
                return { ...m, userType: effective }
            }),
        [messages, getEffectiveUserType]
    )

    const triggerFlyingStars = useCallback(() => {
        const now = Date.now()
        const ids = Array.from({ length: 5 }, (_, i) => now + i)
        setFlyingStars(ids)
        window.setTimeout(() => setFlyingStars([]), 1000)
    }, [])

    const applyPlayerVolume = useCallback((v: number) => {
        const el = videoElRef.current
        if (!el) return
        const clamped = Math.max(0, Math.min(1, v))
        el.volume = clamped
        el.muted = clamped === 0
        setPlayerVolume(clamped)
        setPlayerMuted(el.muted)
        if (clamped > 0) lastVolumeRef.current = clamped
    }, [])

    const togglePlayerMute = useCallback(() => {
        if (playerMuted || playerVolume === 0) {
            applyPlayerVolume(lastVolumeRef.current || 0.6)
        } else {
            applyPlayerVolume(0)
        }
    }, [applyPlayerVolume, playerMuted, playerVolume])

    const VIEWERS_LIST_SWIPE_THRESHOLD = 50
    const handleViewersListSwipeStart = useCallback((clientX: number, clientY: number) => {
        viewersListSwipeStart.current = { x: clientX, y: clientY }
    }, [])
    const handleViewersListSwipeEnd = useCallback((clientX: number, clientY: number) => {
        const deltaX = clientX - viewersListSwipeStart.current.x
        const deltaY = clientY - viewersListSwipeStart.current.y
        if (Math.abs(deltaX) < VIEWERS_LIST_SWIPE_THRESHOLD) return
        if (Math.abs(deltaX) <= Math.abs(deltaY)) return
        if (deltaX < 0) setViewersListTab('fan')
        else setViewersListTab('listeners')
    }, [])

    useEffect(() => {
        if (!viewersListOpen || !viewersListEntering) return
        const id = requestAnimationFrame(() => {
            requestAnimationFrame(() => setViewersListEntering(false))
        })
        return () => cancelAnimationFrame(id)
    }, [viewersListOpen, viewersListEntering])

    useEffect(() => {
        if (!profileUser || !profilePanelEntering) return
        const id = requestAnimationFrame(() => {
            requestAnimationFrame(() => setProfilePanelEntering(false))
        })
        return () => cancelAnimationFrame(id)
    }, [profileUser, profilePanelEntering])

    useEffect(() => {
        if (!profileUser) {
            setProfileUserGroups(null)
            return
        }
        let cancelled = false
        setProfileUserGroups(null)
        const userApiKey = profileUser.id === stream.userId ? initialRoomPublicId : profileUser.id
        fetch(`/api/users/${encodeURIComponent(userApiKey)}?lite=1`)
            .then((res) => res.json())
            .then((json: { ok?: boolean; data?: { groups?: { id: string; name: string }[] } }) => {
                if (cancelled || !json?.ok || !Array.isArray(json.data?.groups)) return
                setProfileUserGroups(json.data.groups)
            })
            .catch(() => {})
        return () => {
            cancelled = true
        }
    }, [profileUser, stream.userId, initialRoomPublicId])

    useEffect(() => {
        const el = messagesContainerRef.current
        if (!el) return

        if (!isAtBottom) {
            if (messages.length > 0) setHasNewMessagesBelow(true)
            return
        }

        el.scrollTop = el.scrollHeight
        setHasNewMessagesBelow(false)
    }, [messages.length, isAtBottom])

    // 送禮面板進場動畫（由下往上滑上來）
    useEffect(() => {
        if (!giftPanelOpen || !giftPanelEntering) return
        const id = requestAnimationFrame(() => {
            requestAnimationFrame(() => setGiftPanelEntering(false))
        })
        return () => cancelAnimationFrame(id)
    }, [giftPanelOpen, giftPanelEntering])

    // WebSocket：聊天室、觀眾數、參與者（參考原版 /room）
    useEffect(() => {
        if (typeof window === 'undefined') return
        let isUnmounting = false
        const connectWebSocket = () => {
            if (isUnmounting) return
            closeWhenOpenRef.current = false
            try {
                const wsUrl = buildChatWsUrl()
                const author = currentUserDisplayName || '訪客'
                const ws = new WebSocket(wsUrl)
                wsRef.current = ws
                ws.onopen = () => {
                    if (closeWhenOpenRef.current) {
                        ws.close()
                        return
                    }
                    reconnectAttemptsRef.current = 0
                    sendChatJoin(ws, { roomId, userId: currentUserId, author })
                    if (currentUserId) {
                        ws.send(encryptWsPayload({ type: 'check_ban_status', userId: currentUserId }))
                        ws.send(encryptWsPayload({ type: 'check_mod_status', userId: currentUserId }))
                    }
                    ws.send(encryptWsPayload({ type: 'check_chat_status' }))
                    ws.send(encryptWsPayload({ type: 'get_participants' }))
                }
                ws.onmessage = (event) => {
                    try {
                        const data = parseChatWsMessage(event.data as string)
                        if (!data) return
                        if (data.type === 'message' || data.type === 'message_sent') {
                            const userId = data.data?.userId ?? null
                            if (data.type === 'message_sent' && userId === currentUserId) return
                            let userType: 'streamer' | 'manager' | 'superAdmin' | undefined = data.data?.userType
                            if (!userType && userId) {
                                if (userId === stream.userId) userType = 'streamer'
                                else if (superAdminUserIdsRef.current.has(userId)) userType = 'superAdmin'
                                else if (modUserIdsRef.current.has(userId)) userType = 'manager'
                            }
                            const msg: ChatMessage = {
                                id: data.data?.id ?? `m-${Date.now()}`,
                                author: data.data?.author ?? '',
                                text: data.data?.text ?? '',
                                isSystem: data.data?.isSystem ?? false,
                                isModeratorOnly: data.data?.isModeratorOnly || false,
                                userId,
                                userType,
                                time: data.data?.createdAtMs
                                    ? new Date(data.data.createdAtMs).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
                                    : new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
                                mentions:
                                    Array.isArray(data.data?.mentions) && data.data.mentions.length > 0
                                        ? (data.data.mentions as MentionInfo[])
                                        : undefined,
                                emojis:
                                    Array.isArray(data.data?.emojis) && data.data.emojis.length > 0
                                        ? (data.data.emojis as EmojiInfo[])
                                        : undefined,
                                bannedUserId: data.data?.bannedUserId || undefined,
                                unbannedUserId: data.data?.unbannedUserId || undefined,
                                adminUserId: data.data?.adminUserId || undefined,
                                adminUserType: data.data?.adminUserType as ChatMessage['adminUserType'],
                                moddedUserId: data.data?.moddedUserId || undefined,
                                unmoddedUserId: data.data?.unmoddedUserId || undefined,
                                systemMessageType: data.data?.systemMessageType as ChatMessage['systemMessageType'],
                                systemMessageData: data.data?.systemMessageData as ChatMessage['systemMessageData'],
                                authorPublicPathKey:
                                    typeof data.data?.authorPublicPathKey === 'string' && data.data.authorPublicPathKey.trim()
                                        ? data.data.authorPublicPathKey.trim()
                                        : undefined,
                            }
                            if (msg.isModeratorOnly) {
                                const isModRoomSystem =
                                    msg.systemMessageType === 'modAdded' || msg.systemMessageType === 'modRemoved'
                                if (!isModRoomSystem) {
                                    const uid = currentUserId
                                    const canSee =
                                        uid &&
                                        (uid === stream.userId ||
                                            modUserIdsRef.current.has(uid) ||
                                            superAdminUserIdsRef.current.has(uid))
                                    if (!canSee) return
                                }
                            }
                            if (isEnterRoomSystemMessage(msg)) {
                                return
                            }
                            setMessages((prev) => {
                                if (prev.some((m) => m.id === msg.id)) return prev
                                return [...prev, msg]
                            })
                        } else if (data.type === 'mod_status') {
                            if (data.data) {
                                const uid = String(data.data.userId ?? '').trim()
                                if (uid) {
                                if (data.data.isSuperAdmin) {
                                    superAdminUserIdsRef.current = new Set([...superAdminUserIdsRef.current, uid])
                                } else {
                                    const next = new Set(superAdminUserIdsRef.current)
                                    next.delete(uid)
                                    superAdminUserIdsRef.current = next
                                }
                                if (data.data.isMod) {
                                    modUserIdsRef.current = new Set([...modUserIdsRef.current, uid])
                                } else {
                                    const next = new Set(modUserIdsRef.current)
                                    next.delete(uid)
                                    modUserIdsRef.current = next
                                }
                                setModUserIds(new Set(modUserIdsRef.current))
                                setSuperAdminUserIds(new Set(superAdminUserIdsRef.current))
                                }
                            }
                        } else if (data.type === 'mod_added') {
                            if (data.data?.userId) {
                                const addedUserId = data.data.userId
                                modUserIdsRef.current = new Set([...modUserIdsRef.current, addedUserId])
                                setModUserIds(new Set(modUserIdsRef.current))
                                setMessages((prev) =>
                                    prev.map((m) =>
                                        m.userId === addedUserId && m.userId !== stream.userId && !superAdminUserIdsRef.current.has(addedUserId)
                                            ? { ...m, userType: 'manager' as const }
                                            : m
                                    )
                                )
                            }
                        } else if (data.type === 'mod_removed') {
                            if (data.data?.userId) {
                                const removedUserId = data.data.userId
                                const next = new Set(modUserIdsRef.current)
                                next.delete(removedUserId)
                                modUserIdsRef.current = next
                                setModUserIds(new Set(modUserIdsRef.current))
                                setMessages((prev) =>
                                    prev.map((m) => {
                                        if (m.userId !== removedUserId || m.userType !== 'manager') return m
                                        if (m.userId === stream.userId) return { ...m, userType: 'streamer' as const }
                                        if (superAdminUserIdsRef.current.has(removedUserId)) return { ...m, userType: 'superAdmin' as const }
                                        return { ...m, userType: undefined }
                                    })
                                )
                            }
                        } else if (data.type === 'participants_update') {
                            const participants = data.data?.participants ?? []
                            setViewersList(
                                participants.map((p: { userId?: string | null; author?: string }, i: number) => {
                                    const id = p.userId ?? `p-${i}`
                                    const profile = p.userId ? userProfilesRef.current.get(p.userId) : undefined
                                    return {
                                        id,
                                        name: p.author ?? '',
                                        avatar: profile?.avatar ?? null,
                                        badge: null,
                                    }
                                })
                            )
                            participants.forEach((p: { userId?: string | null }) => {
                                const uid = p.userId
                                if (uid && wsRef.current?.readyState === WebSocket.OPEN) {
                                    wsRef.current.send(encryptWsPayload({ type: 'check_mod_status', userId: uid }))
                                }
                            })
                        } else if (data.type === 'user_profile') {
                            if (data.data && data.data.userId) {
                                const profileUserId = data.data.userId as string
                                const avatar = (data.data.avatar ?? null) as string | null
                                const rawVip = String(data.data.vipLevel ?? 'none').trim().toLowerCase()
                                const vipLevel: 'none' | 'vip' | 'vvip' | 'svip' | 'royal' =
                                    rawVip === 'vip' || rawVip === 'vvip' || rawVip === 'svip' || rawVip === 'royal'
                                        ? (rawVip as 'vip' | 'vvip' | 'svip' | 'royal')
                                        : 'none'
                                const avatarFramePayload = data.data.avatarFrame as
                                    | { id?: string; image?: string; type?: string }
                                    | null
                                    | undefined
                                const avatarFrame =
                                    avatarFramePayload && avatarFramePayload.image
                                        ? {
                                              id: String(avatarFramePayload.id ?? ''),
                                              image: avatarFramePayload.image,
                                              type: (() => {
                                                  const t = String(avatarFramePayload.type ?? 'IMG').toUpperCase()
                                                  return t === 'LOTTIE' || t === 'LOTTERY'
                                                      ? ('LOTTIE' as const)
                                                      : ('IMG' as const)
                                              })(),
                                          }
                                        : null
                                setUserProfiles((prev) => {
                                    const next = new Map(prev)
                                    next.set(profileUserId, { avatar, vipLevel, avatarFrame })
                                    return next
                                })
                                setViewersList((prev) =>
                                    prev.map((viewer) =>
                                        viewer.id === profileUserId ? { ...viewer, avatar } : viewer
                                    )
                                )
                            }
                        } else if (data.type === 'viewers_update') {
                            if (typeof data.data?.viewers === 'number') setViewersCount(data.data.viewers)
                        } else if (data.type === 'chat_status') {
                            if (typeof data.data?.chatDisabled === 'boolean') setChatDisabled(data.data.chatDisabled)
                        } else if (data.type === 'chat_disabled') {
                            setChatDisabled(true)
                        } else if (data.type === 'chat_enabled') {
                            setChatDisabled(false)
                        }
                    } catch (err) {
                        console.error('Failed to parse WebSocket message:', err)
                    }
                }
                ws.onerror = () => {}
                ws.onclose = () => {
                    wsRef.current = null
                    if (!isUnmounting && reconnectAttemptsRef.current < 5) {
                        reconnectAttemptsRef.current++
                        const delay = Math.min(1000 * Math.pow(2, reconnectAttemptsRef.current), 10000)
                        reconnectTimeoutRef.current = window.setTimeout(connectWebSocket, delay)
                    }
                }
            } catch (err) {
                console.error('Failed to connect WebSocket:', err)
            }
        }
        connectWebSocket()
        return () => {
            isUnmounting = true
            closeWhenOpenRef.current = true
            if (reconnectTimeoutRef.current) {
                clearTimeout(reconnectTimeoutRef.current)
                reconnectTimeoutRef.current = null
            }
            const ws = wsRef.current
            if (ws) {
                wsRef.current = null
                ws.onclose = null
                ws.onmessage = null
                ws.onerror = null
                if (ws.readyState === WebSocket.CONNECTING) {
                    ws.onopen = () => ws.close()
                } else if (ws.readyState === WebSocket.OPEN) {
                    ws.close()
                }
            }
        }
    }, [roomId, currentUserId, currentUserDisplayName, stream.userId])

    useEffect(() => {
        let cancelled = false
        fetch('/lotties/badge/gift.json')
            .then((res) => res.json())
            .then((data: object) => {
                if (!cancelled) setGiftLottieData(data)
            })
            .catch(() => {})
        // preload heart images for flying hearts
        const loadHeartImage = (src: string): Promise<HTMLImageElement> =>
            new Promise((resolve, reject) => {
                const img = window.document.createElement('img')
                img.onload = () => resolve(img)
                img.onerror = reject
                img.src = src
            })
        ;(async () => {
            try {
                const [imgA, imgB] = await Promise.all([
                    loadHeartImage('/images/room/heart_a.png'),
                    loadHeartImage('/images/room/heart_b.png'),
                ])
                const map = new Map<string, HTMLImageElement>()
                map.set('a', imgA)
                map.set('b', imgB)
                heartImagesRef.current = map
            } catch (err) {
                console.warn('Failed to preload heart images:', err)
            }
        })()
        return () => {
            cancelled = true
        }
    }, [])

    // 讀取主播頭像框（若為 LOTTIE）
    useEffect(() => {
        let cancelled = false
        if (!stream.avatarFrame || stream.avatarFrame.type !== 'LOTTIE') {
            setAvatarFrameLottieData(null)
            return
        }
        const url = resolveAvatarFrameAssetUrl(stream.avatarFrame.image)
        if (!url) {
            setAvatarFrameLottieData(null)
            return
        }
        fetch(url)
            .then((res) => res.json())
            .then((data: object) => {
                if (!cancelled) setAvatarFrameLottieData(data)
            })
            .catch(() => {
                if (!cancelled) setAvatarFrameLottieData(null)
            })
        return () => {
            cancelled = true
        }
    }, [stream.avatarFrame])

    // 讀取目前個人資料面板使用者的頭像框（若為 LOTTIE，支援非主播）
    useEffect(() => {
        let cancelled = false
        if (!profileUser) {
            setProfileAvatarFrameLottieData(null)
            return
        }
        const frame =
            profileUser.id === stream.userId
                ? stream.avatarFrame
                : userProfilesRef.current.get(profileUser.id)?.avatarFrame ?? null
        if (!frame || frame.type !== 'LOTTIE') {
            setProfileAvatarFrameLottieData(null)
            return
        }
        const url = resolveAvatarFrameAssetUrl(frame.image)
        if (!url) {
            setProfileAvatarFrameLottieData(null)
            return
        }
        // 主播使用全域 avatarFrameLottieData，不重複載入
        if (profileUser.id === stream.userId) {
            setProfileAvatarFrameLottieData(null)
            return
        }
        fetch(url)
            .then((res) => res.json())
            .then((data: object) => {
                if (!cancelled) setProfileAvatarFrameLottieData(data)
            })
            .catch(() => {
                if (!cancelled) setProfileAvatarFrameLottieData(null)
            })
        return () => {
            cancelled = true
        }
    }, [profileUser, stream.avatarFrame, stream.userId])

    const VIEWERS_LIST_DISMISS_THRESHOLD = 80
    const handleViewersListDragStart = useCallback((clientY: number) => {
        viewersListDragStart.current = { y: clientY, t: Date.now() }
    }, [])
    const handleViewersListDragMove = useCallback((clientY: number) => {
        const delta = clientY - viewersListDragStart.current.y
        if (delta > 0) setViewersListDragY(delta)
    }, [])
    const handleViewersListDragEnd = useCallback(() => {
        const delta = viewersListDragY
        const elapsed = Date.now() - viewersListDragStart.current.t
        const velocity = elapsed > 0 ? delta / elapsed : 0
        if (delta > VIEWERS_LIST_DISMISS_THRESHOLD || velocity > 0.5) {
            setViewersListDragY(0)
            setViewersListClosing(true)
        } else {
            setViewersListDragY(0)
        }
    }, [viewersListDragY])
    const handleViewersListClose = useCallback(() => {
        setViewersListDragY(0)
        setViewersListClosing(true)
    }, [])
    const handleViewersListTransitionEnd = useCallback(
        (e: React.TransitionEvent) => {
            if (e.propertyName !== 'transform') return
            if (viewersListClosing) {
                setViewersListOpen(false)
                setViewersListClosing(false)
                setViewersListDragY(0)
            }
        },
        [viewersListClosing]
    )
    const openViewersList = useCallback(() => {
        setViewersListOpen(true)
        setViewersListEntering(true)
    }, [])
    const handleViewersListRefresh = useCallback(() => {
        if (viewersListRefreshing) return
        setViewersListRefreshing(true)
        setTimeout(() => setViewersListRefreshing(false), 1500)
    }, [viewersListRefreshing])
    const openProfilePanel = useCallback((viewer: { id: string; name: string; avatar: string | null; badge: string | null }) => {
        setProfilePanelClosing(false)
        setProfilePanelEntering(true)
        setProfileUser(viewer)
    }, [])
    /** 從聊天訊息點擊使用者名稱，打開該用戶個人資料（沿用 /room 的 MessageList 介面） */
    const openChatViewerCard = useCallback(
        (_mode: 'desktop' | 'mobile', _e: React.MouseEvent<HTMLButtonElement>, data: ChatMessage | ParticipantItem) => {
            const id = (data as ChatMessage).userId ?? (data as ParticipantItem).id
            const name = (data as ChatMessage).author ?? (data as ParticipantItem).name
            if (id && name) {
                openProfilePanel({ id, name, avatar: null, badge: null })
            }
        },
        [openProfilePanel]
    )

    /** 從 @mention 點擊打開該用戶個人資料（沿用 /room 的 MessageList 介面） */
    const openMentionViewerCard = useCallback(
        (_mode: 'desktop' | 'mobile', _e: React.MouseEvent<HTMLButtonElement>, user: ParticipantItem) => {
            openProfilePanel({ id: user.id, name: user.name, avatar: null, badge: null })
        },
        [openProfilePanel]
    )
    /** 從觀眾列表點擊用戶：立即顯示個人資料，同時開始關閉列表 */
    const openProfileFromViewersList = useCallback(
        (viewer: { id: string; name: string; avatar: string | null; badge: string | null }) => {
            setProfileOpenedFromViewersList(true)
            setProfilePanelClosing(false)
            setProfilePanelEntering(true)
            setProfileUser(viewer)
            handleViewersListClose()
        },
        [handleViewersListClose]
    )
    const closeProfilePanel = useCallback(() => {
        const shouldReopenList = profileOpenedFromViewersList
        setProfilePanelClosing(true)
        if (shouldReopenList) {
            setProfileOpenedFromViewersList(false)
            openViewersList()
        }
    }, [profileOpenedFromViewersList, openViewersList])
    const handleProfilePanelTransitionEnd = useCallback(
        (e: React.TransitionEvent) => {
            if (e.propertyName !== 'transform') return
            if (profilePanelClosing) {
                setProfileUser(null)
                setProfilePanelClosing(false)
            }
        },
        [profilePanelClosing]
    )

    const handleGiftPanelDragStart = useCallback((clientY: number) => {
        giftPanelDragStart.current = { y: clientY, t: Date.now() }
    }, [])

    const handleGiftPanelDragMove = useCallback((clientY: number) => {
        const delta = clientY - giftPanelDragStart.current.y
        if (delta > 0) setGiftPanelDragY(delta)
    }, [])

    const handleGiftPanelDragEnd = useCallback(() => {
        const delta = giftPanelDragY
        const elapsed = Date.now() - giftPanelDragStart.current.t
        const velocity = elapsed > 0 ? delta / elapsed : 0
        if (delta > VIEWERS_LIST_DISMISS_THRESHOLD || velocity > 0.5) {
            setGiftPanelDragY(0)
            setGiftPanelClosing(true)
        } else {
            setGiftPanelDragY(0)
        }
    }, [giftPanelDragY])

    const handleGiftPanelTransitionEnd = useCallback(
        (e: React.TransitionEvent) => {
            if (e.propertyName !== 'transform') return
            if (giftPanelClosing) {
                setGiftPanelOpen(false)
                setGiftPanelClosing(false)
                setSelectedGiftId(null)
                setGiftPanelTab('featured')
                setGiftPanelDragY(0)
            }
        },
        [giftPanelClosing]
    )

    const onViewersPanelTouchStart = useCallback(
        (e: React.TouchEvent) => {
            handleViewersListDragStart(e.touches[0].clientY)
        },
        [handleViewersListDragStart]
    )
    const onViewersPanelTouchMove = useCallback(
        (e: React.TouchEvent) => {
            handleViewersListDragMove(e.touches[0].clientY)
        },
        [handleViewersListDragMove]
    )
    const onViewersPanelPointerDown = useCallback(
        (e: React.PointerEvent) => {
            if (e.pointerType === 'mouse') (e.target as HTMLElement).setPointerCapture?.(e.pointerId)
            handleViewersListDragStart(e.clientY)
        },
        [handleViewersListDragStart]
    )
    const onViewersPanelPointerMove = useCallback(
        (e: React.PointerEvent) => {
            if (viewersListDragStart.current.y === 0) return
            handleViewersListDragMove(e.clientY)
        },
        [handleViewersListDragMove]
    )
    const onViewersPanelPointerUp = useCallback(() => {
        handleViewersListDragEnd()
    }, [handleViewersListDragEnd])

    useEffect(() => {
        if (!isLiveStreamUi) return
        setLiveNowMs(Date.now())
        const id = window.setInterval(() => setLiveNowMs(Date.now()), 1000)
        return () => window.clearInterval(id)
    }, [isLiveStreamUi])

    useEffect(() => {
        setIsLiked(initialIsLiked)
        setLikeCount(stream.likes)
        setIsFollowing(initialIsFollowing)
    }, [initialIsLiked, initialIsFollowing, stream.likes])

    const toggleFollow = () => setIsFollowing((prev) => !prev)
    const toggleLike = useCallback(async () => {
        if (!currentUserId) return
        try {
            const res = await fetch(`/api/rooms/${encodeURIComponent(roomId)}/like`, { method: 'POST' })
            const json = await res.json().catch(() => null)
            const data = json?.data
            if (json?.ok && data) {
                setIsLiked(Boolean(data.isLiked))
                setLikeCount(Number(data.likes ?? 0))
                return
            }
        } catch {
            // ignore
        }
        setIsLiked((prev) => !prev)
        setLikeCount((prev) => (isLiked ? Math.max(0, prev - 1) : prev + 1))
    }, [roomId, currentUserId, isLiked])

    // 用 startedAt 是否為過去時間判斷（支援 Unix 秒 10 位、毫秒 13 位、或 ISO 字串）
    const startedAtMs = (() => {
        if (!stream?.startedAt) return null
        const s = String(stream.startedAt).trim()
        if (!s) return null
        if (/^\d{10}$/.test(s)) return parseInt(s, 10) * 1000 // Unix 秒
        if (/^\d{13}$/.test(s)) return parseInt(s, 10) // Unix 毫秒
        const t = Date.parse(s)
        return Number.isFinite(t) ? t : null
    })()
    const serverDuration = formatLiveDuration(stream.startedAt, liveNowMs)
    const streamHasStartedByServer = startedAtMs != null && startedAtMs <= Date.now()
    // 後端有開播時間用後端；沒有則用播放器偵測到 live 當下的時間（HLS 等會觸發 onDetectedLiveChange）
    const liveDuration =
        streamHasStartedByServer && serverDuration
            ? serverDuration
            : liveStartFallbackMsRef.current != null
              ? formatLiveDuration(liveStartFallbackMsRef.current, liveNowMs)
              : ''
    const streamHasStarted = streamHasStartedByServer || liveStartFallbackMsRef.current != null
    const liveDurationShort =
        isLiveStreamUi && streamHasStarted && liveDuration ? liveDuration.replace(/^00:/, '') : '00:00'

    // 送禮排行榜 placeholder（後續接 WS）
    const giftRanking = [
        { rank: 1, score: '3.8K', avatar: stream.avatar, color: 'amber' as const },
        { rank: 2, score: '507', avatar: null, color: 'slate' as const },
        { rank: 3, score: '4', avatar: null, color: 'amber' as const },
    ]

    const totalViewers = viewersCount

    const sendChat = useCallback(() => {
        const text = chatDraft.trim()
        if (!text || chatDisabled) return
        if (currentUserId && wsRef.current?.readyState === WebSocket.OPEN) {
            setMessages((prev) => [
                ...prev,
                {
                    id: `temp-${Date.now()}`,
                    author: currentUserDisplayName || '我',
                    text,
                    userId: currentUserId,
                    time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
                },
            ])
            wsRef.current.send(encryptWsPayload({ type: 'chat', text }))
            setChatDraft('')
        }
    }, [chatDraft, chatDisabled, currentUserId, currentUserDisplayName])

    const openGiftPanel = useCallback(() => {
        setGiftPanelOpen(true)
        setGiftPanelEntering(true)
    }, [])

    const closeGiftPanel = useCallback(() => {
        setGiftPanelClosing(true)
    }, [])

    const selectGiftTab = useCallback((tab: 'featured' | 'vip' | 'basic' | 'text') => {
        setGiftPanelTab(tab)
        setSelectedGiftId(null)
    }, [])

    const handleSelectGift = useCallback((giftId: string) => {
        setSelectedGiftId((prev) => (prev === giftId ? null : giftId))
    }, [])

    const handleSendGift = useCallback(() => {
        if (!selectedGiftId) return
        // TODO: 之後接後端送禮 API
        // 目前先關閉面板即可
        setGiftPanelClosing(true)
    }, [selectedGiftId])

    const spawnHearts = useCallback(() => {
        const ctx = heartsCtxRef.current
        const canvas = heartsCanvasRef.current
        if (!ctx || !canvas) return
        const images = heartImagesRef.current
        if (!images.size) return

        // 以按鈕中心為起點，讓愛心從按鈕飄出來
        const canvasRect = canvas.getBoundingClientRect()
        const btnRect = heartButtonRef.current?.getBoundingClientRect()
        let baseX = canvas.width * 0.8
        let baseY = canvas.height - 20
        if (btnRect) {
            const centerX = (btnRect.left + btnRect.right) / 2
            const centerY = (btnRect.top + btnRect.bottom) / 2
            baseX = centerX - canvasRect.left
            baseY = centerY - canvasRect.top
        }

        const particles: HeartParticle[] = []
        const count = 1
        for (let i = 0; i < count; i++) {
            const keys = Array.from(images.keys())
            const key = keys.length > 1 ? keys[Math.floor(Math.random() * keys.length)] : keys[0]
            const img = key ? images.get(key) ?? null : null
            if (!img) continue

            // 水平位置：以按鈕為中心，僅少量隨機，維持大致成柱狀
            const spreadX = Math.min(80, canvas.width * 0.25)
            const rawX = baseX + (Math.random() - 0.5) * spreadX
            const margin = 10
            const startX = Math.max(margin, Math.min(canvas.width - margin, rawX))

            // 垂直位置：少量隨機，主要靠時間差與路徑錯開
            const startY = baseY + (Math.random() - 0.5) * 16
            const duration = 3000 + Math.random() * 1800
            const animationType = 1 + (i % 8)
            const now = performance.now()
            particles.push({
                id: `heart-${Date.now()}-${Math.random()}`,
                x: startX,
                y: startY,
                startX,
                startY,
                image: img,
                animationType,
                startTime: now,
                duration,
                type: 'freeLike',
                opacity: 0,
                scale: 1.2,
                verticalProgress: 0,
            })
        }

        heartParticlesRef.current.push(...particles)

        if (heartsAnimFrameRef.current !== null) return

        const step = () => {
            const ctxInner = heartsCtxRef.current
            const canvasInner = heartsCanvasRef.current
            if (!ctxInner || !canvasInner) {
                heartsAnimFrameRef.current = null
                return
            }
            ctxInner.clearRect(0, 0, canvasInner.width, canvasInner.height)
            const next: HeartParticle[] = []
            heartParticlesRef.current.forEach((p) => {
                const now = performance.now()
                const elapsed = now - p.startTime
                const progress = Math.min(elapsed / p.duration, 1)
                if (progress >= 1) return

                const startBottomY = p.startY
                const topY = canvasInner.height * 0.15
                const y = startBottomY - (startBottomY - topY) * progress

                // 水平左右擺動：固定小幅度，讓路徑微微搖晃即可
                const swayBase = 8
                const sway =
                    Math.sin(progress * Math.PI * 2 * p.animationType) *
                    swayBase
                const x = p.startX + sway

                let opacity: number
                if (progress <= 0.2) {
                    opacity = progress / 0.2
                } else {
                    opacity = Math.max(0, 1 - (progress - 0.2) / 0.8)
                }

                // 尺寸：剛出發由小變大，之後再慢慢回到接近原本大小
                let scale: number
                const growPhase = 0.25
                if (progress <= growPhase) {
                    const p = progress / growPhase // 0 → 1
                    // 0.6 → 1.3（由小變大）
                    scale = 0.6 + (1.3 - 0.6) * p
                } else {
                    const p = (progress - growPhase) / (1 - growPhase) // 0 → 1
                    // 1.3 → 1.0（輕微縮回）
                    scale = 1.3 + (1.0 - 1.3) * p
                }

                const baseW = p.image.width
                const baseH = p.image.height
                const sizeFactor = 1
                const w = baseW * sizeFactor * scale
                const h = baseH * sizeFactor * scale

                ctxInner.save()
                ctxInner.globalAlpha = opacity
                ctxInner.drawImage(p.image, x - w / 2, y - h / 2, w, h)
                ctxInner.restore()

                next.push({
                    ...p,
                    x,
                    y,
                    opacity,
                    scale,
                    verticalProgress: progress,
                })
            })
            heartParticlesRef.current = next
            if (next.length === 0) {
                heartsAnimFrameRef.current = null
                return
            }
            heartsAnimFrameRef.current = requestAnimationFrame(step)
        }

        heartsAnimFrameRef.current = requestAnimationFrame(step)
    }, [])

    // 模仿參考實作：一次按下，排程多顆 heart 依序飛出（時間間隔拉大，讓縱向間距更自然）
    const triggerHearts = useCallback(() => {
        // 一次按下產生的 heart 數量稍微增加
        const comboCount = 12
        const baseDelay = 170
        for (let i = 0; i < comboCount; i++) {
            const jitter = Math.random() * 60 // 每顆稍微有一點點時間誤差
            setTimeout(() => {
                spawnHearts()
            }, i * baseDelay + jitter)
        }
    }, [spawnHearts])

    useEffect(() => {
        const ws = wsRef.current
        if (!ws || ws.readyState !== WebSocket.OPEN) return

        const pending = new Set<string>()

        displayMessages.forEach((msg) => {
            if (!msg.userId) return
            if (userProfilesRef.current.has(msg.userId)) return
            pending.add(msg.userId)
        })

        viewersList.forEach((viewer) => {
            if (!viewer.id) return
            if (userProfilesRef.current.has(viewer.id)) return
            pending.add(viewer.id)
        })

        pending.forEach((userId) => {
            try {
                ws.send(
                    encryptWsPayload({
                        type: 'get_user_profile',
                        userId,
                    })
                )
            } catch {
                // ignore
            }
        })
    }, [displayMessages, viewersList, stream.userId])

    const iconBtnClass = 'text-white hover:opacity-90 active:opacity-70 transition-opacity [text-shadow:0_1px_2px_rgba(0,0,0,0.8)]'
    const textShadow = 'drop-shadow(0 1px 2px rgba(0,0,0,0.9))'

    const { giftMessages, pinnedMessages, basicMessages, enterMessages, lastMessageId } = useMemo(() => {
        const gifts: ChatMessage[] = []
        const pinned: ChatMessage[] = []
        const basics: ChatMessage[] = []
        const enters: ChatMessage[] = []

        const isGiftMessage = (m: ChatMessage) => {
            if (m.gift) return true
            const text = (m.text || '').toLowerCase()
            return /送出禮物|贈送.*禮物|gift/.test(text)
        }

        displayMessages.forEach((m) => {
            if (isGiftMessage(m)) {
                gifts.push(m)
                return
            }
            if (m.isPinned) {
                pinned.push(m)
                return
            }
            if (isEnterRoomSystemMessage(m)) {
                enters.push(m)
                return
            }
            basics.push(m)
        })

        const last = displayMessages.length > 0 ? displayMessages[displayMessages.length - 1].id : null

        return {
            giftMessages: gifts,
            pinnedMessages: pinned,
            basicMessages: basics,
            enterMessages: enters,
            lastMessageId: last,
        }
    }, [displayMessages])

    const resolveChatUserAvatar = useCallback(
        (userId: string | null | undefined) => {
            if (!userId) return null
            const profile = userProfilesRef.current.get(userId)
            if (profile?.avatar) return profile.avatar
            if (userId === stream.userId && stream.avatar) return stream.avatar
            if (userId === currentUserId && currentUserAvatar) return currentUserAvatar
            return null
        },
        [stream.userId, stream.avatar, currentUserId, currentUserAvatar]
    )

    const renderChatMessageRow = (msg: ChatMessage) => {
        if (msg.gift) {
            return (
                <div
                    key={msg.id}
                    ref={(el) => {
                        if (el) {
                            messageRowRefs.current.set(msg.id, el)
                            if (msg.id === lastMessageId) {
                                lastMessageRef.current = el
                            }
                        }
                    }}
                    className="w-full py-0.5"
                >
                    <GiftChatMessage
                        message={msg}
                        streamerId={stream.userId}
                        getUserAvatar={resolveChatUserAvatar}
                        openChatViewerCard={openChatViewerCard}
                        openMentionViewerCard={openMentionViewerCard}
                        mode="mobile"
                        t={t}
                    />
                </div>
            )
        }

        if (msg.isSystem) {
            return (
                <div className="flex items-start gap-2" key={msg.id}>
                    <div className="w-8 flex-shrink-0" />
                    <div
                        className="text-base text-white/80"
                        style={{ wordBreak: 'break-all', textShadow: '0 1px 2px #000, 0 0 4px rgba(0,0,0,0.8)' }}
                    >
                        {renderMessageText(msg, openMentionViewerCard, 'mobile', t, undefined, currentUserId, false, stream.userId)}
                    </div>
                </div>
            )
        }

        const effective = getEffectiveUserType(msg)
        const roleItem = { ...msg, userType: effective }
        const roleBadge = getUserRoleBadge(roleItem, stream.userId)

        let avatarSrc: string | null = null
        if (msg.userId) {
            const profile = userProfilesRef.current.get(msg.userId)
            if (profile?.avatar) {
                avatarSrc = profile.avatar
            } else if (msg.userId === stream.userId && stream.avatar) {
                avatarSrc = stream.avatar
            } else if (msg.userId === currentUserId && currentUserAvatar) {
                avatarSrc = currentUserAvatar
            }
        }

        return (
            <div
                key={msg.id}
                ref={(el) => {
                    if (el) {
                        messageRowRefs.current.set(msg.id, el)
                        if (msg.id === lastMessageId) {
                            lastMessageRef.current = el
                        }
                    }
                }}
                className="px-2 py-1 rounded-full bg-black/50 flex items-center gap-2"
            >
                <div
                    className="flex-shrink-0 w-8 h-8 rounded-full overflow-hidden bg-gray/90 flex items-center justify-center cursor-pointer"
                    onClick={(e) =>
                        openChatViewerCard(
                            'mobile',
                            e as unknown as React.MouseEvent<HTMLButtonElement>,
                            { id: msg.userId, name: msg.author } as ParticipantItem
                        )
                    }
                >
                    {avatarSrc ? (
                        <Image
                            src={avatarSrc}
                            alt={msg.author || ''}
                            width={32}
                            height={32}
                            className="w-full h-full object-cover"
                            unoptimized
                        />
                    ) : (
                        <FaUser className="w-4 h-4 text-white/70" />
                    )}
                </div>
                <div className="min-w-0 flex-1">
                    <div className="flex items-center gap-0.5 mb-0.5">
                        {roleBadge ? <span className="flex-shrink-0">{roleBadge}</span> : null}
                        <button
                            type="button"
                            onClick={(e) => openChatViewerCard('mobile', e, msg)}
                            className={`text-sm font-semibold truncate ${getUserNameColor(roleItem, stream.userId)}`}
                            style={{ textShadow: '0 1px 2px #000, 0 0 4px rgba(0,0,0,0.8)' }}
                        >
                            {msg.author ?? ''}
                        </button>
                    </div>
                    <div
                        className="text-[13px] leading-snug text-white break-words"
                        style={{ textShadow: '0 1px 2px #000, 0 0 4px rgba(0,0,0,0.8)' }}
                    >
                        {renderMessageText(msg, openMentionViewerCard, 'mobile', t, undefined, currentUserId, false, stream.userId)}
                    </div>
                </div>
            </div>
        )
    }

    // 新「聊天訊息」由下往上推上來（不包含純進入房間系統訊息）——對應 addUser()
    useEffect(() => {
        const container = messagesContainerRef.current
        if (!container) {
            prevMessageIdsRef.current = displayMessages.filter((m) => !isEnterRoomSystemMessage(m)).map((m) => m.id)
            return
        }

        const prevIds = prevMessageIdsRef.current
        const newIds = displayMessages.filter((m) => !isEnterRoomSystemMessage(m)).map((m) => m.id)

        // 判斷是否真的有新訊息（避免純 re-render 也觸發動畫）
        const hasNew =
            newIds.length > prevIds.length ||
            newIds.some((id, index) => prevIds[index] !== id)

        if (!hasNew || !isAtBottom) {
            prevMessageIdsRef.current = newIds
            return
        }

        // 將整個 message-stack 由下往上推（模仿 demo 的 stack transform）
        container.style.transition = 'none'
        let offset = 0
        const lastEl = lastMessageRef.current
        if (lastEl) {
            try {
                // 新訊息實際高度 + margin-top:8px
                const h = lastEl.offsetHeight
                if (Number.isFinite(h)) {
                    offset = h + 8
                }
            } catch {
                // ignore
            }
        }
        container.style.transform = `translateY(${offset}px)`

        requestAnimationFrame(() => {
            container.style.transition = 'transform .25s ease'
            container.style.transform = 'translateY(0)'
        })

        prevMessageIdsRef.current = newIds
    }, [displayMessages, isAtBottom])

    // 進入房間訊息：新訊息由下往上蓋到舊訊息上，然後舊訊息淡出消失
    useEffect(() => {
        if (!enterMessages.length) return
        const latest = enterMessages[enterMessages.length - 1]
        // 同一則進場訊息不要重複觸發動畫
        if (latest.id === lastEnterMessageIdRef.current) return
        lastEnterMessageIdRef.current = latest.id
        setPrevEnterMessage(activeEnterMessage)
        setActiveEnterMessage(latest)
    }, [enterMessages, activeEnterMessage])

    useEffect(() => {
        const newEl = enterBannerRef.current
        if (!newEl || !activeEnterMessage) return

        // 新進入房間訊息：由下往上推上來（初始 translateY(100%)，對應 demo 的 .join）
        newEl.style.transition = 'none'
        newEl.style.transform = 'translateY(100%)'

        requestAnimationFrame(() => {
            newEl.style.transition = 'transform .25s ease'
            newEl.style.transform = 'translateY(0)'
        })

        const oldEl = prevEnterBannerRef.current
        if (oldEl && prevEnterMessage) {
            // 舊進入房間訊息：被新訊息蓋到後淡出（對應 demo 的 oldJoin opacity 動畫）
            oldEl.style.transition = 'opacity .35s ease'
            oldEl.style.opacity = '0'
            const timer = window.setTimeout(() => {
                setPrevEnterMessage(null)
            }, 250)
            return () => {
                window.clearTimeout(timer)
            }
        }
    }, [activeEnterMessage, prevEnterMessage])

    return (
        <div className="h-[100dvh] min-h-[100dvh] relative bg-black overflow-hidden select-none">
            {/* 背景：直播畫面（撐滿高度） */}
            <div className="absolute inset-0 min-h-full">
                <RoomVideoPlayer
                    src={playbackSrc}
                    isLive={isLiveStreamUi}
                    onDetectedLiveChange={(isLive) => {
                        setPlayerDetectedLive(isLive)
                        if (isLive && liveStartFallbackMsRef.current === null) {
                            liveStartFallbackMsRef.current = Date.now()
                        }
                    }}
                    onReady={(el) => {
                        videoElRef.current = el
                        try {
                            const v = typeof el.volume === 'number' ? el.volume : 1
                            const m = !!el.muted
                            setPlayerVolume(v)
                            setPlayerMuted(m)
                            if (!m && v > 0) lastVolumeRef.current = v
                        } catch {
                            // ignore
                        }
                    }}
                    className="w-full h-full min-h-full object-contain"
                    showChatReopenButton={false}
                    isTheater={false}
                    fillHeight
                    disableUserControls
                />
            </div>

            {/* 左上：主播區塊 + 喜歡數/直播時長膠囊 */}
            <div
                className="absolute top-4 left-1 z-[100] flex flex-col gap-2 pointer-events-auto"
                style={{ paddingLeft: 'max(env(safe-area-inset-left), 0.375rem)' }}
            >
                {/* 主播區塊（固定尺寸 + 膠囊圓角，點擊頭像/名稱顯示個人資料） */}
                <div className="w-[220px] h-14 flex items-center gap-2 px-2 py-1.5 rounded-full bg-black/85 border border-white/15 shadow-lg">
                    <div className="relative flex items-center gap-2 min-w-0 flex-1">
                        <button
                            type="button"
                            onClick={() =>
                                openProfilePanel({
                                    id: stream.userId,
                                    name: stream.channelName,
                                    avatar: stream.avatar ?? null,
                                    badge: null,
                                })
                            }
                            className="flex items-center gap-2 min-w-0 flex-1 text-left"
                            aria-label={`查看 ${stream.channelName} 的個人資料`}
                        >
                            <div className="relative flex-shrink-0 flex flex-col justify-start items-stretch">
                                <div className="relative flex-shrink-0 bg-[#f7f7f7] rounded-full border border-black/5 w-9 h-9 flex items-center justify-center overflow-hidden">
                                    <div className="relative w-full h-full rounded-full overflow-hidden flex items-center justify-center">
                                        {stream.avatar ? (
                                            <Image
                                                src={stream.avatar}
                                                alt={stream.channelName}
                                                fill
                                                className="object-cover"
                                                unoptimized
                                            />
                                        ) : (
                                            <FaUser className="w-5 h-5 text-gray-500" />
                                        )}
                                    </div>
                                </div>
                                {stream.avatarFrame && (
                                    <div className="absolute -top-[10px] -left-[7px] w-[50px] h-[50px] z-10">
                                        {stream.avatarFrame.type === 'LOTTIE' && avatarFrameLottieData && (
                                            <Lottie
                                                animationData={avatarFrameLottieData}
                                                loop
                                                autoplay
                                                style={{ width: '100%', height: '100%' }}
                                            />
                                        )}
                                        {stream.avatarFrame.type === 'IMG' && (
                                            <Image
                                                src={resolveAvatarFrameAssetUrl(stream.avatarFrame.image)}
                                                alt={stream.channelName}
                                                fill
                                                className="object-contain"
                                                unoptimized
                                            />
                                        )}
                                    </div>
                                )}
                            </div>
                            <div className="min-w-0 flex-1 space-y-0.5">
                                <div className="flex items-center gap-1 min-w-0">
                                    <span
                                        className="text-white font-medium text-sm truncate leading-tight"
                                        style={{ textShadow }}
                                    >
                                        {stream.channelName}
                                    </span>
                                <VipBadgeLottie
                                    vipLevel={stream.vipLevel}
                                    innerRef={vipBadgeRef}
                                    className="inline-flex items-center flex-shrink-0 vip-badge-container pointer-events-none"
                                    style={{ width: 60, height: 18 }}
                                />
                                </div>
                                <p className="text-white/70 text-xs truncate leading-tight">
                                    {stream.title || '直播中'}
                                </p>
                            </div>
                        </button>
                    </div>
                    <button
                        type="button"
                        onClick={() => {
                            if (!isFollowing) triggerFlyingStars()
                            toggleFollow()
                        }}
                        className="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center bg-white text-gray-900 hover:bg-white/90 active:bg-white/80 transition-colors relative"
                        aria-label={isFollowing ? '取消追蹤' : '追蹤'}
                    >
                        <span className="relative inline-flex items-center justify-center w-8 h-8">
                            {flyingStars.map((id, index) => {
                                const animations = [
                                    'animate-heart-fly-1',
                                    'animate-heart-fly-2',
                                    'animate-heart-fly-3',
                                    'animate-heart-fly-4',
                                    'animate-heart-fly-5',
                                ]
                                return (
                                    <span
                                        key={id}
                                        className={`absolute inset-0 flex items-center justify-center pointer-events-none text-yellow-300 drop-shadow ${animations[index] ?? animations[0]}`}
                                    >
                                        <FaStar className="w-4 h-4" />
                                    </span>
                                )
                            })}
                            {isFollowing ? (
                                <FaStar className="w-4 h-4 text-yellow-500" />
                            ) : (
                                <FiStar className="w-4 h-4" />
                            )}
                        </span>
                    </button>
                </div>

                {/* 喜歡數 & 直播時長膠囊 */}
                <div className="flex items-center gap-2 relative">
                    <button
                        type="button"
                        onClick={toggleLike}
                        className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-black/85 border border-white/15 text-white text-sm hover:opacity-90 transition-opacity"
                    >
                        {isLiked ? (
                            <FaHeart className="w-4 h-4 text-red-400 flex-shrink-0" />
                        ) : (
                            <FaRegHeart className="w-4 h-4 flex-shrink-0" />
                        )}
                        <span>{likeCount}</span>
                    </button>
                    <span className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-black/85 border border-white/15 text-white text-sm">
                        <HiClock className="w-4 h-4 flex-shrink-0" />
                        <span>{liveDurationShort}</span>
                    </span>
                </div>
            </div>

            {/* 右上：送禮排行榜 + 觀眾數。窄螢幕（與主播資訊卡到）時，送禮排行改顯示在觀眾按鈕下方且只顯示第一名金色 */}
            <div
                className="absolute top-4 right-1 z-[100] flex flex-col items-end gap-1.5 pointer-events-auto"
                style={{ paddingRight: 'env(safe-area-inset-right)' }}
            >
                {/* 第一行：送禮前三名（寬螢幕顯示）+ 觀眾數按鈕 */}
                <div className="h-14 flex items-center gap-1.5">
                    {/* 送禮前三名：僅在寬螢幕顯示，避免與左側主播區重疊 */}
                    <div className="hidden min-[431px]:flex items-center gap-1.5">
                        {giftRanking.map(({ rank, score, avatar, color }) => (
                            <div
                                key={rank}
                                className="flex flex-col items-center shrink-0"
                                title={`送禮第${rank}名`}
                            >
                                <div className="relative">
                                    <div
                                        className={`w-11 h-8 rounded-t-full overflow-hidden border border-b-0 ${
                                            color === 'amber' && rank === 1
                                                ? 'bg-amber-400/90 border-amber-300/50'
                                                : color === 'slate'
                                                  ? 'bg-slate-400/90 border-slate-300/50'
                                                  : 'bg-amber-700/90 border-amber-600/50'
                                        }`}
                                    >
                                        {avatar ? (
                                            <Image
                                                src={avatar}
                                                alt=""
                                                width={44}
                                                height={32}
                                                className="w-full h-full object-cover"
                                                unoptimized
                                            />
                                        ) : (
                                            <div className="w-full h-full bg-white/10" />
                                        )}
                                    </div>
                                    <span className="absolute -top-2 -right-3 flex items-center justify-center pointer-events-none">
                                        <Image
                                            src={
                                                rank === 1
                                                    ? '/images/room/wing_gold.png'
                                                    : rank === 2
                                                      ? '/images/room/wing_silver.png'
                                                      : '/images/room/wing_bronze.png'
                                            }
                                            alt=""
                                            width={32}
                                            height={32}
                                            className="w-8 h-8 object-contain"
                                            unoptimized
                                        />
                                    </span>
                                </div>
                                <span
                                    className={`px-2 rounded-sm text-white text-xs font-medium min-w-[50px] text-center border ${
                                        rank === 1
                                            ? 'bg-amber-500 border-amber-600'
                                            : rank === 2
                                              ? 'bg-slate-500 border-slate-600'
                                              : 'bg-amber-700 border-amber-800'
                                    }`}
                                >
                                    {score}
                                </span>
                            </div>
                        ))}
                    </div>
                    {/* 觀眾數（點擊展開觀眾列表） */}
                    <button
                        type="button"
                        onClick={openViewersList}
                        className="flex flex-col items-center justify-center w-12 h-12 rounded-full border-2 border-white/30 text-white shrink-0 gap-1.5 py-2 hover:opacity-90 active:opacity-80 transition-opacity"
                        title="觀眾數"
                    >
                        <FaUser className="w-3 h-3 flex-shrink-0" />
                        <span className="text-xs font-medium leading-tight">
                            {viewersCount > 0 ? viewersCount : totalViewers}
                        </span>
                    </button>
                </div>
                {/* 窄螢幕：送禮第一名（金色）顯示在觀眾按鈕下方，僅頭像 + 金框 + 翅膀圖示（完整圓形，無下方裁切） */}
                <div className="flex min-[431px]:hidden flex-col items-center shrink-0" title="送禮第1名">
                    {giftRanking.filter((r) => r.rank === 1).map(({ rank, avatar }) => (
                        <div key={rank} className="flex flex-col items-center shrink-0">
                            <div className="relative w-11 h-11">
                                <div className="absolute inset-0 rounded-full overflow-hidden border-1 bg-amber-400/20" style={{ borderColor: 'rgb(255,182,0)', boxShadow: '0 0 0 2px rgb(255,182,0)' }}>
                                    {avatar ? (
                                        <Image
                                            src={avatar}
                                            alt=""
                                            fill
                                            className="object-cover"
                                            unoptimized
                                        />
                                    ) : (
                                        <div className="w-full h-full bg-white/10" />
                                    )}
                                </div>
                                <span className="absolute -top-1.5 -right-2.5 flex items-center justify-center pointer-events-none z-10">
                                    <Image
                                        src="/images/room/wing_gold.png"
                                        alt=""
                                        width={28}
                                        height={28}
                                        className="w-7 h-7 object-contain"
                                        unoptimized
                                    />
                                </span>
                            </div>
                        </div>
                    ))}
                </div>
            </div>

            {/* 左中下：聊天內容 —— 1:1 對應 visible-area / message-stack */}
            <div
                className="absolute left-3 right-20 bottom-[4.5rem] z-[100] pointer-events-none dark"
                style={{
                    // safe-area 這種環境變數用 Tailwind 不好表達，保留 style
                    paddingLeft: 'env(safe-area-inset-left)',
                }}
            >
                {/* visible-area */}
                <div className="relative h-[300px] max-h-[25vh] bg-transparent overflow-hidden block">
                    {/* message-stack */}
                    <div
                        ref={messagesContainerRef}
                        className="pointer-events-auto w-full space-y-1.5 max-h-full overflow-y-auto scrollbar-none transform-gpu translate-y-0"
                        style={{
                            // 這兩個是隱藏捲軸的瀏覽器私有屬性，Tailwind 無法表達，保留 style
                            scrollbarWidth: 'none',
                            msOverflowStyle: 'none',
                        }}
                        onScroll={(e) => {
                            const el = e.currentTarget
                            const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 50
                            setIsAtBottom(nearBottom)
                            if (nearBottom) {
                                setHasNewMessagesBelow(false)
                            }
                        }}
                    >
                        {/* 第一層：送禮訊息 */}
                        {giftMessages.map((msg) => renderChatMessageRow(msg))}
                        {/* 第二層：主播置頂訊息 */}
                        {pinnedMessages.map((msg) => renderChatMessageRow(msg))}
                        {/* 第三層：一般聊天訊息 */}
                        {basicMessages.map((msg) => renderChatMessageRow(msg))}
                    </div>
                </div>
                {/* 新訊息提示：懸浮於訊息區與輸入列之間（absolute，不推擠訊息） */}
                {!isAtBottom && hasNewMessagesBelow && (
                    <div className="absolute left-0 right-0 bottom-0 flex justify-center pointer-events-auto z-10">
                        <button
                            type="button"
                            onClick={() => {
                                setIsAtBottom(true)
                                const el = messagesContainerRef.current
                                if (el) {
                                    el.scrollTop = el.scrollHeight
                                }
                                setHasNewMessagesBelow(false)
                            }}
                            className="px-4 py-1.5 rounded-full bg-white/90 text-gray-900 text-xs font-medium shadow-sm hover:bg-white active:bg-gray-100 transition-colors inline-flex items-center gap-1 pointer-events-auto"
                        >
                            <span>新訊息</span>
                            <FiChevronDown className="w-3.5 h-3.5" />
                        </button>
                    </div>
                )}
                {/* 進入房間訊息：新訊息由下往上推上來蓋到舊訊息上，舊訊息淡出消失 */}
                {(activeEnterMessage || prevEnterMessage) && (
                    <div className="mt-1.5 pointer-events-auto h-10 relative overflow-hidden flex items-center justify-start">
                        {prevEnterMessage && (
                            <div ref={prevEnterBannerRef} className="absolute bottom-0 left-0">
                                <div
                                    className="inline-flex items-center px-3 py-1.5 rounded-lg bg-black/75 text-xs text-white"
                                    style={{ textShadow: '0 1px 2px #000, 0 0 4px rgba(0,0,0,0.8)' }}
                                >
                                    {renderMessageText(prevEnterMessage, openMentionViewerCard, 'mobile', t, undefined, currentUserId)}
                                </div>
                            </div>
                        )}
                        {activeEnterMessage && (
                            <div ref={enterBannerRef} className="absolute bottom-0 left-0">
                                <div
                                    className="inline-flex items-center px-3 py-1.5 rounded-lg bg-black/75 text-xs text-white"
                                    style={{ textShadow: '0 1px 2px #000, 0 0 4px rgba(0,0,0,0.8)' }}
                                >
                                    {renderMessageText(activeEnterMessage, openMentionViewerCard, 'mobile', t, undefined, currentUserId)}
                                </div>
                            </div>
                        )}
                    </div>
                )}
            </div>

            {/* 左下：聊天輸入框、送禮（聊天預設為輸入框，撐滿寬度） */}
            <div
                className="absolute bottom-4 left-3 right-3 z-[100] flex flex-col gap-2 pointer-events-auto"
                style={{
                    paddingLeft: 'env(safe-area-inset-left)',
                    paddingRight: 'env(safe-area-inset-right)',
                    paddingBottom: 'env(safe-area-inset-bottom)',
                }}
            >
                <div className="flex items-center gap-2">
                    <button
                        type="button"
                        onClick={openGiftPanel}
                        className={`${iconBtnClass} flex-shrink-0 flex items-center justify-center w-11 h-11 rounded-full bg-transparent cursor-pointer`}
                        aria-label="送禮"
                    >
                        <Lottie
                            animationData={giftLottieData}
                            loop={false}
                            style={{ width: 44, height: 44 }}
                        />
                    </button>
                    <div className="flex-1 flex items-center h-[40px] rounded-full bg-white dark:bg-neutral-900 text-gray-900 dark:text-white shadow px-3">
                        <input
                            type="text"
                            value={chatDraft}
                            onChange={(e) => setChatDraft(e.target.value)}
                            onKeyDown={(e) => e.key === 'Enter' && sendChat()}
                            placeholder={chatDisabled ? '聊天已關閉' : (currentUserId ? '聊天' : '登入後可聊天')}
                            disabled={!currentUserId || chatDisabled}
                            className="w-full border-0 bg-transparent outline-none text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 text-sm leading-none"
                        />
                        <button
                            type="button"
                            onClick={sendChat}
                            disabled={!currentUserId || chatDisabled || !chatDraft.trim()}
                            className="ml-2 flex-shrink-0 w-7 h-7 rounded-full flex items-center justify-center text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-neutral-800 hover:text-gray-900 dark:hover:text-white disabled:opacity-40 disabled:pointer-events-none transition-colors"
                            aria-label="送出"
                        >
                            <FiSend className="w-4 h-4" />
                        </button>
                    </div>
                    {/* 音量條：背景與愛心按鈕一致 */}
                    <div className="flex items-center flex-shrink-0 max-h-12">
                        <div className="relative flex items-center gap-1 h-11 flex-1 min-w-0 max-w-[100px] rounded-[50px] bg-white dark:bg-neutral-900 shadow">
                            <button
                                type="button"
                                onClick={togglePlayerMute}
                                className="relative z-10 flex items-center justify-center min-w-9 h-11 bg-transparent border-none outline-none text-gray-900 dark:text-white cursor-pointer"
                                aria-label={playerMuted || playerVolume === 0 ? '開啟聲音' : '關閉聲音'}
                            >
                                {playerMuted || playerVolume === 0 ? (
                                    <FaVolumeMute className="w-5 h-5" />
                                ) : playerVolume < 0.5 ? (
                                    <FaVolumeDown className="w-5 h-5" />
                                ) : (
                                    <FaVolumeUp className="w-5 h-5" />
                                )}
                            </button>
                            <div className="relative z-10 flex items-center min-w-0 pr-3 flex-1">
                                <input
                                    type="range"
                                    min={0}
                                    max={100}
                                    value={Math.round((playerMuted ? 0 : playerVolume) * 100)}
                                    style={{ ['--p' as any]: `${Math.round((playerMuted ? 0 : playerVolume) * 100)}%` }}
                                    onChange={(e) => applyPlayerVolume(e.currentTarget.valueAsNumber / 100)}
                                    aria-label="音量"
                                    className="flex-grow min-w-0 bg-transparent cursor-pointer appearance-none py-1
                                      [&::-webkit-slider-runnable-track]:h-[2px] [&::-webkit-slider-runnable-track]:rounded-[12px]
                                      [&::-webkit-slider-runnable-track]:bg-[linear-gradient(to_right,rgba(75,85,99,0.4)_0,rgba(75,85,99,0.4)_var(--p),rgb(75,85,99)_var(--p),rgb(75,85,99)_100%)]
                                      dark:[&::-webkit-slider-runnable-track]:bg-[linear-gradient(to_right,#fff_0,#fff_var(--p),rgba(255,255,255,0.3)_var(--p),rgba(255,255,255,0.3)_100%)]
                                      [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-gray-600 [&::-webkit-slider-thumb]:mt-[-7px] dark:[&::-webkit-slider-thumb]:bg-white
                                      [&::-moz-range-track]:h-[2px] [&::-moz-range-track]:rounded-[12px]
                                      [&::-moz-range-track]:bg-[linear-gradient(to_right,rgba(75,85,99,0.4)_0,rgba(75,85,99,0.4)_var(--p),rgb(75,85,99)_var(--p),rgb(75,85,99)_100%)]
                                      dark:[&::-moz-range-track]:bg-[linear-gradient(to_right,#fff_0,#fff_var(--p),rgba(255,255,255,0.3)_var(--p),rgba(255,255,255,0.3)_100%)]
                                      [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-gray-600 [&::-moz-range-thumb]:border-0 dark:[&::-moz-range-thumb]:bg-white"
                                />
                            </div>
                        </div>
                    </div>
                    <button
                        ref={heartButtonRef}
                        type="button"
                        onClick={triggerHearts}
                        className="w-11 h-11 rounded-full bg-white dark:bg-neutral-900 flex items-center justify-center text-red-600 hover:bg-pink-50 dark:hover:bg-neutral-800 active:bg-pink-100 dark:active:bg-neutral-700 transition-colors shadow flex-shrink-0"
                        aria-label="喜歡"
                    >
                        <FaHeart className="w-5 h-5" />
                    </button>
                </div>
            </div>

            {giftPanelOpen && (
                <>
                    <div
                        className="fixed inset-0 z-[280] bg-black/60"
                        onClick={closeGiftPanel}
                        aria-hidden
                    />
                    <div
                        className="fixed inset-x-0 bottom-0 z-[290] max-h-[80vh] bg-[#141416]/90 text-white rounded-t-2xl shadow-2xl pb-[env(safe-area-inset-bottom)] transition-transform duration-300 ease-out"
                        onClick={(e) => e.stopPropagation()}
                        style={{
                            transform: giftPanelClosing
                                ? 'translateY(100%)'
                                : giftPanelEntering
                                  ? 'translateY(100%)'
                                  : `translateY(${giftPanelDragY}px)`,
                        }}
                        onTransitionEnd={handleGiftPanelTransitionEnd}
                    >
                        {/* 把手條 + Tabs */}
                        <div
                            className="pt-3 touch-none cursor-grab active:cursor-grabbing select-none"
                            onTouchStart={(e) => handleGiftPanelDragStart(e.touches[0].clientY)}
                            onTouchMove={(e) => handleGiftPanelDragMove(e.touches[0].clientY)}
                            onTouchEnd={handleGiftPanelDragEnd}
                            onTouchCancel={handleGiftPanelDragEnd}
                            onPointerDown={(e: React.PointerEvent) => {
                                if (e.pointerType === 'mouse') (e.target as HTMLElement).setPointerCapture?.(e.pointerId)
                                handleGiftPanelDragStart(e.clientY)
                            }}
                            onPointerMove={(e: React.PointerEvent) => {
                                if (giftPanelDragStart.current.y === 0) return
                                handleGiftPanelDragMove(e.clientY)
                            }}
                            onPointerUp={handleGiftPanelDragEnd}
                            onPointerCancel={handleGiftPanelDragEnd}
                        >
                            <div className="flex justify-center pb-2">
                                <span className="w-10 h-1 rounded-full bg-white/40" />
                            </div>
                        </div>
                        <div className="px-4 pb-2 flex items-center justify-between">
                            <div className="flex gap-4 text-sm font-medium">
                                {[
                                    { id: 'featured' as const, label: '優選專屬' },
                                    { id: 'vip' as const, label: 'VIP專屬' },
                                    { id: 'basic' as const, label: '基本' },
                                    { id: 'text' as const, label: '文字' },
                                ].map((tab) => (
                                    <button
                                        key={tab.id}
                                        type="button"
                                        onClick={() => selectGiftTab(tab.id)}
                                        className={`relative pb-1 ${
                                            giftPanelTab === tab.id
                                                ? 'text-white'
                                                : 'text-white/50'
                                        }`}
                                    >
                                        {tab.label}
                                        {giftPanelTab === tab.id && (
                                            <span className="absolute left-0 right-0 -bottom-0.5 h-0.5 rounded-full bg-orange-400" />
                                        )}
                                    </button>
                                ))}
                            </div>
                        </div>

                        {/* Gifts Grid */}
                        {(() => {
                            const allGifts: {
                                id: string
                                tab: 'featured' | 'vip' | 'basic' | 'text'
                                name: string
                                price: number
                                badge?: string
                            }[] = [
                                { id: 'top10', tab: 'featured', name: 'Top chat x10', price: 10, badge: 'Top' },
                                { id: 'mic', tab: 'featured', name: '麥克風', price: 5 },
                                { id: 'love', tab: 'featured', name: 'LOVE', price: 77 },
                                { id: 'cake', tab: 'featured', name: '蛋糕', price: 200 },
                                { id: 'vip-crown', tab: 'vip', name: 'VIP 皇冠', price: 520 },
                                { id: 'vip-fire', tab: 'vip', name: 'VIP 火焰', price: 999 },
                                { id: 'flower', tab: 'basic', name: '鮮花', price: 3 },
                                { id: 'thumb', tab: 'basic', name: '讚', price: 1 },
                                { id: 'text-nice', tab: 'text', name: 'Nice', price: 2 },
                                { id: 'text-love', tab: 'text', name: 'Love you', price: 4 },
                            ]
                            const visible = allGifts.filter((g) => g.tab === giftPanelTab)
                            return (
                                <div className="px-4 pt-3 pb-2 h-[35vh] overflow-y-auto">
                                    <div className="grid grid-cols-4 gap-3">
                                        {visible.map((gift) => {
                                            const selected = selectedGiftId === gift.id
                                            return (
                                                <button
                                                    key={gift.id}
                                                    type="button"
                                                    onClick={() => handleSelectGift(gift.id)}
                                                    className={`relative flex flex-col items-center gap-1 rounded-2xl px-2 py-2.5 bg-white/5 border ${
                                                        selected ? 'border-orange-400 bg-orange-500/15' : 'border-white/10'
                                                    }`}
                                                >
                                                    {gift.badge && (
                                                        <span className="absolute -top-1 left-1 rounded-full bg-orange-500 px-1.5 py-0.5 text-[10px] font-semibold">
                                                            {gift.badge}
                                                        </span>
                                                    )}
                                                    <div className="w-10 h-10 rounded-full bg-white/10 flex items-center justify-center text-orange-300">
                                                        <FiGift className="w-5 h-5" />
                                                    </div>
                                                    <span className="text-[11px] truncate max-w-full">{gift.name}</span>
                                                    <span className="text-[11px] text-orange-300 font-semibold">
                                                        {gift.price}
                                                    </span>
                                                </button>
                                            )
                                        })}
                                    </div>
                                </div>
                            )
                        })()}

                        {/* Bottom send bar */}
                        <div className="px-4 py-3 border-t border-white/10 flex items-center justify-between text-xs">
                            <div className="flex flex-col gap-0.5 text-white/70">
                                <span>
                                    已選禮物：
                                    {selectedGiftId
                                        ? '已選擇'
                                        : '尚未選擇'}
                                </span>
                                <span className="text-white/40">
                                    之後可接後端 coin 餘額與送禮邏輯
                                </span>
                            </div>
                            <button
                                type="button"
                                disabled={!selectedGiftId}
                                onClick={handleSendGift}
                                className="px-5 py-2 rounded-full bg-orange-500 text-white text-sm font-semibold disabled:bg-white/20 disabled:text-white/50 disabled:cursor-not-allowed"
                            >
                                送出
                            </button>
                        </div>
                    </div>
                </>
            )}

            {/* 觀眾列表底部面板（點擊觀眾數展開） */}
            {viewersListOpen && (
                <>
                    <div
                        className="fixed inset-0 z-[300] bg-black/50"
                        onClick={handleViewersListClose}
                        aria-hidden
                    />
                    <div
                        className={`fixed inset-x-0 bottom-0 z-[300] flex flex-col h-[92vh] max-h-[92vh] bg-white dark:bg-[#141416] rounded-t-2xl shadow-2xl dark:shadow-[0_-4px_24px_rgba(0,0,0,0.5)] pb-[env(safe-area-inset-bottom)] ${viewersListDragY > 0 ? '' : 'transition-transform duration-300 ease-out'}`}
                        style={{
                            transform: viewersListClosing
                                ? 'translateY(100%)'
                                : viewersListEntering
                                  ? 'translateY(100%)'
                                  : `translateY(${viewersListDragY}px)`,
                        }}
                        onTransitionEnd={handleViewersListTransitionEnd}
                        role="dialog"
                        aria-label="觀眾列表"
                    >
                        {/* 可下拉關閉區：把手 + Tab 列（拖曳此區可關閉用戶列表） */}
                        <div
                            className="touch-none cursor-grab active:cursor-grabbing select-none"
                            onTouchStart={onViewersPanelTouchStart}
                            onTouchMove={onViewersPanelTouchMove}
                            onTouchEnd={onViewersPanelPointerUp}
                            onTouchCancel={onViewersPanelPointerUp}
                            onPointerDown={onViewersPanelPointerDown}
                            onPointerMove={onViewersPanelPointerMove}
                            onPointerUp={onViewersPanelPointerUp}
                            onPointerCancel={onViewersPanelPointerUp}
                        >
                            <div className="flex justify-center py-3">
                                <span className="w-10 h-1 rounded-full bg-gray-300 dark:bg-white/30" />
                            </div>
                            {/* 標題列：聽眾 | FAN 排行榜 */}
                            <div className="px-4">
                                <div className="flex gap-6">
                                    <button
                                        type="button"
                                        onClick={() => setViewersListTab('listeners')}
                                        className="relative font-bold text-lg text-gray-900 dark:text-white"
                                    >
                                        聽眾
                                        {viewersListTab === 'listeners' && (
                                            <span className="absolute left-0 right-0 -bottom-1 h-0.5 rounded-full bg-orange-500" />
                                        )}
                                    </button>
                                    <button
                                        type="button"
                                        onClick={() => setViewersListTab('fan')}
                                        className="relative font-bold text-lg text-gray-900 dark:text-white"
                                    >
                                        FAN 排行榜
                                        {viewersListTab === 'fan' && (
                                            <span className="absolute left-0 right-0 -bottom-1 h-0.5 rounded-full bg-orange-500" />
                                        )}
                                    </button>
                                </div>
                            </div>
                        </div>
                        {/* 聽眾 / FAN 排行榜：左右滑動切換（總觀眾數列一併滑動，支援左滑/右滑手勢） */}
                        <div
                            className="flex-1 min-h-0 overflow-x-hidden overflow-y-hidden flex flex-col touch-pan-y"
                            onTouchStart={(e) => handleViewersListSwipeStart(e.touches[0].clientX, e.touches[0].clientY)}
                            onTouchEnd={(e) => {
                                if (e.changedTouches[0]) handleViewersListSwipeEnd(e.changedTouches[0].clientX, e.changedTouches[0].clientY)
                            }}
                        >
                            <div
                                className="flex w-[200%] flex-1 min-h-0 transition-transform duration-300 ease-out"
                                style={{
                                    transform: viewersListTab === 'listeners' ? 'translateX(0)' : 'translateX(-50%)',
                                }}
                            >
                                {/* 聽眾：總觀眾數 + 列表 */}
                                <div className="w-1/2 flex-shrink-0 flex flex-col min-h-0">
                                    <div className="flex items-center justify-between px-4 py-3 flex-shrink-0">
                                        <span className="text-gray-600 dark:text-white/80 text-sm">
                                            總觀眾數 {totalViewers} 人
                                        </span>
                                        <button
                                            type="button"
                                            onClick={handleViewersListRefresh}
                                            disabled={viewersListRefreshing}
                                            className="p-2 rounded-full text-gray-600 hover:text-gray-900 dark:text-white/80 dark:hover:text-white transition-colors disabled:opacity-70"
                                            aria-label="重新整理"
                                        >
                                            <FiRefreshCw className={`w-5 h-5 ${viewersListRefreshing ? 'animate-spin' : ''}`} />
                                        </button>
                                    </div>
                                    <div className="flex-1 overflow-y-auto min-h-0">
                                        <ul className="space-y-0">
                                        {viewersList.map((viewer) => (
                                            <li key={viewer.id}>
                                                <button
                                                    type="button"
                                                    onClick={() => openProfileFromViewersList(viewer)}
                                                    className="w-full flex items-center gap-3 px-3 py-2 text-left hover:bg-gray-100 active:bg-gray-200 dark:hover:bg-white/5 dark:active:bg-white/10 transition-colors"
                                                >
                                                    <div className="relative flex-shrink-0">
                                                        <div className="relative w-12 h-12 rounded-full overflow-hidden bg-gray-200 dark:bg-white/10">
                                                            {viewer.avatar ? (
                                                                <Image
                                                                    src={viewer.avatar}
                                                                    alt=""
                                                                    fill
                                                                    className="object-cover"
                                                                    unoptimized
                                                                />
                                                            ) : (
                                                                <div className="w-full h-full flex items-center justify-center">
                                                                    <FaUser className="w-6 h-6 text-gray-400 dark:text-white/50" />
                                                                </div>
                                                            )}
                                                        </div>
                                                        {viewer.badge && (
                                                            <span className="absolute -bottom-0.5 -left-0.5 px-1.5 py-0.5 rounded bg-green-600 text-white text-[10px] font-medium">
                                                                {viewer.badge}
                                                            </span>
                                                        )}
                                                    </div>
                                                    <span className="text-gray-900 dark:text-white font-medium text-sm truncate flex-1">
                                                        {viewer.name}
                                                    </span>
                                                </button>
                                            </li>
                                        ))}
                                        </ul>
                                    </div>
                                </div>
                                {/* FAN 排行榜：總觀眾數 + 內容 */}
                                <div className="w-1/2 flex-shrink-0 flex flex-col min-h-0">
                                    <div className="flex items-center justify-between px-4 py-3 flex-shrink-0">
                                        <span className="text-gray-600 dark:text-white/80 text-sm">
                                            總觀眾數 {totalViewers} 人
                                        </span>
                                        <button
                                            type="button"
                                            onClick={handleViewersListRefresh}
                                            disabled={viewersListRefreshing}
                                            className="p-2 rounded-full text-gray-600 hover:text-gray-900 dark:text-white/80 dark:hover:text-white transition-colors disabled:opacity-70"
                                            aria-label="重新整理"
                                        >
                                            <FiRefreshCw className={`w-5 h-5 ${viewersListRefreshing ? 'animate-spin' : ''}`} />
                                        </button>
                                    </div>
                                    <div className="flex-1 overflow-y-auto min-h-0 px-4 pb-4">
                                        <div className="py-8 text-center text-gray-500 dark:text-white/60 text-sm">
                                            FAN 排行榜後續接 API
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </>
            )}

            {/* 使用者個人資料彈出面板（與參考圖一致：白底、標題列、FAN、LIVE/時間ハート、關注鈕） */}
            {profileUser && (
                <>
                    <div
                        className="fixed inset-0 z-[350] bg-black/50"
                        onClick={closeProfilePanel}
                        aria-hidden
                    />
                    <div
                        className="fixed inset-x-0 bottom-0 z-[350] flex flex-col max-h-[70vh] bg-white dark:bg-[#1a1a1e] rounded-t-2xl shadow-2xl pb-[env(safe-area-inset-bottom)] transition-transform duration-300 ease-out dark:shadow-[0_-4px_24px_rgba(0,0,0,0.5)]"
                        style={{
                            transform: profilePanelClosing ? 'translateY(100%)' : profilePanelEntering ? 'translateY(100%)' : 'translateY(0)',
                        }}
                        onTransitionEnd={handleProfilePanelTransitionEnd}
                        onClick={(e) => e.stopPropagation()}
                        role="dialog"
                        aria-modal="true"
                        aria-label="使用者個人資料"
                    >
                        {/* 標題列：もっと見る + 關閉 */}
                        <div className="flex items-center justify-end px-4 py-3 flex-shrink-0">
                            <button
                                type="button"
                                onClick={closeProfilePanel}
                                className="p-2 -mr-2 rounded-full text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white transition-colors"
                                aria-label="關閉"
                            >
                                <FiX className="w-5 h-5" />
                            </button>
                        </div>
                        {/* 頭像 + 名稱 + @id + FAN */}
                        <div className="flex items-start gap-4 px-4 py-4 flex-shrink-0">
                                <div className="relative flex-shrink-0 flex flex-col justify-start items-stretch">
                                <div className="relative flex-shrink-0 rounded-full border border-black/5 w-24 h-24 flex items-center justify-center overflow-hidden">
                                    <Link href={`/user/${encodeURIComponent(profileUser.id)}`} className="block w-24 h-24">
                                        <div className="relative w-full h-full rounded-full overflow-hidden bg-gray-100 dark:bg-white/10 pointer-events-none">
                                            {profileUser.avatar ? (
                                                <Image
                                                    src={profileUser.avatar}
                                                    alt=""
                                                    fill
                                                    className="object-cover"
                                                    unoptimized
                                                />
                                            ) : (
                                                <div className="w-full h-full flex items-center justify-center text-gray-400 dark:text-white/50">
                                                    <FaUser className="w-10 h-10" />
                                                </div>
                                            )}
                                        </div>
                                    </Link>
                                </div>
                                {(() => {
                                    const frame =
                                        profileUser.id === stream.userId
                                            ? stream.avatarFrame
                                            : userProfilesRef.current.get(profileUser.id)?.avatarFrame ?? null
                                    if (!frame) return null
                                    const isLottie = frame.type === 'LOTTIE'
                                    return (
                                        <div className="absolute -top-[26px] -left-[18px] w-[132px] h-[132px] z-10 pointer-events-none">
                                            {isLottie ? (
                                                profileUser.id === stream.userId && avatarFrameLottieData ? (
                                                    <Lottie
                                                        animationData={avatarFrameLottieData}
                                                        loop
                                                        autoplay
                                                        style={{ width: '100%', height: '100%' }}
                                                    />
                                                ) : profileAvatarFrameLottieData ? (
                                                    <Lottie
                                                        animationData={profileAvatarFrameLottieData}
                                                        loop
                                                        autoplay
                                                        style={{ width: '100%', height: '100%' }}
                                                    />
                                                ) : null
                                            ) : (
                                                <Image
                                                    src={resolveAvatarFrameAssetUrl(frame.image)}
                                                    alt={profileUser.name}
                                                    fill
                                                    className="object-contain"
                                                    unoptimized
                                                />
                                            )}
                                        </div>
                                    )
                                })()}
                            </div>
                            <div className="min-w-0 flex-1 pt-0.5">
                                <div className="inline-flex items-center gap-1 flex-wrap">
                                    <Link
                                        href={`/user/${encodeURIComponent(profileUser.id)}`}
                                        className="text-gray-900 dark:text-white font-semibold text-base truncate"
                                    >
                                        <span className="truncate max-w-full">{profileUser.name}</span>
                                    </Link>
                                    <VipBadgeLottie
                                        vipLevel={
                                            profileUser.id === stream.userId
                                                ? stream.vipLevel
                                                : userProfiles.get(profileUser.id)?.vipLevel
                                        }
                                        className="flex-shrink-0 h-6 w-auto pointer-events-none"
                                        style={{ height: 24 }}
                                    />
                                </div>
                                <p className="text-gray-500 dark:text-white/70 text-sm mt-1 truncate">@{profileUser.id}</p>
                                <p className="text-gray-900 dark:text-white font-bold text-sm mt-1">
                                    {(() => {
                                        const familyPart =
                                            profileUserGroups === null
                                                ? ''
                                                : profileUserGroups.length === 0
                                                  ? '未加入家族'
                                                  : profileUserGroups.map((g) => g.name).join('、')
                                        const isHost = profileUser?.id === stream.userId
                                        if (!isHost) return familyPart || ' '
                                        const followNum = stream.followers ?? 0
                                        return familyPart ? `${familyPart} • 關注 ${followNum}` : `關注 ${followNum}`
                                    })()}
                                </p>
                            </div>
                        </div>
                        {/* 排名區：LIVE / 時間ハート（淺灰底；深色模式改為深色卡片底） */}
                        <div className="mx-4 mb-4 rounded-xl bg-[#f7f7f7] dark:bg-[#252528] p-3 flex flex-row items-center gap-3 flex-shrink-0">
                            <button
                                type="button"
                                className="flex-1 min-w-0 flex flex-row items-center gap-3 bg-transparent border-0 p-0 cursor-pointer text-left outline-none font-inherit text-inherit"
                            >
                                <span className="flex-shrink-0 w-9 h-9 flex items-center justify-center text-gray-400 dark:text-amber-400/90">
                                    <Image
                                        src="/images/room/wing_gold.png"
                                        alt=""
                                        width={24}
                                        height={24}
                                        className="w-6 h-6 object-contain"
                                        unoptimized
                                    />
                                </span>
                                <div className="flex-1 min-w-0 flex flex-col items-start justify-center gap-0 overflow-hidden">
                                    <span className="text-xs font-normal leading-5 text-[#666] dark:text-white/60">LIVE</span>
                                    <span className="text-xs font-normal leading-4 text-[#666] dark:text-white/60">24時間</span>
                                </div>
                            </button>
                            <button
                                type="button"
                                className="flex-1 min-w-0 flex flex-row items-center gap-3 bg-transparent border-0 p-0 cursor-pointer text-left outline-none font-inherit text-inherit"
                            >
                                <span className="flex-shrink-0 w-9 h-9 flex items-center justify-center text-gray-400 dark:text-red-400/90">
                                    <FaHeart className="w-6 h-6" aria-hidden />
                                </span>
                                <div className="flex-1 min-w-0 flex flex-col items-start justify-center gap-0 overflow-hidden">
                                    <span className="text-xs font-normal leading-5 text-[#666] dark:text-white/60">時間ハート</span>
                                    <div className="flex flex-row items-center gap-1 mt-0.5">
                                        <span className="text-[10px] font-bold leading-4 text-[#666] dark:text-white/70 rounded px-1 py-0 bg-[#e6e6e6] dark:bg-white/15">
                                            24時間
                                        </span>
                                    </div>
                                </div>
                            </button>
                        </div>
                        {/* 關注按鈕（全寬黑底白字，與參考圖一致；僅主播時可點，點擊未關注時先飛星再切換） */}
                        <div className="px-4 pb-4 flex-shrink-0">
                            <button
                                type="button"
                                onClick={() => {
                                    if (profileUser?.id !== stream.userId) return
                                    if (!isFollowing) triggerFlyingStars()
                                    toggleFollow()
                                }}
                                disabled={profileUser?.id !== stream.userId}
                                className="w-full py-3.5 rounded-xl bg-black text-white font-semibold text-base hover:bg-gray-800 active:bg-gray-700 dark:bg-white dark:text-gray-900 dark:hover:bg-gray-100 dark:active:bg-gray-200 dark:disabled:opacity-50 disabled:opacity-50 disabled:pointer-events-none transition-colors"
                            >
                                {profileUser?.id === stream.userId && isFollowing ? '已關注' : '關注'}
                            </button>
                        </div>
                    </div>
                </>
            )}
            <canvas
                ref={heartsCanvasRef}
                className="pointer-events-none fixed bottom-0 right-0 z-[99]"
            />
        </div>
    )
}
