'use client'

// packages
import { useCallback, useEffect, useMemo, useRef, useState, type MouseEvent as ReactMouseEvent } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import Image from 'next/image'
import { FiShare2 } from 'react-icons/fi'

import { FaUser } from 'react-icons/fa'
import { MdVerified } from 'react-icons/md'
import { useTranslation } from 'react-i18next'

// components
import Header from '@/components/Header'
import Sidebar from '@/components/Sidebar'
import BackToTop from '@/components/BackToTop'
import GiftModal from '@/components/GiftModal'
import GiftFullscreenLottieOverlay from '@/components/GiftFullscreenLottieOverlay'
import ConfirmModal from '@/components/ConfirmModal'

// contexts
import { useToast } from '@/contexts/ToastContext'

// _components
import SettingsPanel from '@/app/room/_components/chat/SettingsPanel'
import AdminPanel from '@/app/room/_components/chat/AdminPanel'
import RoomVideoPlayer from '@/app/room/_components/RoomVideoPlayer'
import BanModal from '@/app/room/_components/chat/BanModal'
import Container from '@/app/room/_components/chat/Container'
import MessageList from '@/app/room/_components/chat/MessageList'
import PinnedMessage from '@/app/room/_components/chat/PinnedMessage'
import InputArea from '@/app/room/_components/chat/InputArea'
import MessagesOverlay from '@/app/room/_components/chat/MessagesOverlay'
import ViewerCard from '@/app/room/_components/chat/ViewerCard'
import ParticipantsPopoverViewerCard from '@/app/room/_components/chat/ParticipantsPopoverViewerCard'
import type {
    ChatMessage,
    GiftRecipient,
    ParticipantItem,
    ParticipantSection,
    RoomStream,
    ViewerCardState,
    MentionInfo,
    UserAdminInfo,
} from '@/app/room/_components/types'
import {
    buildParticipantIdentity,
    participantToGiftRecipient,
    resolveGiftRecipientDisplayId,
} from '@/app/room/_components/types'
import { CHAT_EMOJI_ITEMS } from '@/app/room/_components/chat/chatEmojis'
import {
    appendOptimisticBanSystemMessage,
    buildOptimisticBanSystemMessage,
    canViewModeratorOnlySystemMessage,
    mergeIncomingBanSystemMessage,
    normalizeBanSystemMessage,
    resolveViewerCardIsBanned,
} from '@/app/room/_components/chat/banChatHelpers'
import {
    sendChat as sendChatUtil,
    type SendChatParams,
    isEnterRoomSystemMessage,
} from '@/app/room/_components/chat/utils'
import SharePanel from '@/app/catch/_components/SharePanel'
import { buildEmbedIframeHtml, buildEmbedUrl } from '@/lib/embed/buildEmbedUrl'
import DesktopActionPill from '@/app/room/_components/DesktopActionPill'
import LikeFollowActionGroup from '@/app/room/_components/LikeFollowActionGroup'
import RoomStreamDescription from '@/app/room/_components/RoomStreamDescription'
import {
    RoomVideoPlayerSkeleton,
    RoomInfoSkeleton,
    ChatCardSkeleton,
    MobileActionBarSkeleton,
    MobileInfoSkeleton,
} from '@/app/room/_components/Skeletons'

// utils
import {
    formatLastStreamAtLabel,
    formatLiveDuration,
    formatLiveStartedDatePart,
    formatStreamStartedAtDateTime,
    formatViews,
} from '@/utils/format'
import { encryptWsPayload } from '../../../utils/wsCrypto'
import {
    buildSendGiftWsPayload,
    GIFT_SEND_COUNT_MAX,
    wsGiftDataToChatMessage,
    appendGiftChatMessage,
} from '@/lib/gifts/wsGift'
import { useGiftLottiePlayback } from '@/lib/gifts/useGiftLottiePlayback'
import { useUserCoinBalance } from '@/lib/users/useUserCoinBalance'
import { roomChatFontSizePx } from '@/lib/chat/chatFontLevel'
import type { ChatSettingsView } from '@/lib/chat/chatSettingsView'
import { filterVisibleChatMessages } from '@/lib/chat/filterVisibleChatMessages'
import { buildChatWsUrl, sendChatJoin, parseChatWsMessageLoose } from '@/lib/chat/wsChat'
import { applyModAdded, applyModRemoved } from '@/lib/chat/modRoleState'
import { useSyncParticipantModStatus } from '@/lib/chat/useSyncParticipantModStatus'
import { parseStreamStatusPayload } from '@/lib/streams/parseStreamStatusPayload'
import {
    buildStreamsWsUrl,
    findStreamStatusForRoom,
    getStreamsSnapshotList,
    parseStreamsWsMessage,
    sendStreamsSubscribe,
    sendStreamsUnsubscribe,
    type StreamStatusPayload,
    isStreamPayloadLive,
} from '@/lib/streams/wsStreams'
import { isStreamOfflineByLifecycleStatus } from '@/lib/streams/lifecycleStatus'

export default function RoomPageClient({
    initialRoomId,
    initialRoomPublicId,
    initialStream,
    initialIsLiveStream,
    initialPlaybackSrc,
    initialIsLiked,
    initialIsFollowing,
    currentUserId,
    currentUserDisplayName,
    currentUserAvatar,
}: {
    initialRoomId: string
    /** 網址用公開房間 id（display_id 或與 roomId 相同），供 profileApiKey / 對外連結 */
    initialRoomPublicId: string
    initialStream: RoomStream
    initialIsLiveStream: boolean
    initialPlaybackSrc: string
    initialIsLiked: boolean
    initialIsFollowing: boolean
    currentUserId?: string | null
    currentUserDisplayName?: string | null
    currentUserAvatar?: string | null
}) {
    const roomId = initialRoomId

    const router = useRouter()
    const { showToast } = useToast()

    // use translation
    const { t } = useTranslation()
    const isOwnStream = currentUserId !== null && currentUserId !== undefined && currentUserId === initialStream.userId
    const isLoggedIn = Boolean(currentUserId)

    const [isMod, setIsMod] = useState(false)
    const [isSuperAdmin, setIsSuperAdmin] = useState(false)
    // State
    const [sidebarOpen, setSidebarOpen] = useState(false)
    const [isLoading, setIsLoading] = useState(true)
    const [theaterMode, setTheaterMode] = useState(false)
    const [windowHeight, setWindowHeight] = useState<number>(() => {
        if (typeof window !== 'undefined') {
            return window.innerHeight
        }
        return 0
    })
    const [isShareOpen, setIsShareOpen] = useState(false)
    const [shareUrl, setShareUrl] = useState('')
    const [embedIframeHtml, setEmbedIframeHtml] = useState('')
    const [isLiked, setIsLiked] = useState(initialIsLiked)
    const [likeCount, setLikeCount] = useState(initialStream.likes)
    const [isFollowing, setIsFollowing] = useState(initialIsFollowing)
    const [followCount, setFollowCount] = useState(initialStream.followers)
    const [viewersCount, setViewersCount] = useState(0)
    const [liveNowMs, setLiveNowMs] = useState<number>(() => Date.now())
    /** lifecycle status：ready=開播；SSR 初值 + /streams WS */
    const [streamIsLive, setStreamIsLive] = useState(initialIsLiveStream)
    const [streamPlaybackSessionId, setStreamPlaybackSessionId] = useState<string | null>(null)
    const [streamStartedAt, setStreamStartedAt] = useState(initialStream.startedAt)
    const [flyingHearts, setFlyingHearts] = useState<number[]>([])
    const [flyingStars, setFlyingStars] = useState<number[]>([])
    const [giftOpen, setGiftOpen] = useState(false)
    const [giftClosing, setGiftClosing] = useState(false)
    const [giftEntering, setGiftEntering] = useState(false)
    const [giftCount, setGiftCount] = useState<number>(1)
    const [giftSelected, setGiftSelected] = useState<string | null>(null)
    const [giftRecipient, setGiftRecipient] = useState<GiftRecipient | null>(null)
    const [giftSending, setGiftSending] = useState(false)
    const { coinBalance, coinBalanceLoading, setCoinBalance } = useUserCoinBalance(giftOpen && !!currentUserId)
    const {
        current: giftLottiePlay,
        finishCurrent: finishGiftLottie,
        enqueueFromGift: enqueueGiftLottie,
    } = useGiftLottiePlayback()
    const [emojiOpen, setEmojiOpen] = useState(false)
    const [emojiClosing, setEmojiClosing] = useState(false)
    const [emojiEntering, setEmojiEntering] = useState(false)
    const [emojiMode, setEmojiMode] = useState<'desktop' | 'mobile'>('desktop')
    const [emojiTab, setEmojiTab] = useState<'recent' | 'subscription' | 'default'>('default')
    const [emojiShow, setEmojiShow] = useState(true)
    const [highlightMentions, setHighlightMentions] = useState(true)
    const [hideSystemMessages, setHideSystemMessages] = useState(false)
    const [showGiftMessages, setShowGiftMessages] = useState(true)
    const [participantsOpen, setParticipantsOpen] = useState(false)
    const [participantsClosing, setParticipantsClosing] = useState(false)
    const [participantsEntering, setParticipantsEntering] = useState(false)
    const [participantsMaxHDesktop, setParticipantsMaxHDesktop] = useState<number | null>(null)
    const [participantsMaxHMobile, setParticipantsMaxHMobile] = useState<number | null>(null)
    const [participantsShiftXDesktop, setParticipantsShiftXDesktop] = useState(0)
    const [participantsShiftXMobile, setParticipantsShiftXMobile] = useState(0)
    const [settingsOpen, setSettingsOpen] = useState(false)
    const [settingsClosing, setSettingsClosing] = useState(false)
    const [settingsEntering, setSettingsEntering] = useState(false)
    const [settingsMaxHDesktop, setSettingsMaxHDesktop] = useState<number | null>(null)
    const [settingsMaxHMobile, setSettingsMaxHMobile] = useState<number | null>(null)
    const [settingsView, setSettingsView] = useState<ChatSettingsView>('root')
    const [adminOpen, setAdminOpen] = useState(false)
    const [adminClosing, setAdminClosing] = useState(false)
    const [adminEntering, setAdminEntering] = useState(false)
    const [adminMaxHDesktop, setAdminMaxHDesktop] = useState<number | null>(null)
    const [adminMaxHMobile, setAdminMaxHMobile] = useState<number | null>(null)
    const [chatDisabled, setChatDisabled] = useState(false)
    const [chatFontLevel, setChatFontLevel] = useState(2)
    const [viewerCardDepth, setViewerCardDepth] = useState<'root' | 'gift' | 'admin'>('root')
    const [userAdminInfo, setUserAdminInfo] = useState<UserAdminInfo | null>(null)
    const pendingAdminInfoRequestRef = useRef<string | null>(null)
    const [viewerCard, setViewerCard] = useState<ViewerCardState | null>(null)
    const [chatViewerCard, setChatViewerCard] = useState<ViewerCardState | null>(null)
    const [banModalOpen, setBanModalOpen] = useState(false)
    const [banClosing, setBanClosing] = useState(false)
    const [banEntering, setBanEntering] = useState(false)
    const [banTargetUser, setBanTargetUser] = useState<ParticipantItem | null>(null)
    const [viewerCardBanStatus, setViewerCardBanStatus] = useState<{ userId: string; isBanned: boolean } | null>(null)
    const [confirmModalOpen, setConfirmModalOpen] = useState(false)
    const [confirmClosing, setConfirmClosing] = useState(false)
    const [confirmEntering, setConfirmEntering] = useState(false)
    const [confirmTitle, setConfirmTitle] = useState('')
    const [confirmMessage, setConfirmMessage] = useState('')
    const [confirmSeverity, setConfirmSeverity] = useState<'info' | 'warning' | 'danger'>('info')
    const [confirmCallback, setConfirmCallback] = useState<(() => void) | null>(null)
    const confirmRef = useRef<HTMLDivElement>(null)
    const confirmCloseTimerRef = useRef<number | null>(null)
    const [chatOpen, setChatOpen] = useState(true)
    const [onlineParticipants, setOnlineParticipants] = useState<
        Array<{ userId: string | null; author: string; id?: string; publicPathKey?: string | null; displayId?: string | null }>
    >([])
    const [chatDraftDesktop, setChatDraftDesktop] = useState('')
    const [chatDraftMobile, setChatDraftMobile] = useState('')
    const [bannedUserIds, setBannedUserIds] = useState<Set<string>>(new Set())
    const [isAtBottomDesktop, setIsAtBottomDesktop] = useState(true)
    const [isAtBottomMobile, setIsAtBottomMobile] = useState(true)
    const [banInfo, setBanInfo] = useState<{
        isPermanent: boolean
        expiresAt: number | null
        remainingMs: number | null
        reason?: string | null
    } | null>(null)
    const [participantsInfo] = useState<Map<string, { avatar?: string | null }>>(new Map())
    const [unreadMentionMessages, setUnreadMentionMessages] = useState<Set<string>>(new Set())

    const isModerator = isOwnStream || isMod || isSuperAdmin

    // Refs
    const mentionMessageRefs = useRef<Map<string, HTMLDivElement>>(new Map())
    const isModeratorRef = useRef(isModerator)
    const likeFxTimerRef = useRef<number | null>(null)
    const followFxTimerRef = useRef<number | null>(null)
    const giftRef = useRef<HTMLDivElement>(null)
    const giftCloseTimerRef = useRef<number | null>(null)
    const banRef = useRef<HTMLDivElement>(null)
    const banCloseTimerRef = useRef<number | null>(null)
    const emojiPanelRefDesktop = useRef<HTMLDivElement>(null)
    const emojiPanelRefMobile = useRef<HTMLDivElement>(null)
    const emojiBtnRefDesktop = useRef<HTMLButtonElement>(null)
    const emojiBtnRefMobile = useRef<HTMLButtonElement>(null)
    const emojiCloseTimerRef = useRef<number | null>(null)
    const participantsRefDesktop = useRef<HTMLDivElement>(null)
    const participantsRefMobile = useRef<HTMLDivElement>(null)
    const participantsBtnRefDesktop = useRef<HTMLButtonElement>(null)
    const participantsBtnRefMobile = useRef<HTMLButtonElement>(null)
    const participantsCloseTimerRef = useRef<number | null>(null)
    const settingsRefDesktop = useRef<HTMLDivElement>(null)
    const settingsRefMobile = useRef<HTMLDivElement>(null)
    const settingsBtnRefDesktop = useRef<HTMLButtonElement>(null)
    const settingsBtnRefMobile = useRef<HTMLButtonElement>(null)
    const settingsCloseTimerRef = useRef<number | null>(null)
    const adminRefDesktop = useRef<HTMLDivElement>(null)
    const adminRefMobile = useRef<HTMLDivElement>(null)
    const adminBtnRefDesktop = useRef<HTMLButtonElement>(null)
    const adminBtnRefMobile = useRef<HTMLButtonElement>(null)
    const adminCloseTimerRef = useRef<number | null>(null)
    const viewerCardRef = useRef<HTMLDivElement>(null)
    const chatViewerCardRef = useRef<HTMLDivElement>(null)
    const chatCardRefDesktop = useRef<HTMLDivElement>(null)
    const chatCardRefMobile = useRef<HTMLDivElement>(null)
    const chatInputBarRefDesktop = useRef<HTMLDivElement>(null)
    const chatInputBarRefMobile = useRef<HTMLDivElement>(null)
    const chatWriteAreaRefDesktop = useRef<HTMLDivElement>(null)
    const chatWriteAreaRefMobile = useRef<HTMLDivElement>(null)
    const chatMessagesRefDesktop = useRef<HTMLDivElement>(null)
    const chatMessagesRefMobile = useRef<HTMLDivElement>(null)
    const wsRef = useRef<WebSocket | null>(null)
    const streamsWsRef = useRef<WebSocket | null>(null)
    const reconnectTimeoutRef = useRef<number | null>(null)
    const reconnectAttemptsRef = useRef(0)
    const streamsReconnectTimeoutRef = useRef<number | null>(null)
    const streamsReconnectAttemptsRef = useRef(0)
    const streamOfflineTimerRef = useRef<number | null>(null)
    const chatPopoutWindowRef = useRef<Window | null>(null)
    const [chatPopoutOpen, setChatPopoutOpen] = useState(false)

    // Memo
    const stream: RoomStream = useMemo(
        () => ({ ...initialStream, startedAt: streamStartedAt }),
        [initialStream, streamStartedAt]
    )

    /** videoUID + startedAt：OBS 重連新場次時強制播放器重拉 WHEP */
    const playbackSessionKey = useMemo(() => {
        const uid = (streamPlaybackSessionId ?? '').trim().toLowerCase()
        const started = String(streamStartedAt ?? '').trim()
        if (uid && started) return `${uid}:${started}`
        if (uid) return uid
        if (started) return `started:${started}`
        return null
    }, [streamPlaybackSessionId, streamStartedAt])

    useEffect(() => {
        setStreamStartedAt(initialStream.startedAt)
    }, [initialStream.startedAt, roomId])

    useEffect(() => {
        setStreamIsLive(initialIsLiveStream)
    }, [initialIsLiveStream, roomId])

    const isLiveStreamUi = streamIsLive
    const playbackSrc = initialPlaybackSrc
    const streamStartedAtTitle = useMemo(
        () => formatStreamStartedAtDateTime(streamStartedAt),
        [streamStartedAt]
    )

    const streamDescriptionMeta = useMemo(() => {
        if (isLiveStreamUi) {
            const watching = t('room.streamDescription.watchingNow', {
                count: viewersCount,
            })
            const startedDate = formatLiveStartedDatePart(stream.startedAt, liveNowMs)
            const collapsed = startedDate
                ? `${watching} · ${t('room.streamDescription.liveStarted', { date: startedDate })}`
                : watching
            const duration = formatLiveDuration(stream.startedAt, liveNowMs) || '00:00:00'
            const expanded = `${watching} · ${t('room.streamDescription.liveDuration', { duration })}`
            return { collapsed, expanded }
        }
        const when = formatLastStreamAtLabel(streamStartedAt, liveNowMs)
        return { collapsed: when, expanded: when }
    }, [isLiveStreamUi, stream.startedAt, viewersCount, streamStartedAt, liveNowMs, t])

    useEffect(() => {
        isModeratorRef.current = isModerator
    }, [isModerator])

    useEffect(() => {
        if (!currentUserId) {
            setIsMod(false)
            setIsSuperAdmin(false)
            return
        }
        if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return
        wsRef.current.send(
            encryptWsPayload({
                type: 'check_mod_status',
                userId: currentUserId,
            })
        )
    }, [currentUserId, roomId])

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

    const emojiDefaultItems = CHAT_EMOJI_ITEMS

    const initialMessages: ChatMessage[] = useMemo(() => [], [])

    const appendGiftMessageAndMaybePlayLottie = useCallback(
        (message: ChatMessage) => {
            setMessages((prev) => {
                const isNew = !prev.some((m) => m.id === message.id)
                if (isNew && message.gift) {
                    void enqueueGiftLottie(message.gift, message.id, message.userId)
                }
                return appendGiftChatMessage(prev, message)
            })
        },
        [enqueueGiftLottie]
    )

    const resolveGiftMessageUserType = useCallback(
        (userId: string | null | undefined): ChatMessage['userType'] | undefined => {
            if (!userId) return undefined
            if (userId === stream.userId) return 'streamer'
            if (superAdminUserIdsRef.current.has(userId)) return 'superAdmin'
            if (modUserIdsRef.current.has(userId)) return 'manager'
            return undefined
        },
        [stream.userId]
    )

    const [messages, setMessages] = useState<ChatMessage[]>(initialMessages)
    const [pinnedMessage, setPinnedMessage] = useState<ChatMessage | null>(null)

    useEffect(() => {
        if (typeof window === 'undefined') return
        if (chatPopoutOpen) {
            if (wsRef.current) {
                wsRef.current.onclose = null
                wsRef.current.onerror = null
                wsRef.current.onmessage = null
                wsRef.current.close()
                wsRef.current = null
            }
            return
        }
        let isUnmounting = false
        const connectWebSocket = () => {
            if (isUnmounting) return
            try {
                const wsUrl = buildChatWsUrl()
                const ws = new WebSocket(wsUrl)
                wsRef.current = ws
                ws.onopen = () => {
                    console.log('WebSocket connected to:', wsUrl)
                    reconnectAttemptsRef.current = 0
                    sendChatJoin(ws, {
                        roomId,
                        userId: currentUserId,
                        author: currentUserDisplayName || t('common.guest'),
                    })
                    if (currentUserId) {
                        ws.send(
                            encryptWsPayload({
                                type: 'check_mod_status',
                                userId: currentUserId,
                            })
                        )
                        ws.send(
                            encryptWsPayload({
                                type: 'check_ban_status',
                                userId: currentUserId,
                            })
                        )
                    }
                    ws.send(
                        encryptWsPayload({
                            type: 'check_chat_status',
                        })
                    )
                    ws.send(encryptWsPayload({ type: 'get_participants' }))
                }
                ws.onmessage = (event) => {
                    try {
                        const data = parseChatWsMessageLoose(event.data as string)
                        if (!data) return
                        if (data.type === 'message') {
                            const msgData = data.data
                            if (!msgData) return
                            const userId = msgData.userId || null
                            let userType: 'streamer' | 'manager' | 'superAdmin' | undefined = msgData.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 message: ChatMessage = normalizeBanSystemMessage({
                                id: msgData.id,
                                author: msgData.author,
                                text: msgData.text,
                                isSystem: msgData.isSystem || false,
                                isModeratorOnly: msgData.isModeratorOnly || false,
                                userId,
                                userType,
                                time: new Date(msgData.createdAtMs).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
                                mentions: Array.isArray(msgData.mentions) && msgData.mentions.length > 0 ? msgData.mentions : undefined,
                                bannedUserId: msgData.bannedUserId || undefined,
                                unbannedUserId: msgData.unbannedUserId || undefined,
                                adminUserId: msgData.adminUserId || undefined,
                                adminUserType: msgData.adminUserType || undefined,
                                moddedUserId: msgData.moddedUserId || undefined,
                                unmoddedUserId: msgData.unmoddedUserId || undefined,
                                systemMessageType: msgData.systemMessageType || undefined,
                                systemMessageData: msgData.systemMessageData || undefined,
                                authorPublicPathKey:
                                    typeof msgData.authorPublicPathKey === 'string' && msgData.authorPublicPathKey.trim()
                                        ? msgData.authorPublicPathKey.trim()
                                        : undefined,
                            })
                            if (
                                !canViewModeratorOnlySystemMessage(
                                    message,
                                    currentUserId,
                                    isModeratorRef.current
                                )
                            ) {
                                return
                            }
                            if (isEnterRoomSystemMessage(message)) {
                                return
                            }
                            if (message.isSystem && message.systemMessageType) {
                                if (message.systemMessageType === 'banAdded' && message.bannedUserId) {
                                    setBannedUserIds((prev) => {
                                        const updated = new Set(prev)
                                        updated.add(message.bannedUserId!)
                                        return updated
                                    })
                                } else if (message.systemMessageType === 'banRemoved' && message.unbannedUserId) {
                                    setBannedUserIds((prev) => {
                                        const updated = new Set(prev)
                                        updated.delete(message.unbannedUserId!)
                                        return updated
                                    })
                                }
                            }
                            setMessages((prev) => mergeIncomingBanSystemMessage(prev, message))
                        } else if (data.type === 'mod_status') {
                            if (data.data) {
                                const userId = String(data.data.userId ?? '').trim()
                                if (!userId) return
                                if (userId === currentUserId) {
                                    const nextMod = Boolean(data.data.isMod)
                                    const nextSuper = Boolean(data.data.isSuperAdmin)
                                    setIsMod(nextMod)
                                    setIsSuperAdmin(nextSuper)
                                    isModeratorRef.current = isOwnStream || nextMod || nextSuper
                                }
                                if (data.data.isSuperAdmin) {
                                    setSuperAdminUserIds((prev) => {
                                        const updated = new Set(prev)
                                        updated.add(userId)
                                        superAdminUserIdsRef.current = updated
                                        return updated
                                    })
                                } else {
                                    setSuperAdminUserIds((prev) => {
                                        const updated = new Set(prev)
                                        updated.delete(userId)
                                        superAdminUserIdsRef.current = updated
                                        return updated
                                    })
                                }
                            }
                        } else if (data.type === 'mod_added') {
                            if (data.data) {
                                const addedUserId = String(data.data.userId ?? '').trim()
                                if (!addedUserId) return
                                applyModAdded(
                                    {
                                        streamUserId: stream.userId,
                                        currentUserId,
                                        isOwnStream,
                                        modUserIdsRef,
                                        superAdminUserIdsRef,
                                        isModeratorRef,
                                    },
                                    {
                                        setModUserIds,
                                        setIsMod,
                                        setMessages,
                                        setPinnedMessage,
                                        setUserAdminInfo,
                                        setChatViewerCard,
                                    },
                                    addedUserId
                                )
                            }
                        } else if (data.type === 'mod_removed') {
                            if (data.data) {
                                const removedUserId = String(data.data.userId ?? '').trim()
                                if (!removedUserId) return
                                applyModRemoved(
                                    {
                                        streamUserId: stream.userId,
                                        currentUserId,
                                        isOwnStream,
                                        modUserIdsRef,
                                        superAdminUserIdsRef,
                                        isModeratorRef,
                                    },
                                    {
                                        setModUserIds,
                                        setIsMod,
                                        setMessages,
                                        setPinnedMessage,
                                        setUserAdminInfo,
                                        setChatViewerCard,
                                    },
                                    removedUserId
                                )
                            }
                        } else if (data.type === 'ban_status') {
                            if (data.data) {
                                if (data.data.userId === currentUserId) {
                                    if (data.data.isBanned) {
                                        const banData = data.data
                                        const now = Date.now()
                                        const expiresAt = banData.expiresAt
                                        const isPermanent = banData.isPermanent || expiresAt === null
                                        const remainingMs = isPermanent ? null : (banData.remainingMs !== undefined ? banData.remainingMs : (expiresAt > now ? expiresAt - now : 0))
                                        if (!isPermanent && expiresAt !== null && expiresAt <= now) {
                                            setBanInfo(null)
                                        } else {
                                            setBanInfo({
                                                isPermanent,
                                                expiresAt,
                                                remainingMs,
                                                reason: banData.reason ?? null,
                                            })
                                        }
                                    } else {
                                        setBanInfo(null)
                                    }
                                }
                                if (data.data.userId && data.data.userId !== currentUserId) {
                                    setViewerCardBanStatus({
                                        userId: data.data.userId,
                                        isBanned: data.data.isBanned || false,
                                    })
                                }
                                const banUserId = String(data.data.userId ?? '').trim()
                                if (banUserId && data.data.isBanned) {
                                    setBannedUserIds((prev) => {
                                        const updated = new Set(prev)
                                        updated.add(banUserId)
                                        return updated
                                    })
                                } else if (banUserId && !data.data.isBanned) {
                                    setBannedUserIds((prev) => {
                                        const updated = new Set(prev)
                                        updated.delete(banUserId)
                                        return updated
                                    })
                                }
                            }
                        } else if (data.type === 'user_banned') {
                            if (data.data) {
                                const bannedId = String(data.data.userId ?? '').trim()
                                setViewerCardBanStatus({
                                    userId: bannedId || data.data.userId || '',
                                    isBanned: true,
                                })
                                if (bannedId) {
                                    setBannedUserIds((prev) => {
                                        const updated = new Set(prev)
                                        updated.add(bannedId)
                                        return updated
                                    })
                                }
                            }
                        } else if (data.type === 'user_unbanned') {
                            if (data.data) {
                                const unbannedId = String(data.data.userId ?? '').trim()
                                setViewerCardBanStatus({
                                    userId: unbannedId || data.data.userId || '',
                                    isBanned: false,
                                })
                                if (unbannedId) {
                                    setBannedUserIds((prev) => {
                                        const updated = new Set(prev)
                                        updated.delete(unbannedId)
                                        return updated
                                    })
                                }
                            }
                        } else if (data.type === 'user_admin_info') {
                            if (data.data) {
                                setUserAdminInfo(data.data as UserAdminInfo)
                                if (data.data.isSuperAdmin) {
                                    setSuperAdminUserIds((prev) => {
                                        const updated = new Set(prev)
                                        updated.add(data.data.userId)
                                        superAdminUserIdsRef.current = updated
                                        return updated
                                    })
                                } else {
                                    setSuperAdminUserIds((prev) => {
                                        const updated = new Set(prev)
                                        updated.delete(data.data.userId)
                                        superAdminUserIdsRef.current = updated
                                        return updated
                                    })
                                }
                                if (pendingAdminInfoRequestRef.current === data.data.userId) {
                                    setViewerCardDepth('admin')
                                    const avatar = participantsInfo.get(data.data.userId)?.avatar ?? data.data.avatarUrl
                                    const user: ParticipantItem = {
                                        role: 'viewer',
                                        name: data.data.userName,
                                        id: data.data.userId,
                                        avatarUrl: avatar ?? undefined,
                                    }
                                    const containerEl = chatMessagesRefDesktop.current
                                    if (containerEl) {
                                        const messageEl = containerEl.querySelector(`[data-message-id="${data.data.userId}"]`)
                                        if (messageEl) {
                                            const rect = messageEl.getBoundingClientRect()
                                            const containerRect = containerEl.getBoundingClientRect()
                                            setChatViewerCard({
                                                mode: 'desktop',
                                                user,
                                                top: rect.bottom - containerRect.top + 4,
                                                left: rect.left - containerRect.left,
                                            })
                                        } else {
                                            setChatViewerCard({
                                                mode: 'desktop',
                                                user,
                                                top: 0,
                                                left: 0,
                                            })
                                        }
                                    } else {
                                        setChatViewerCard({
                                            mode: 'desktop',
                                            user,
                                            top: 0,
                                            left: 0,
                                        })
                                    }
                                    pendingAdminInfoRequestRef.current = null
                                }
                            } else {
                                setUserAdminInfo(null)
                                pendingAdminInfoRequestRef.current = null
                            }
                        } else if (data.type === 'message_deleted') {
                            const deletedMessageId = data.data?.messageId
                            const deletedBy = data.data?.deletedBy
                            if (deletedMessageId) {
                                setMessages((prev) =>
                                    prev.map((msg) => {
                                        if (msg.id === deletedMessageId) {
                                            return {
                                                ...msg,
                                                isDeleted: true,
                                                deletedBy: deletedBy ? {
                                                    userId: deletedBy.userId,
                                                    author: deletedBy.author,
                                                    userType: deletedBy.userType,
                                                } : undefined,
                                            }
                                        }
                                        return msg
                                    })
                                )
                                setPinnedMessage((prev) => {
                                    if (prev && prev.id === deletedMessageId) {
                                        return null
                                    }
                                    return prev
                                })
                            }
                        } else if (data.type === 'pinned_message') {
                            if (data.data) {
                                const pinnedMsg = data.data
                                setPinnedMessage(pinnedMsg as ChatMessage)
                                setMessages((prev) =>
                                    prev.map((msg) => ({
                                        ...msg,
                                        isPinned: msg.id === pinnedMsg.id,
                                    }))
                                )
                            } else {
                                setPinnedMessage(null)
                                setMessages((prev) =>
                                    prev.map((msg) => ({
                                        ...msg,
                                        isPinned: false,
                                    }))
                                )
                            }
                        } else if (data.type === 'gift' && data.data) {
                            const giftMessage = wsGiftDataToChatMessage(
                                data.data as Parameters<typeof wsGiftDataToChatMessage>[0],
                                resolveGiftMessageUserType
                            )
                            if (giftMessage) {
                                appendGiftMessageAndMaybePlayLottie(giftMessage)
                            }
                        } else if (data.type === 'gift_sent' && data.data?.message) {
                            setGiftSending(false)
                            if (typeof data.data.coinBalance === 'number') {
                                setCoinBalance(data.data.coinBalance)
                            }
                            const giftMessage = wsGiftDataToChatMessage(data.data.message, resolveGiftMessageUserType)
                            if (giftMessage) {
                                appendGiftMessageAndMaybePlayLottie(giftMessage)
                            }
                            closeGift()
                            showToast(t('room.giftModal.sendSuccess'), 'success')
                        } else if (data.type === 'gift_error') {
                            setGiftSending(false)
                            if (typeof data.coinBalance === 'number') {
                                setCoinBalance(data.coinBalance)
                            }
                            const messageKey =
                                typeof data.messageKey === 'string' && data.messageKey.trim()
                                    ? data.messageKey.trim()
                                    : 'room.giftModal.sendFailed'
                            showToast(t(messageKey), 'error')
                        } else if (data.type === 'error' && data.error) {
                            if (data.error === 'FORBIDDEN' || data.error === 'UNAUTHORIZED' || data.error === 'NOT_FOUND') {
                                console.log('[Client] User admin info access denied:', data.message)
                                setUserAdminInfo(null)
                                if (pendingAdminInfoRequestRef.current) {
                                    showToast(data.message || t('room.command.userNotFound'), 'error')
                                    pendingAdminInfoRequestRef.current = null
                                }
                            }
                        } else if (data.type === 'message_sent') {
                            const userId = data.data.userId || null
                            // 若是當前使用者自己發送的訊息，就不再從伺服器回填，避免「自己收到自己的訊息」
                            if (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 message: ChatMessage = {
                                id: data.data.id,
                                author: data.data.author,
                                text: data.data.text,
                                isSystem: false,
                                userId,
                                userType,
                                time: new Date(data.data.createdAtMs).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
                                mentions: Array.isArray(data.data.mentions) && data.data.mentions.length > 0 ? data.data.mentions : undefined,
                                emojis: Array.isArray(data.data.emojis) && data.data.emojis.length > 0 ? data.data.emojis : undefined,
                                authorPublicPathKey:
                                    typeof data.data.authorPublicPathKey === 'string' && data.data.authorPublicPathKey.trim()
                                        ? data.data.authorPublicPathKey.trim()
                                        : undefined,
                            }
                            setMessages((prev) => {
                                const filtered = prev.filter((m) => {
                                    if (m.id.startsWith('temp-')) return false
                                    if (m.id === message.id) return false
                                    return true
                                })
                                return [...filtered, message]
                            })
                        } else if (data.type === 'participants_update') {
                            setOnlineParticipants(data.data.participants || [])
                        } else if (data.type === 'viewers_update') {
                            if (data.data && typeof data.data.viewers === 'number') {
                                console.log('[Client] Received viewers update:', data.data.viewers)
                                setViewersCount(data.data.viewers)
                            }
                        } else if (data.type === 'error') {
                            if (data.banInfo) {
                                const bi = data.banInfo
                                setBanInfo({
                                    isPermanent: Boolean(bi.isPermanent ?? bi.expiresAt === null),
                                    expiresAt: bi.expiresAt ?? null,
                                    remainingMs: bi.remainingMs ?? null,
                                    reason: bi.reason ?? null,
                                })
                            }
                            if (data.messageKey) {
                                const translatedMessage = t(data.messageKey)
                                console.error('WebSocket error:', translatedMessage)
                            } else if (data.message) {
                                console.error('WebSocket error:', data.message)
                            }
                        } else if (data.type === 'ban_removed') {
                            setBanInfo(null)
                            console.log('Ban removed notification received')
                        } else if (data.type === 'chat_disabled') {
                            setChatDisabled(true)
                        } else if (data.type === 'chat_enabled') {
                            setChatDisabled(false)
                        } else if (data.type === 'chat_status') {
                            if (data.data && typeof data.data.chatDisabled === 'boolean') {
                                setChatDisabled(data.data.chatDisabled)
                            }
                        }
                    } catch (error) {
                        console.error('Failed to parse WebSocket message:', error)
                    }
                }
                ws.onerror = (error) => {
                    console.error('WebSocket error:', error)
                }
                ws.onclose = () => {
                    console.log('WebSocket disconnected')
                    wsRef.current = null
                    if (!isUnmounting) {
                        if (reconnectAttemptsRef.current < 5) {
                            reconnectAttemptsRef.current++
                            const delay = Math.min(1000 * Math.pow(2, reconnectAttemptsRef.current), 10000)
                            reconnectTimeoutRef.current = window.setTimeout(() => {
                                if (!isUnmounting) {
                                    connectWebSocket()
                                }
                            }, delay)
                        }
                    }
                }
            } catch (error) {
                console.error('Failed to connect WebSocket:', error)
            }
        }
        connectWebSocket()
        return () => {
            isUnmounting = true
            if (reconnectTimeoutRef.current) {
                clearTimeout(reconnectTimeoutRef.current)
                reconnectTimeoutRef.current = null
            }
            if (wsRef.current) {
                wsRef.current.onclose = null
                wsRef.current.onerror = null
                wsRef.current.onmessage = null
                wsRef.current.close()
                wsRef.current = null
            }
        }
    }, [roomId, currentUserId, currentUserDisplayName, chatPopoutOpen, participantsInfo, showToast, stream.userId, t, isOwnStream, resolveGiftMessageUserType, appendGiftMessageAndMaybePlayLottie, setCoinBalance])

    useEffect(() => {
        if (typeof window === 'undefined') return
        let isUnmounting = false

        const applyStreamStatus = (payload: StreamStatusPayload | null | undefined) => {
            if (!payload || payload.roomId !== roomId) return
            if (payload.cfHasHistory === true) {
                if (streamOfflineTimerRef.current != null) {
                    window.clearTimeout(streamOfflineTimerRef.current)
                    streamOfflineTimerRef.current = null
                }
                setStreamIsLive(false)
                return
            }
            const nextLive = isStreamPayloadLive(payload)
            if (nextLive) {
                if (streamOfflineTimerRef.current != null) {
                    window.clearTimeout(streamOfflineTimerRef.current)
                    streamOfflineTimerRef.current = null
                }
                setStreamIsLive(true)
            } else {
                const immediateOffline =
                    payload.live === false ||
                    isStreamOfflineByLifecycleStatus(payload.status)
                if (immediateOffline) {
                    if (streamOfflineTimerRef.current != null) {
                        window.clearTimeout(streamOfflineTimerRef.current)
                        streamOfflineTimerRef.current = null
                    }
                    setStreamIsLive(false)
                    return
                }
                if (streamOfflineTimerRef.current != null) return
                streamOfflineTimerRef.current = window.setTimeout(() => {
                    setStreamIsLive(false)
                    streamOfflineTimerRef.current = null
                }, 5000)
            }
            if (payload.startedAt != null && Number(payload.startedAt) > 0) {
                const nextStarted = String(payload.startedAt)
                setStreamStartedAt((prev) => (prev === nextStarted ? prev : nextStarted))
            }
            const uid = String(payload.videoUID ?? '')
                .trim()
                .toLowerCase()
            if (uid) {
                setStreamPlaybackSessionId(uid)
            }
        }

        const connectStreamsWebSocket = () => {
            if (isUnmounting) return
            try {
                const wsUrl = buildStreamsWsUrl()
                const ws = new WebSocket(wsUrl)
                streamsWsRef.current = ws

                ws.onopen = () => {
                    console.log('[Streams WS] connected:', wsUrl)
                    streamsReconnectAttemptsRef.current = 0
                    sendStreamsSubscribe(ws, [roomId])
                }

                ws.onmessage = (event) => {
                    try {
                        const data = parseStreamsWsMessage(event.data as string)
                        if (!data) return
                        if (data.type === 'streams_snapshot') {
                            const status = findStreamStatusForRoom(
                                getStreamsSnapshotList(data.data),
                                roomId
                            )
                            if (status) applyStreamStatus(status)
                        } else if (data.type === 'stream_status') {
                            const status = parseStreamStatusPayload(data.data)
                            if (status) applyStreamStatus(status)
                        }
                    } catch (error) {
                        console.error('[Streams WS] parse failed:', error)
                    }
                }

                ws.onerror = (error) => {
                    console.error('[Streams WS] error:', error)
                }

                ws.onclose = () => {
                    console.log('[Streams WS] disconnected')
                    streamsWsRef.current = null
                    if (!isUnmounting && streamsReconnectAttemptsRef.current < 5) {
                        streamsReconnectAttemptsRef.current++
                        const delay = Math.min(1000 * Math.pow(2, streamsReconnectAttemptsRef.current), 10000)
                        streamsReconnectTimeoutRef.current = window.setTimeout(() => {
                            if (!isUnmounting) connectStreamsWebSocket()
                        }, delay)
                    }
                }
            } catch (error) {
                console.error('[Streams WS] connect failed:', error)
            }
        }

        setStreamIsLive(initialIsLiveStream)
        connectStreamsWebSocket()

        return () => {
            isUnmounting = true
            if (streamOfflineTimerRef.current != null) {
                window.clearTimeout(streamOfflineTimerRef.current)
                streamOfflineTimerRef.current = null
            }
            if (streamsReconnectTimeoutRef.current) {
                clearTimeout(streamsReconnectTimeoutRef.current)
                streamsReconnectTimeoutRef.current = null
            }
            if (streamsWsRef.current) {
                if (streamsWsRef.current.readyState === WebSocket.OPEN) {
                    sendStreamsUnsubscribe(streamsWsRef.current, [roomId])
                }
                streamsWsRef.current.onclose = null
                streamsWsRef.current.onerror = null
                streamsWsRef.current.onmessage = null
                streamsWsRef.current.close()
                streamsWsRef.current = null
            }
        }
    }, [roomId, initialIsLiveStream])

    useEffect(() => {
        setMessages(initialMessages)
    }, [initialMessages, roomId])

    useEffect(() => {
        setIsLoading(false)
    }, [])

    useEffect(() => {
        if (typeof window === 'undefined') return
        const origin = window.location.origin
        setShareUrl(`${origin}/room/${encodeURIComponent(initialRoomPublicId)}`)
        const embedUrl = buildEmbedUrl(origin, initialRoomPublicId)
        setEmbedIframeHtml(
            buildEmbedIframeHtml(embedUrl, {
                title: initialStream.title || initialStream.channelName,
            })
        )
    }, [roomId, initialRoomPublicId, initialStream.title, initialStream.channelName])

    useEffect(() => {
        setIsLiked(initialIsLiked)
        setLikeCount(initialStream.likes)
        setIsFollowing(initialIsFollowing)
        setFollowCount(initialStream.followers)
        // 观看人数默认为 0，通过 WebSocket 实时更新，不依赖数据库值
        setViewersCount(0)
    }, [initialIsFollowing, initialIsLiked, initialStream.followers, initialStream.likes, roomId])

    useEffect(() => {
        if (!currentUserId || typeof window === 'undefined') return
        if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return
        wsRef.current.send(
            encryptWsPayload({
                type: 'check_ban_status',
                userId: currentUserId,
            })
        )
    }, [currentUserId, roomId])

    useEffect(() => {
        if (!banInfo || banInfo.isPermanent || banInfo.expiresAt === null) return
        const interval = setInterval(() => {
            setBanInfo((prev) => {
                if (!prev || prev.isPermanent || prev.expiresAt === null) return null
                const now = Date.now()
                const remaining = prev.expiresAt - now
                if (remaining <= 0) {
                    return null
                }
                return { ...prev, remainingMs: remaining }
            })
        }, 1000)

        return () => clearInterval(interval)
    }, [banInfo])

    const mentionUsers = useMemo(() => {
        return onlineParticipants
            .filter((p) => p.userId && p.author)
            .map((p) => ({
                userId: p.userId!,
                name: p.author,
                avatarUrl: participantsInfo.get(p.userId!)?.avatar ?? null,
            }))
    }, [onlineParticipants, participantsInfo])

    const isMessageMentioningMe = useCallback((message: ChatMessage): boolean => {
        if (!message.mentions || message.mentions.length === 0) return false
        const hasEveryone = message.mentions.some((mention) => mention.userId === '@everyone')
        if (hasEveryone) return true
        if (!currentUserId) return false
        return message.mentions.some((mention) => mention.userId === currentUserId)
    }, [currentUserId])

    const scrollToMentionMessage = useCallback((messageId: string, mode: 'desktop' | 'mobile') => {
        const element = mentionMessageRefs.current.get(messageId)
        if (element) {
            element.scrollIntoView({ behavior: 'smooth', block: 'center' })
            element.classList.add('animate-pulse')
            setTimeout(() => {
                element.classList.remove('animate-pulse')
            }, 2000)
        } else {
            const container = mode === 'desktop' ? chatMessagesRefDesktop.current : chatMessagesRefMobile.current
            if (container) {
                container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' })
            }
        }
        setUnreadMentionMessages((prev) => {
            const next = new Set(prev)
            next.delete(messageId)
            return next
        })
    }, [])

    const openShare = () => {
        if (typeof window !== 'undefined') {
            const origin = window.location.origin
            const publicId = initialRoomPublicId || roomId
            setShareUrl(`${origin}/room/${encodeURIComponent(publicId)}`)
            const embedUrl = buildEmbedUrl(origin, publicId)
            setEmbedIframeHtml(
                buildEmbedIframeHtml(embedUrl, {
                    title: stream.title || stream.channelName,
                })
            )
        }
        setIsShareOpen(true)
    }

    const clearChat = () => setMessages([])

    const popoutChat = () => {
        if (typeof window === 'undefined') return
        if (chatPopoutOpen) return
        const popoutUrl = `${window.location.origin}/room/${roomId}/chat`
        const popoutWindow = window.open(
            popoutUrl,
            'chatPopout',
            'width=600,height=600,resizable=yes,scrollbars=yes'
        )
        if (popoutWindow) {
            chatPopoutWindowRef.current = popoutWindow
            setChatPopoutOpen(true)
            if (wsRef.current) {
                wsRef.current.onclose = null
                wsRef.current.onerror = null
                wsRef.current.onmessage = null
                wsRef.current.close()
                wsRef.current = null
            }
            const checkClosed = setInterval(() => {
                if (popoutWindow.closed) {
                    clearInterval(checkClosed)
                    chatPopoutWindowRef.current = null
                    setChatPopoutOpen(false)
                }
            }, 500)
            popoutWindow.addEventListener('beforeunload', () => {
                clearInterval(checkClosed)
                chatPopoutWindowRef.current = null
                setChatPopoutOpen(false)
            })
        }
    }

    const restoreChat = () => {
        if (chatPopoutWindowRef.current && !chatPopoutWindowRef.current.closed) {
            chatPopoutWindowRef.current.close()
        }
        chatPopoutWindowRef.current = null
        setChatPopoutOpen(false)
    }

    const sendChat = (mode: 'desktop' | 'mobile') => {
        if (chatDisabled && !isSuperAdmin) {
            showToast(t('room.chat.disabled'), 'error')
            return
        }
        const writeAreaRef = mode === 'desktop' ? chatWriteAreaRefDesktop : chatWriteAreaRefMobile
        const params: SendChatParams = {
            writeAreaRef,
            currentUserId,
            currentUserDisplayName,
            streamUserId: stream.userId,
            wsRef,
            setMessages,
            setChatDraftDesktop,
            setChatDraftMobile,
            chatWriteAreaRefDesktop,
            chatWriteAreaRefMobile,
            mode,
            superAdminUserIdsRef,
            modUserIdsRef,
            authorPublicPathKey: (() => {
                const raw = onlineParticipants.find((op) => op.userId === currentUserId)?.publicPathKey
                const t = typeof raw === 'string' ? raw.trim() : ''
                if (t) return t
                return currentUserId && !currentUserId.startsWith('guest_') ? currentUserId : undefined
            })(),
        }
        sendChatUtil(params)
    }

    const handleUserCommand = useCallback(async (command: { type: 'user'; args: string[] }) => {
        if (!isModerator) {
            showToast(t('room.command.onlyModerator'), 'error')
            return
        }
        const userName = command.args.join(' ')
        if (!userName) {
            showToast(t('room.command.userNameRequired'), 'error')
            return
        }
        try {
            const res = await fetch(`/api/users/info?ids=${encodeURIComponent(userName)}`)
            const data = await res.json()
            let targetUserId: string | null = null
            if (data.users && data.users.length > 0) {
                const matchedUser = data.users.find((u: { displayName: string; id: string }) =>
                    u.displayName.toLowerCase() === userName.toLowerCase()
                )
                if (matchedUser) {
                    targetUserId = matchedUser.id
                } else if (data.users.length === 1) {
                    targetUserId = data.users[0].id
                }
            }
            if (!targetUserId) {
                targetUserId = userName
            }
            if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
                showToast(t('room.command.userNotFound'), 'error')
                return
            }
            pendingAdminInfoRequestRef.current = targetUserId
            wsRef.current.send(
                encryptWsPayload({
                    type: 'get_user_admin',
                    targetUserId: targetUserId,
                })
            )
            setTimeout(() => {
                if (pendingAdminInfoRequestRef.current === targetUserId) {
                    pendingAdminInfoRequestRef.current = null
                    showToast(t('room.command.userNotFound'), 'error')
                }
            }, 5000)
        } catch (error) {
            console.error('Failed to handle user command:', error)
            showToast(t('room.command.failed'), 'error')
        }
    }, [isModerator, showToast, t])

    const openGift = (recipient?: GiftRecipient) => {
        if (giftCloseTimerRef.current) {
            window.clearTimeout(giftCloseTimerRef.current)
            giftCloseTimerRef.current = null
        }
        const baseRecipient =
            recipient ?? {
                name: stream.channelName,
                userId: stream.userId,
                avatarUrl: stream.avatar,
            }
        setGiftCount(1)
        setGiftSelected(null)
        setGiftEntering(true)
        setGiftOpen(true)
        setGiftClosing(false)
        requestAnimationFrame(() => requestAnimationFrame(() => setGiftEntering(false)))

        setGiftRecipient(baseRecipient)
        void resolveGiftRecipientDisplayId(baseRecipient).then(setGiftRecipient)
    }

    const closeGift = (immediate?: boolean) => {
        setGiftEntering(false)
        setGiftClosing(true)
        if (giftCloseTimerRef.current) {
            window.clearTimeout(giftCloseTimerRef.current)
            giftCloseTimerRef.current = null
        }
        giftCloseTimerRef.current = window.setTimeout(
            () => {
                setGiftOpen(false)
                setGiftClosing(false)
                giftCloseTimerRef.current = null
            },
            immediate ? 0 : 200
        )
    }

    useEffect(() => {
        if (!giftOpen) return
        const onMouseDown = (e: globalThis.MouseEvent) => {
            const target = e.target as HTMLElement | null
            if (!target) return
            if (giftRef.current?.contains(target)) return
            closeGift()
        }
        const onKeyDown = (e: KeyboardEvent) => {
            if (e.key === 'Escape') closeGift()
        }
        document.addEventListener('mousedown', onMouseDown)
        window.addEventListener('keydown', onKeyDown)
        return () => {
            document.removeEventListener('mousedown', onMouseDown)
            window.removeEventListener('keydown', onKeyDown)
        }
    }, [giftOpen])

    const sendGift = useCallback(() => {
        if (!currentUserId) {
            showToast(t('room.loginRequired'), 'error')
            return
        }
        if (!giftSelected || giftSending) return
        if (giftCount < 1 || giftCount > GIFT_SEND_COUNT_MAX) {
            showToast(t('room.giftModal.sendInvalidCount'), 'error')
            return
        }

        const recipient =
            giftRecipient ??
            ({
                name: stream.channelName,
                userId: stream.userId,
            } satisfies GiftRecipient)

        if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
            showToast(t('room.giftModal.wsDisconnected'), 'error')
            return
        }

        setGiftSending(true)
        wsRef.current.send(
            encryptWsPayload(
                buildSendGiftWsPayload({
                    stickerName: giftSelected,
                    recipientUserId: recipient.userId,
                    count: giftCount,
                })
            )
        )
    }, [
        currentUserId,
        giftCount,
        giftRecipient,
        giftSelected,
        giftSending,
        showToast,
        stream.channelName,
        stream.userId,
        t,
    ])

    const openEmoji = (mode: 'desktop' | 'mobile') => {
        if (emojiCloseTimerRef.current) {
            window.clearTimeout(emojiCloseTimerRef.current)
            emojiCloseTimerRef.current = null
        }
        setEmojiMode(mode)
        setEmojiTab('default')
        setEmojiEntering(true)
        setEmojiOpen(true)
        setEmojiClosing(false)
        requestAnimationFrame(() => requestAnimationFrame(() => setEmojiEntering(false)))
    }

    const closeEmoji = (immediate?: boolean) => {
        setEmojiEntering(false)
        setEmojiClosing(true)
        if (emojiCloseTimerRef.current) {
            window.clearTimeout(emojiCloseTimerRef.current)
            emojiCloseTimerRef.current = null
        }
        emojiCloseTimerRef.current = window.setTimeout(
            () => {
                setEmojiOpen(false)
                setEmojiClosing(false)
                emojiCloseTimerRef.current = null
            },
            immediate ? 0 : 200
        )
    }

    useEffect(() => {
        if (!emojiOpen) return
        const onMouseDown = (e: globalThis.MouseEvent) => {
            const target = e.target as HTMLElement | null
            if (!target) return
            if (emojiPanelRefDesktop.current?.contains(target)) return
            if (emojiPanelRefMobile.current?.contains(target)) return
            if (target.closest?.('[data-emoji-trigger="1"]')) return
            closeEmoji()
        }
        const onKeyDown = (e: KeyboardEvent) => {
            if (e.key === 'Escape') closeEmoji()
        }
        document.addEventListener('mousedown', onMouseDown)
        window.addEventListener('keydown', onKeyDown)
        return () => {
            document.removeEventListener('mousedown', onMouseDown)
            window.removeEventListener('keydown', onKeyDown)
        }
    }, [emojiOpen])


    const openViewerCard = async (mode: 'desktop' | 'mobile', e: React.MouseEvent<HTMLButtonElement>, u: ParticipantItem) => {
        const panelEl = mode === 'desktop' ? participantsRefDesktop.current : participantsRefMobile.current
        const btnEl = e.currentTarget
        const btnRect = btnEl.getBoundingClientRect()
        let top = 0
        let left = 0
        if (panelEl) {
            const panelRect = panelEl.getBoundingClientRect()
            const nameSpan = btnEl.querySelector('span:nth-child(3)') as HTMLElement
            let nameBottom = btnRect.bottom
            if (nameSpan) {
                const nameRect = nameSpan.getBoundingClientRect()
                nameBottom = nameRect.bottom
            }
            top = nameBottom - panelRect.top + 4
            left = btnRect.left - panelRect.left
        }
        setViewerCardDepth('root')
        const userWithCorrectType: ParticipantItem = {
            ...u,
            userType: u.id === stream.userId ? 'streamer' : (u.userType || (modUserIds.has(u.id) ? 'manager' : undefined)),
        }
        setViewerCard((prev) => {
            if (prev?.mode === mode && prev.user.id === userWithCorrectType.id) return null
            return { mode, user: userWithCorrectType, top, left }
        })
        if (isModerator && u.id && !u.id.startsWith('guest_')) {
            if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
                wsRef.current.send(
                    encryptWsPayload({
                        type: 'check_ban_status',
                        userId: u.id,
                    })
                )
                setViewerCardBanStatus(null)
            } else {
                setViewerCardBanStatus(null)
            }
        } else {
            setViewerCardBanStatus(null)
        }
        if ((isOwnStream || isSuperAdmin) && u.id && !u.id.startsWith('guest_')) {
            if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
                wsRef.current.send(
                    encryptWsPayload({
                        type: 'get_user_admin',
                        targetUserId: u.id,
                    })
                )
                setUserAdminInfo(null)
            } else {
                setUserAdminInfo(null)
            }
        } else {
            setUserAdminInfo(null)
        }
    }

    const openChatViewerCard = async (mode: 'desktop' | 'mobile', e: React.MouseEvent<HTMLButtonElement>, message: ChatMessage) => {
        if (!message.author) return
        const containerEl = mode === 'desktop' ? chatMessagesRefDesktop.current : chatMessagesRefMobile.current
        if (!containerEl) return
        const btnEl = e.currentTarget
        const btnRect = btnEl.getBoundingClientRect()
        const chatCardEl = mode === 'desktop' ? chatCardRefDesktop.current : chatCardRefMobile.current
        if (!chatCardEl) return
        const chatCardRect = chatCardEl.getBoundingClientRect()
        const top = btnRect.top - chatCardRect.top
        const left = btnRect.left - chatCardRect.left
        const avatar = message.userId ? participantsInfo.get(message.userId)?.avatar : undefined
        const isSuperAdmin = message.userId ? superAdminUserIds.has(message.userId) : false
        const userType = message.userId === stream.userId ? 'streamer' : (message.userType || (isSuperAdmin ? 'superAdmin' : (modUserIds.has(message.userId || '') ? 'manager' : undefined)))
        const trimKey = (v: unknown) => {
            if (v == null) return undefined
            const s = String(v).trim()
            return s || undefined
        }
        const participantKey = message.userId
            ? trimKey(onlineParticipants.find((op) => op.userId === message.userId)?.publicPathKey)
            : undefined
        const profileApiKey =
            trimKey(message.authorPublicPathKey) ??
            participantKey ??
            (message.userId && !message.userId.startsWith('guest_') ? message.userId : undefined)
        const user: ParticipantItem = {
            role: message.userId === stream.userId ? t('room.participants.streamerBadge') : (message.userId ? t('room.participants.viewerBadge') : t('room.participants.guestBadge')),
            name: message.author,
            id: message.userId || `guest_${message.author}`,
            avatarUrl: avatar ?? undefined,
            userType: userType,
            profileApiKey: profileApiKey ?? undefined,
        }
        setChatViewerCard((prev) => {
            if (prev?.mode === mode && prev.user.id === user.id) return null
            return { mode, user, top, left }
        })
        if (isModerator && user.id && !user.id.startsWith('guest_')) {
            if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
                wsRef.current.send(
                    encryptWsPayload({
                        type: 'check_ban_status',
                        userId: user.id,
                    })
                )
                setViewerCardBanStatus(null)
            } else {
                setViewerCardBanStatus(null)
            }
        } else {
            setViewerCardBanStatus(null)
        }
        if ((isOwnStream || isSuperAdmin) && user.id && !user.id.startsWith('guest_')) {
            if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
                wsRef.current.send(
                    encryptWsPayload({
                        type: 'get_user_admin',
                        targetUserId: user.id,
                    })
                )
                setUserAdminInfo(null)
            } else {
                setUserAdminInfo(null)
            }
        } else {
            setUserAdminInfo(null)
        }
    }

    const openMentionViewerCard = async (mode: 'desktop' | 'mobile', e: React.MouseEvent<HTMLButtonElement>, user: ParticipantItem) => {
        const containerEl = mode === 'desktop' ? chatMessagesRefDesktop.current : chatMessagesRefMobile.current
        if (!containerEl) return
        const btnEl = e.currentTarget
        const btnRect = btnEl.getBoundingClientRect()
        const chatCardEl = mode === 'desktop' ? chatCardRefDesktop.current : chatCardRefMobile.current
        if (!chatCardEl) return
        const chatCardRect = chatCardEl.getBoundingClientRect()
        const top = btnRect.top - chatCardRect.top
        const left = btnRect.left - chatCardRect.left
        const avatar = user.id ? participantsInfo.get(user.id)?.avatar : undefined
        const userWithAvatar: ParticipantItem = {
            ...user,
            avatarUrl: avatar ?? user.avatarUrl,
        }
        if (!userWithAvatar.avatarUrl && user.id && !user.id.startsWith('guest_')) {
            try {
                const res = await fetch(`/api/users/${encodeURIComponent(user.id)}?lite=1`)
                const data = await res.json()
                const urow = data?.data?.user
                if (data.ok && urow) {
                    userWithAvatar.avatarUrl = urow.avatar ?? undefined
                }
            } catch (error) {
                console.error('Failed to fetch user info:', error)
            }
        }
        if (!userWithAvatar.role) {
            if (userWithAvatar.id === stream.userId) {
                userWithAvatar.role = t('room.participants.streamerBadge')
            } else if (userWithAvatar.id && !userWithAvatar.id.startsWith('guest_')) {
                userWithAvatar.role = t('room.participants.viewerBadge')
            } else {
                userWithAvatar.role = t('room.participants.guestBadge')
            }
        }

        setChatViewerCard((prev) => {
            if (prev?.mode === mode && prev.user.id === userWithAvatar.id) return null
            return { mode, user: userWithAvatar, top, left }
        })
        if (isModerator && userWithAvatar.id && !userWithAvatar.id.startsWith('guest_')) {
            if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
                wsRef.current.send(
                    JSON.stringify({
                        type: 'check_ban_status',
                        userId: userWithAvatar.id,
                    })
                )
                setViewerCardBanStatus(null)
            } else {
                setViewerCardBanStatus(null)
            }
        } else {
            setViewerCardBanStatus(null)
        }
        if ((isOwnStream || isSuperAdmin) && userWithAvatar.id && !userWithAvatar.id.startsWith('guest_')) {
            if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
                wsRef.current.send(
                    JSON.stringify({
                        type: 'get_user_admin',
                        targetUserId: userWithAvatar.id,
                    })
                )
                setUserAdminInfo(null)
            } else {
                setUserAdminInfo(null)
            }
        } else {
            setUserAdminInfo(null)
        }
    }

    const triggerHearts = () => {
        const newHearts = Array.from({ length: 5 }, (_, i) => Date.now() + i)
        setFlyingHearts(newHearts)
        if (likeFxTimerRef.current !== null) window.clearTimeout(likeFxTimerRef.current)
        likeFxTimerRef.current = window.setTimeout(() => {
            setFlyingHearts([])
            likeFxTimerRef.current = null
        }, 1000)
    }

    const triggerStars = () => {
        const newStars = Array.from({ length: 5 }, (_, i) => Date.now() + i)
        setFlyingStars(newStars)
        if (followFxTimerRef.current !== null) window.clearTimeout(followFxTimerRef.current)
        followFxTimerRef.current = window.setTimeout(() => {
            setFlyingStars([])
            followFxTimerRef.current = null
        }, 1000)
    }

    const toggleLike = async () => {
        // 未登入不可點愛心
        if (!currentUserId) {
            showToast(t('room.loginRequired'), 'error')
            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) {
                const nextLiked = Boolean(data.isLiked)
                const nextLikes = Number(data.likes ?? 0)
                setIsLiked(nextLiked)
                setLikeCount(nextLikes)
                if (nextLiked) triggerHearts()
                return
            }
        } catch {
            // ignore
        }
        setIsLiked((prev) => {
            setLikeCount((c) => (prev ? Math.max(0, c - 1) : c + 1))
            if (!prev) triggerHearts()
            return !prev
        })
    }

    const toggleFollow = async () => {
        // 未登入不可關注
        if (!currentUserId) {
            showToast(t('room.loginRequired'), 'error')
            return
        }
        const targetUserId = stream.userId
        try {
            const res = await fetch(`/api/users/${encodeURIComponent(targetUserId)}/follow`, { method: 'POST' })
            const json = await res.json().catch(() => null)
            const data = json?.data
            if (json?.ok && data) {
                const nextFollowing = Boolean(data.isFollowing)
                const nextFollowers = Number(data.followersCount ?? 0)
                setIsFollowing(nextFollowing)
                setFollowCount(nextFollowers)
                if (nextFollowing) triggerStars()
                return
            }
        } catch {
            // ignore
        }
        setIsFollowing((prev) => {
            const next = !prev
            setFollowCount((c) => (next ? c + 1 : Math.max(0, c - 1)))
            if (!prev) triggerStars()
            return next
        })
    }

    const tipButtonClass =
        "w-9 h-9 relative overflow-visible " +
        "before:content-[''] before:absolute before:left-1/2 before:top-[calc(100%+2px)] before:w-0 before:h-0 before:border-x-[5px] before:border-x-transparent before:border-b-[5px] before:border-b-[#525661] " +
        "before:z-10 before:pointer-events-none before:opacity-0 before:-translate-x-[60%] before:transition-all before:duration-200 before:ease-in " +
        "after:content-[attr(tip)] after:absolute after:left-1/2 after:top-[calc(100%+6px)] after:px-2.5 after:py-[7px] after:rounded-xl after:bg-[#525661] " +
        "after:text-[#fcfcfd] after:text-[13px] after:font-semibold after:leading-[1.2] after:whitespace-nowrap after:z-10 after:pointer-events-none " +
        "after:opacity-0 after:-translate-x-[60%] after:transition-all after:duration-200 after:ease-in " +
        "hover:before:opacity-100 hover:before:-translate-x-1/2 hover:after:opacity-100 hover:after:-translate-x-1/2 " +
        "focus-visible:before:opacity-100 focus-visible:before:-translate-x-1/2 focus-visible:after:opacity-100 focus-visible:after:-translate-x-1/2"

    const tipBelowOnlyClass =
        "relative overflow-visible " +
        "before:content-[''] before:absolute before:left-1/2 before:top-[calc(100%+2px)] before:w-0 before:h-0 before:border-x-[5px] before:border-x-transparent before:border-b-[5px] before:border-b-[#525661] " +
        "before:z-10 before:pointer-events-none before:opacity-0 before:-translate-x-[60%] before:transition-all before:duration-200 before:ease-in " +
        "after:content-[attr(tip)] after:absolute after:left-1/2 after:top-[calc(100%+6px)] after:px-2.5 after:py-[7px] after:rounded-xl after:bg-[#525661] " +
        "after:text-[#fcfcfd] after:text-[13px] after:font-semibold after:leading-[1.2] after:whitespace-nowrap after:z-10 after:pointer-events-none " +
        "after:opacity-0 after:-translate-x-[60%] after:transition-all after:duration-200 after:ease-in " +
        "hover:before:opacity-100 hover:before:-translate-x-1/2 hover:after:opacity-100 hover:after:-translate-x-1/2 " +
        "focus-visible:before:opacity-100 focus-visible:before:-translate-x-1/2 focus-visible:after:opacity-100 focus-visible:after:-translate-x-1/2"

    const tipAboveOnlyClass =
        "relative overflow-visible " +
        "before:content-[''] before:absolute before:left-1/2 before:bottom-[calc(100%+2px)] before:w-0 before:h-0 before:border-x-[5px] before:border-x-transparent before:border-t-[5px] before:border-t-[#525661] " +
        "before:z-10 before:pointer-events-none before:opacity-0 before:-translate-x-[60%] before:transition-all before:duration-200 before:ease-in " +
        "after:content-[attr(tip)] after:absolute after:left-1/2 after:bottom-[calc(100%+6px)] after:px-2.5 after:py-[7px] after:rounded-xl after:bg-[#525661] " +
        "after:text-[#fcfcfd] after:text-[13px] after:font-semibold after:leading-[1.2] after:whitespace-nowrap after:z-10 after:pointer-events-none " +
        "after:opacity-0 after:-translate-x-[60%] after:transition-all after:duration-200 after:ease-in " +
        "hover:before:opacity-100 hover:before:-translate-x-1/2 hover:after:opacity-100 hover:after:-translate-x-1/2 " +
        "focus-visible:before:opacity-100 focus-visible:before:-translate-x-1/2 focus-visible:after:opacity-100 focus-visible:after:-translate-x-1/2"

    const [modUserIds, setModUserIds] = useState<Set<string>>(new Set())
    const modUserIdsRef = useRef<Set<string>>(new Set())
    const [superAdminUserIds, setSuperAdminUserIds] = useState<Set<string>>(new Set())
    const superAdminUserIdsRef = useRef<Set<string>>(new Set())

    useSyncParticipantModStatus(
        wsRef,
        onlineParticipants,
        stream.userId,
        roomId,
        setModUserIds,
        setSuperAdminUserIds,
        modUserIdsRef,
        superAdminUserIdsRef
    )

    useEffect(() => {
        if (bannedUserIds.size === 0) return
        if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return

        const checkBannedUsers = () => {
            bannedUserIds.forEach((userId) => {
                if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
                    wsRef.current.send(
                        encryptWsPayload({
                            type: 'check_ban_status',
                            userId: userId,
                        })
                    )
                }
            })
        }
        checkBannedUsers()
        const interval = setInterval(checkBannedUsers, 30000)
        return () => clearInterval(interval)
    }, [bannedUserIds])

    const participantSections: ParticipantSection[] = useMemo(() => {
        const sections: ParticipantSection[] = []
        const streamerOnline = onlineParticipants.find((p) => p.userId === stream.userId)
        const streamerIdentity = buildParticipantIdentity(
            stream.userId,
            streamerOnline?.publicPathKey,
            streamerOnline?.displayId ?? stream.ownerDisplayId ?? null,
        )
        const superAdminUsers: ParticipantItem[] = onlineParticipants
            .filter((p) => p.userId && p.userId !== stream.userId && superAdminUserIds.has(p.userId!))
            .map((p) => {
                const info = participantsInfo.get(p.userId!)
                const identity = buildParticipantIdentity(p.userId!, p.publicPathKey, p.displayId)
                return {
                    role: t('room.participants.viewerBadge'),
                    name: p.author,
                    ...identity,
                    avatarUrl: info?.avatar || undefined,
                    userType: 'superAdmin' as const,
                    isBanned: bannedUserIds.has(p.userId!),
                }
            })
        if (superAdminUsers.length > 0) {
            sections.push({
                title: t('room.participants.superAdmins'),
                items: superAdminUsers,
            })
        }
        sections.push({
            title: t('room.participants.streamer'),
            items: [
                {
                    role: t('room.participants.streamerBadge'),
                    name: stream.channelName,
                    id: streamerIdentity.id,
                    displayId: streamerIdentity.displayId,
                    profileApiKey: initialRoomPublicId,
                    avatarUrl: stream.avatar,
                    userType: 'streamer',
                    isBanned: bannedUserIds.has(stream.userId),
                },
            ],
        })
        const modUsers: ParticipantItem[] = onlineParticipants
            .filter((p) => p.userId && p.userId !== stream.userId && modUserIds.has(p.userId!) && !superAdminUserIds.has(p.userId!))
            .map((p) => {
                const info = participantsInfo.get(p.userId!)
                const identity = buildParticipantIdentity(p.userId!, p.publicPathKey, p.displayId)
                return {
                    role: t('room.participants.viewerBadge'),
                    name: p.author,
                    ...identity,
                    avatarUrl: info?.avatar || undefined,
                    userType: 'manager' as const,
                    isBanned: bannedUserIds.has(p.userId!),
                }
            })
        if (modUsers.length > 0) {
            sections.push({
                title: t('room.participants.mods'),
                items: modUsers,
            })
        }
        const onlineUsers: ParticipantItem[] = onlineParticipants
            .filter((p) => p.userId && p.userId !== stream.userId && !modUserIds.has(p.userId!) && !superAdminUserIds.has(p.userId!))
            .map((p) => {
                const info = participantsInfo.get(p.userId!)
                const identity = buildParticipantIdentity(p.userId!, p.publicPathKey, p.displayId)
                return {
                    role: t('room.participants.viewerBadge'),
                    name: p.author,
                    ...identity,
                    avatarUrl: info?.avatar || undefined,
                    userType: undefined,
                    isBanned: bannedUserIds.has(p.userId!),
                }
            })
        if (onlineUsers.length > 0) {
            sections.push({
                title: t('room.participants.online'),
                items: onlineUsers,
            })
        }
        const guests = onlineParticipants
            .filter((p) => !p.userId)
            .map((p) => ({
                role: t('room.participants.guestBadge'),
                name: p.author,
                id: p.id ?? `guest_${p.author}`,
            }))
        if (guests.length > 0) {
            sections.push({
                title: t('room.participants.guests'),
                items: guests,
            })
        }
        return sections
    }, [
        stream.avatar,
        stream.channelName,
        stream.userId,
        stream.ownerDisplayId,
        initialRoomPublicId,
        onlineParticipants,
        participantsInfo,
        modUserIds,
        superAdminUserIds,
        bannedUserIds,
        t,
    ])

    const streamerParticipantForViewerCard = useMemo(
        (): ParticipantItem => ({
            role: t('room.participants.streamerBadge'),
            name: stream.channelName,
            id: stream.userId,
            avatarUrl: stream.avatar || undefined,
            userType: 'streamer',
            profileApiKey: initialRoomPublicId,
        }),
        [t, stream.channelName, stream.userId, stream.avatar, initialRoomPublicId]
    )

    const handleBanClick = (user: ParticipantItem) => {
        if (banCloseTimerRef.current) {
            window.clearTimeout(banCloseTimerRef.current)
            banCloseTimerRef.current = null
        }
        setBanTargetUser(user)
        setBanEntering(true)
        setBanModalOpen(true)
        setBanClosing(false)
        requestAnimationFrame(() => requestAnimationFrame(() => setBanEntering(false)))
    }

    const closeBan = (immediate?: boolean) => {
        setBanEntering(false)
        setBanClosing(true)
        if (banCloseTimerRef.current) {
            window.clearTimeout(banCloseTimerRef.current)
            banCloseTimerRef.current = null
        }
        banCloseTimerRef.current = window.setTimeout(
            () => {
                setBanModalOpen(false)
                setBanClosing(false)
                setBanTargetUser(null)
                banCloseTimerRef.current = null
            },
            immediate ? 0 : 200
        )
    }

    const handleBan = async (durationMs: number | null, reason: string) => {
        if (!banTargetUser || !currentUserId) return

        if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
            showToast(t('room.ban.wsDisconnected'), 'error')
            return
        }

        try {
            wsRef.current.send(
                encryptWsPayload({
                    type: 'ban_user',
                    userId: banTargetUser.id,
                    durationMs,
                    reason,
                })
            )

            // 樂觀更新本地狀態，實際狀態之後會由伺服器廣播再同步修正
            setBannedUserIds((prev) => {
                const updated = new Set(prev)
                updated.add(banTargetUser.id)
                return updated
            })
            setViewerCardBanStatus({ userId: banTargetUser.id, isBanned: true })
            const adminUserType = isSuperAdmin ? 'superAdmin' : isOwnStream ? 'streamer' : 'manager'
            setMessages((prev) =>
                appendOptimisticBanSystemMessage(
                    prev,
                    buildOptimisticBanSystemMessage({
                        action: 'banAdded',
                        targetUserId: banTargetUser.id,
                        targetUserName: banTargetUser.name,
                        adminUserId: currentUserId,
                        adminUserName: currentUserDisplayName || currentUserId,
                        adminUserType,
                        durationMs,
                        reason,
                    })
                )
            )
            setViewerCard(null)
            setChatViewerCard(null)
            closeBan()
        } catch (error) {
            console.error('Failed to ban user via WebSocket:', error)
            showToast(t('room.ban.banFailed'), 'error')
        }
    }

    const openConfirm = useCallback((title: string, message: string, onConfirm: () => void, severity: 'info' | 'warning' | 'danger' = 'info') => {
        if (confirmCloseTimerRef.current) {
            window.clearTimeout(confirmCloseTimerRef.current)
            confirmCloseTimerRef.current = null
        }
        setConfirmTitle(title)
        setConfirmMessage(message)
        setConfirmSeverity(severity)
        setConfirmCallback(() => onConfirm)
        setConfirmEntering(true)
        setConfirmModalOpen(true)
        setConfirmClosing(false)
        requestAnimationFrame(() => requestAnimationFrame(() => setConfirmEntering(false)))
    }, [])

    const closeConfirm = (immediate?: boolean) => {
        setConfirmEntering(false)
        setConfirmClosing(true)
        if (confirmCloseTimerRef.current) {
            window.clearTimeout(confirmCloseTimerRef.current)
            confirmCloseTimerRef.current = null
        }
        confirmCloseTimerRef.current = window.setTimeout(
            () => {
                setConfirmModalOpen(false)
                setConfirmClosing(false)
                setConfirmTitle('')
                setConfirmMessage('')
                setConfirmSeverity('info')
                setConfirmCallback(null)
                confirmCloseTimerRef.current = null
            },
            immediate ? 0 : 200
        )
    }

    const handleConfirm = () => {
        if (confirmCallback) {
            confirmCallback()
        }
        closeConfirm()
    }

    const handleUnban = async (user: ParticipantItem) => {
        if (!user.id || !currentUserId) return

        openConfirm(
            t('room.ban.title'),
            t('room.ban.confirmUnban', { name: user.name }),
            async () => {
                if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
                    showToast(t('room.ban.unbanFailed'), 'error')
                    return
                }

                try {
                    wsRef.current.send(
                        encryptWsPayload({
                            type: 'unban_user',
                            userId: user.id,
                        })
                    )

                    // 樂觀更新本地狀態，實際狀態之後會由伺服器廣播再同步修正
                    setBannedUserIds((prev) => {
                        const updated = new Set(prev)
                        updated.delete(user.id)
                        return updated
                    })
                    setViewerCardBanStatus({ userId: user.id, isBanned: false })
                    const adminUserType = isSuperAdmin ? 'superAdmin' : isOwnStream ? 'streamer' : 'manager'
                    setMessages((prev) =>
                        appendOptimisticBanSystemMessage(
                            prev,
                            buildOptimisticBanSystemMessage({
                                action: 'banRemoved',
                                targetUserId: user.id,
                                targetUserName: user.name,
                                adminUserId: currentUserId,
                                adminUserName: currentUserDisplayName || currentUserId,
                                adminUserType,
                            })
                        )
                    )
                    setViewerCard(null)
                    setChatViewerCard(null)
                } catch (error) {
                    console.error('Failed to unban user via API:', error)
                    showToast(t('room.ban.unbanFailed'), 'error')
                }
            }
        )
    }

    const handleAddMod = useCallback(
        async (user: ParticipantItem) => {
            if (!isOwnStream && !isSuperAdmin) return
            openConfirm(
                t('room.viewerCard.addMod'),
                t('room.viewerCard.confirmAddMod', { name: user.name }),
                async () => {
                    if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
                        showToast(t('room.viewerCard.addModFailed'), 'error')
                        return
                    }
                    try {
                        wsRef.current.send(
                            encryptWsPayload({
                                type: 'add_mod',
                                userId: user.id,
                            })
                        )
                        applyModAdded(
                            {
                                streamUserId: stream.userId,
                                currentUserId,
                                isOwnStream,
                                modUserIdsRef,
                                superAdminUserIdsRef,
                                isModeratorRef,
                            },
                            {
                                setModUserIds,
                                setIsMod,
                                setMessages,
                                setPinnedMessage,
                                setUserAdminInfo,
                                setChatViewerCard,
                            },
                            user.id
                        )
                    } catch (error) {
                        console.error('Failed to add mod via WebSocket:', error)
                        showToast(t('room.viewerCard.addModFailed'), 'error')
                    }
                }
            )
        },
        [isOwnStream, isSuperAdmin, t, showToast, openConfirm]
    )

    const handlePinMessage = useCallback(
        async (messageId: string) => {
            if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
                showToast(t('room.message.pinFailed'), 'error')
                return
            }
            const msg = messages.find((m) => m.id === messageId)
            if (!msg) {
                showToast(t('room.message.pinFailed'), 'error')
                return
            }
            wsRef.current.send(
                encryptWsPayload({
                    type: 'pin_message',
                    messageId,
                    message: {
                        id: msg.id,
                        userId: msg.userId ?? null,
                        author: msg.author ?? '',
                        text: msg.text,
                        isSystem: Boolean(msg.isSystem),
                        isDeleted: Boolean(msg.isDeleted),
                        createdAt: (msg as { createdAt?: string }).createdAt,
                        createdAtMs: (msg as { createdAtMs?: number }).createdAtMs,
                    },
                })
            )
        },
        [t, showToast, messages]
    )

    const handleUnpinMessage = useCallback(async () => {
        if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
            showToast(t('room.message.unpinFailed'), 'error')
            return
        }
        wsRef.current.send(encryptWsPayload({ type: 'unpin_message' }))
    }, [t, showToast])

    const handleDeleteMessage = useCallback(
        async (messageId: string) => {
            if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
                showToast(t('room.message.deleteFailed'), 'error')
                return
            }
            wsRef.current.send(
                encryptWsPayload({
                    type: 'delete_message',
                    messageId,
                })
            )
        },
        [t, showToast]
    )

    const handleDeleteMessageClick = useCallback((messageId: string) => {
        handleDeleteMessage(messageId)
    }, [handleDeleteMessage])

    const handleRemoveMod = useCallback(
        async (user: ParticipantItem) => {
            if (!isOwnStream && !isSuperAdmin) return
            openConfirm(
                t('room.viewerCard.removeMod'),
                t('room.viewerCard.confirmRemoveMod', { name: user.name }),
                async () => {
                    if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
                        showToast(t('room.viewerCard.removeModFailed'), 'error')
                        return
                    }
                    try {
                        wsRef.current.send(
                            encryptWsPayload({
                                type: 'remove_mod',
                                userId: user.id,
                            })
                        )
                        applyModRemoved(
                            {
                                streamUserId: stream.userId,
                                currentUserId,
                                isOwnStream,
                                modUserIdsRef,
                                superAdminUserIdsRef,
                                isModeratorRef,
                            },
                            {
                                setModUserIds,
                                setIsMod,
                                setMessages,
                                setPinnedMessage,
                                setUserAdminInfo,
                                setChatViewerCard,
                            },
                            user.id
                        )
                    } catch (error) {
                        console.error('Failed to remove mod via WebSocket:', error)
                        showToast(t('room.viewerCard.removeModFailed'), 'error')
                    }
                }
            )
        },
        [isOwnStream, isSuperAdmin, t, showToast, openConfirm]
    )

    const closeSettings = useCallback((immediate?: boolean) => {
        if (!settingsOpen) return
        if (immediate) {
            if (settingsCloseTimerRef.current !== null) {
                window.clearTimeout(settingsCloseTimerRef.current)
                settingsCloseTimerRef.current = null
            }
            setSettingsEntering(false)
            setSettingsOpen(false)
            setSettingsClosing(false)
            return
        }
        if (settingsClosing) return
        if (settingsCloseTimerRef.current !== null) {
            window.clearTimeout(settingsCloseTimerRef.current)
        }
        setSettingsEntering(false)
        setSettingsClosing(true)
        settingsCloseTimerRef.current = window.setTimeout(() => {
            setSettingsOpen(false)
            setSettingsClosing(false)
            settingsCloseTimerRef.current = null
        }, 200)
    }, [settingsOpen, settingsClosing])

    const closeAdmin = useCallback((immediate?: boolean) => {
        if (!adminOpen) return
        if (immediate) {
            if (adminCloseTimerRef.current !== null) {
                window.clearTimeout(adminCloseTimerRef.current)
                adminCloseTimerRef.current = null
            }
            setAdminEntering(false)
            setAdminOpen(false)
            setAdminClosing(false)
            return
        }
        if (adminClosing) return
        if (adminCloseTimerRef.current !== null) {
            window.clearTimeout(adminCloseTimerRef.current)
        }
        setAdminEntering(false)
        setAdminClosing(true)
        adminCloseTimerRef.current = window.setTimeout(() => {
            setAdminOpen(false)
            setAdminClosing(false)
            adminCloseTimerRef.current = null
        }, 200)
    }, [adminOpen, adminClosing])

    const closeParticipants = useCallback(() => {
        if (!participantsOpen || participantsClosing) return
        if (participantsCloseTimerRef.current !== null) {
            window.clearTimeout(participantsCloseTimerRef.current)
        }
        setParticipantsEntering(false)
        setParticipantsClosing(true)
        setViewerCardDepth('root')
        setViewerCard(null)
        participantsCloseTimerRef.current = window.setTimeout(() => {
            setParticipantsOpen(false)
            setParticipantsClosing(false)
            participantsCloseTimerRef.current = null
        }, 200)
    }, [participantsOpen, participantsClosing])

    useEffect(() => {
        if (!settingsOpen) return

        const onMouseDown = (e: globalThis.MouseEvent) => {
            const target = e.target as HTMLElement | null
            if (!target) return
            if (settingsRefDesktop.current?.contains(target)) return
            if (settingsRefMobile.current?.contains(target)) return
            if (target.closest?.('[data-settings-trigger="1"]')) return
            closeSettings()
        }

        const onKeyDown = (e: KeyboardEvent) => {
            if (e.key === 'Escape') closeSettings()
        }

        document.addEventListener('mousedown', onMouseDown)
        window.addEventListener('keydown', onKeyDown)
        return () => {
            document.removeEventListener('mousedown', onMouseDown)
            window.removeEventListener('keydown', onKeyDown)
        }
    }, [settingsOpen, closeSettings])

    useEffect(() => {
        if (!adminOpen) return

        const onMouseDown = (e: globalThis.MouseEvent) => {
            const target = e.target as HTMLElement | null
            if (!target) return
            if (adminRefDesktop.current?.contains(target)) return
            if (adminRefMobile.current?.contains(target)) return
            if (target.closest?.('[data-admin-trigger="1"]')) return
            closeAdmin()
        }

        const onKeyDown = (e: KeyboardEvent) => {
            if (e.key === 'Escape') closeAdmin()
        }

        document.addEventListener('mousedown', onMouseDown)
        window.addEventListener('keydown', onKeyDown)
        return () => {
            document.removeEventListener('mousedown', onMouseDown)
            window.removeEventListener('keydown', onKeyDown)
        }
    }, [adminOpen, closeAdmin])

    useEffect(() => {
        if (!settingsOpen) return

        const compute = () => {
            const computeOne = (
                btn: HTMLButtonElement | null,
                card: HTMLDivElement | null,
                setMaxH: (v: number | null) => void
            ) => {
                if (!btn || !card) return false
                const btnRect = btn.getBoundingClientRect()
                const cardRect = card.getBoundingClientRect()
                if (btnRect.width === 0 || btnRect.height === 0) return false

                const bottomPad = 12
                const available = cardRect.bottom - btnRect.bottom - bottomPad
                const clamped = Math.max(180, Math.min(430, Math.floor(available)))
                setMaxH(Number.isFinite(clamped) ? clamped : null)
                return true
            }

            const desktopOk = computeOne(settingsBtnRefDesktop.current, chatCardRefDesktop.current, setSettingsMaxHDesktop)
            const mobileOk = computeOne(settingsBtnRefMobile.current, chatCardRefMobile.current, setSettingsMaxHMobile)
            if (!desktopOk) setSettingsMaxHDesktop(null)
            if (!mobileOk) setSettingsMaxHMobile(null)
        }

        requestAnimationFrame(compute)
        window.addEventListener('resize', compute)
        return () => window.removeEventListener('resize', compute)
    }, [settingsOpen])

    useEffect(() => {
        if (!adminOpen) return

        const compute = () => {
            const computeOne = (
                btn: HTMLButtonElement | null,
                card: HTMLDivElement | null,
                setMaxH: (v: number | null) => void
            ) => {
                if (!btn || !card) return false
                const btnRect = btn.getBoundingClientRect()
                const cardRect = card.getBoundingClientRect()
                if (btnRect.width === 0 || btnRect.height === 0) return false

                const bottomPad = 12
                const available = cardRect.bottom - btnRect.bottom - bottomPad
                const clamped = Math.max(180, Math.min(430, Math.floor(available)))
                setMaxH(Number.isFinite(clamped) ? clamped : null)
                return true
            }

            const desktopOk = computeOne(adminBtnRefDesktop.current, chatCardRefDesktop.current, setAdminMaxHDesktop)
            const mobileOk = computeOne(adminBtnRefMobile.current, chatCardRefMobile.current, setAdminMaxHMobile)
            if (!desktopOk) setAdminMaxHDesktop(null)
            if (!mobileOk) setAdminMaxHMobile(null)
        }

        requestAnimationFrame(compute)
        window.addEventListener('resize', compute)
        return () => window.removeEventListener('resize', compute)
    }, [adminOpen])

    useEffect(() => {
        const handleResize = () => {
            setWindowHeight(window.innerHeight)
        }
        handleResize()
        window.addEventListener('resize', handleResize)
        return () => window.removeEventListener('resize', handleResize)
    }, [])

    useEffect(() => {
        if (!participantsOpen) return

        const onMouseDown = (e: globalThis.MouseEvent) => {
            const target = e.target as HTMLElement | null
            if (!target) return
            if (participantsRefDesktop.current?.contains(target)) return
            if (participantsRefMobile.current?.contains(target)) return
            if (viewerCardRef.current?.contains(target)) return
            if (target.closest?.('[data-participants-trigger="1"]')) return
            closeParticipants()
        }

        const onKeyDown = (e: KeyboardEvent) => {
            if (e.key === 'Escape') {
                if (viewerCard) return
                closeParticipants()
            }
        }

        document.addEventListener('mousedown', onMouseDown)
        window.addEventListener('keydown', onKeyDown)
        return () => {
            document.removeEventListener('mousedown', onMouseDown)
            window.removeEventListener('keydown', onKeyDown)
        }
    }, [participantsOpen, viewerCard, closeParticipants])

    useEffect(() => {
        if (!participantsOpen) return

        const compute = () => {
            const computeShift = (
                btn: HTMLButtonElement | null,
                card: HTMLDivElement | null,
                panel: HTMLDivElement | null,
                setShift: (v: number) => void
            ) => {
                if (!btn || !card || !panel) return false
                const btnRect = btn.getBoundingClientRect()
                const cardRect = card.getBoundingClientRect()
                if (btnRect.width === 0 || btnRect.height === 0) return false

                const panelW = panel.offsetWidth || 280
                const pad = 12

                const desiredLeft = btnRect.right - panelW
                const desiredRight = btnRect.right

                const overflowLeft = cardRect.left + pad - desiredLeft
                const overflowRight = desiredRight - (cardRect.right - pad)

                let shift = 0
                if (overflowLeft > 0) shift += overflowLeft
                if (overflowRight > 0) shift -= overflowRight

                const maxRightShift = (cardRect.right - pad) - desiredRight
                const maxLeftShift = (cardRect.left + pad) - desiredLeft
                shift = Math.min(Math.max(shift, maxLeftShift), maxRightShift)

                setShift(Math.round(shift))
                return true
            }

            const computeOne = (
                btn: HTMLButtonElement | null,
                card: HTMLDivElement | null,
                setMaxH: (v: number | null) => void
            ) => {
                if (!btn || !card) return false
                const btnRect = btn.getBoundingClientRect()
                const cardRect = card.getBoundingClientRect()
                if (btnRect.width === 0 || btnRect.height === 0) return false

                const gap = 8
                const bottomPad = 12
                const available = cardRect.bottom - (btnRect.bottom + gap) - bottomPad
                const clamped = Math.max(180, Math.min(430, Math.floor(available)))
                setMaxH(Number.isFinite(clamped) ? clamped : null)
                return true
            }

            const desktopOk = computeOne(participantsBtnRefDesktop.current, chatCardRefDesktop.current, setParticipantsMaxHDesktop)
            const mobileOk = computeOne(participantsBtnRefMobile.current, chatCardRefMobile.current, setParticipantsMaxHMobile)

            const desktopShiftOk = computeShift(
                participantsBtnRefDesktop.current,
                chatCardRefDesktop.current,
                participantsRefDesktop.current,
                setParticipantsShiftXDesktop
            )
            const mobileShiftOk = computeShift(
                participantsBtnRefMobile.current,
                chatCardRefMobile.current,
                participantsRefMobile.current,
                setParticipantsShiftXMobile
            )

            if (!desktopOk) setParticipantsMaxHDesktop(null)
            if (!mobileOk) setParticipantsMaxHMobile(null)
            if (!desktopShiftOk) setParticipantsShiftXDesktop(0)
            if (!mobileShiftOk) setParticipantsShiftXMobile(0)
        }

        requestAnimationFrame(compute)
        window.addEventListener('resize', compute)
        return () => window.removeEventListener('resize', compute)
    }, [participantsOpen])

    useEffect(() => {
        return () => {
            if (participantsCloseTimerRef.current !== null) {
                window.clearTimeout(participantsCloseTimerRef.current)
                participantsCloseTimerRef.current = null
            }
            if (settingsCloseTimerRef.current !== null) {
                window.clearTimeout(settingsCloseTimerRef.current)
                settingsCloseTimerRef.current = null
            }
        }
    }, [])

    const openParticipants = () => {
        if (participantsCloseTimerRef.current !== null) {
            window.clearTimeout(participantsCloseTimerRef.current)
            participantsCloseTimerRef.current = null
        }
        if (settingsOpen) closeSettings(true)
        setViewerCardDepth('root')
        setViewerCard(null)
        setParticipantsEntering(true)
        setParticipantsOpen(true)
        setParticipantsClosing(false)
        requestAnimationFrame(() => setParticipantsEntering(false))
    }

    const checkScrollPosition = useCallback((container: HTMLDivElement | null, setIsAtBottom: (value: boolean) => void) => {
        if (!container) return
        const { scrollTop, scrollHeight, clientHeight } = container
        const threshold = 50
        const isAtBottom = scrollHeight - scrollTop - clientHeight < threshold
        setIsAtBottom(isAtBottom)
    }, [])

    const scrollToBottom = useCallback((mode: 'desktop' | 'mobile') => {
        const container = mode === 'desktop' ? chatMessagesRefDesktop.current : chatMessagesRefMobile.current
        if (container) {
            container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' })
        }
    }, [])

    useEffect(() => {
        const container = chatMessagesRefDesktop.current
        if (!container || !chatOpen) return
        const handleScroll = () => {
            checkScrollPosition(container, setIsAtBottomDesktop)
        }
        container.addEventListener('scroll', handleScroll)
        checkScrollPosition(container, setIsAtBottomDesktop)
        return () => {
            container.removeEventListener('scroll', handleScroll)
        }
    }, [chatOpen, messages, checkScrollPosition])

    useEffect(() => {
        const container = chatMessagesRefMobile.current
        if (!container) return
        const handleScroll = () => {
            checkScrollPosition(container, setIsAtBottomMobile)
        }
        container.addEventListener('scroll', handleScroll)
        checkScrollPosition(container, setIsAtBottomMobile)
        return () => {
            container.removeEventListener('scroll', handleScroll)
        }
    }, [messages, checkScrollPosition])

    useEffect(() => {
        if (isAtBottomDesktop && chatMessagesRefDesktop.current) {
            chatMessagesRefDesktop.current.scrollTo({ top: chatMessagesRefDesktop.current.scrollHeight, behavior: 'smooth' })
        }
    }, [messages, isAtBottomDesktop])

    useEffect(() => {
        if (isAtBottomMobile && chatMessagesRefMobile.current) {
            chatMessagesRefMobile.current.scrollTo({ top: chatMessagesRefMobile.current.scrollHeight, behavior: 'smooth' })
        }
    }, [messages, isAtBottomMobile])

    useEffect(() => {
        if (!currentUserId || typeof window === 'undefined') return
        const observer = new IntersectionObserver(
            (entries) => {
                entries.forEach((entry) => {
                    const messageId = entry.target.getAttribute('data-message-id')
                    if (!messageId) return
                    const message = messages.find((m) => m.id === messageId)
                    if (message && message.userId === currentUserId) return
                    if (entry.isIntersecting) {
                        setUnreadMentionMessages((prev) => {
                            if (prev.has(messageId)) {
                                const next = new Set(prev)
                                next.delete(messageId)
                                return next
                            }
                            return prev
                        })
                    }
                })
            },
            {
                root: null,
                rootMargin: '0px',
                threshold: 0.1,
            }
        )
        requestAnimationFrame(() => {
            mentionMessageRefs.current.forEach((element) => {
                observer.observe(element)
            })
        })

        return () => {
            observer.disconnect()
        }
    }, [currentUserId, messages, isMessageMentioningMe])

    useEffect(() => {
        if (!currentUserId) return
        requestAnimationFrame(() => {
            messages.forEach((message) => {
                if (message.userId === currentUserId) return
                if (isMessageMentioningMe(message) && !message.isSystem) {
                    const element = mentionMessageRefs.current.get(message.id)
                    if (element) {
                        const rect = element.getBoundingClientRect()
                        const container = chatMessagesRefDesktop.current || chatMessagesRefMobile.current
                        if (container) {
                            const containerRect = container.getBoundingClientRect()
                            const isVisible =
                                rect.bottom > containerRect.top &&
                                rect.top < containerRect.bottom &&
                                rect.right > containerRect.left &&
                                rect.left < containerRect.right
                            if (!isVisible) {
                                setUnreadMentionMessages((prev) => {
                                    const next = new Set(prev)
                                    next.add(message.id)
                                    return next
                                })
                            } else {
                                setUnreadMentionMessages((prev) => {
                                    if (prev.has(message.id)) {
                                        const next = new Set(prev)
                                        next.delete(message.id)
                                        return next
                                    }
                                    return prev
                                })
                            }
                        }
                    } else {
                        setTimeout(() => {
                            const delayedElement = mentionMessageRefs.current.get(message.id)
                            if (delayedElement) {
                                const rect = delayedElement.getBoundingClientRect()
                                const container = chatMessagesRefDesktop.current || chatMessagesRefMobile.current
                                if (container) {
                                    const containerRect = container.getBoundingClientRect()
                                    const isVisible =
                                        rect.bottom > containerRect.top &&
                                        rect.top < containerRect.bottom &&
                                        rect.right > containerRect.left &&
                                        rect.left < containerRect.right

                                    if (!isVisible) {
                                        setUnreadMentionMessages((prev) => {
                                            const next = new Set(prev)
                                            next.add(message.id)
                                            return next
                                        })
                                    } else {
                                        setUnreadMentionMessages((prev) => {
                                            if (prev.has(message.id)) {
                                                const next = new Set(prev)
                                                next.delete(message.id)
                                                return next
                                            }
                                            return prev
                                        })
                                    }
                                }
                            } else {
                                setUnreadMentionMessages((prev) => {
                                    const next = new Set(prev)
                                    next.add(message.id)
                                    return next
                                })
                            }
                        }, 100)
                    }
                }
            })
        })
    }, [messages, currentUserId, isMessageMentioningMe])

    const openSettings = () => {
        if (settingsCloseTimerRef.current !== null) {
            window.clearTimeout(settingsCloseTimerRef.current)
            settingsCloseTimerRef.current = null
        }
        if (participantsOpen) closeParticipants()
        setSettingsView('root')
        setSettingsEntering(true)
        setSettingsOpen(true)
        setSettingsClosing(false)
        requestAnimationFrame(() => setSettingsEntering(false))
    }

    const openAdmin = () => {
        if (adminCloseTimerRef.current !== null) {
            window.clearTimeout(adminCloseTimerRef.current)
            adminCloseTimerRef.current = null
        }
        if (participantsOpen) closeParticipants()
        if (settingsOpen) closeSettings()
        setAdminEntering(true)
        setAdminOpen(true)
        setAdminClosing(false)
        requestAnimationFrame(() => setAdminEntering(false))
    }

    const handleToggleChat = useCallback(() => {
        if (!isSuperAdmin) return
        if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
            showToast(t('room.admin.toggleChatFailed'), 'error')
            return
        }

        wsRef.current.send(
            JSON.stringify({
                type: 'toggle_chat',
            })
        )
        closeAdmin()
    }, [isSuperAdmin, t, showToast, closeAdmin])

    const chatFontSizePx = roomChatFontSizePx(chatFontLevel)
    const chatLineHeightPx = Math.max(18, Math.min(40, chatFontSizePx + 9))
    const chatScrollPreviewMessage = useMemo(() => {
        const visible = filterVisibleChatMessages(messages, {
            hideSystemMessages,
            showGiftMessages,
            currentUserId,
            isModerator,
        })
        return visible.slice(-1)[0] ?? null
    }, [messages, hideSystemMessages, showGiftMessages, currentUserId, isModerator])

    return (
        <>
            <div
                className={`transition-transform duration-200 ${theaterMode ? '-translate-y-[72px] opacity-0 pointer-events-none' : ''
                    }`}
            >
                <Header onMenuClick={() => setSidebarOpen(!sidebarOpen)} />
            </div>
            <div
                className={`
                    min-h-screen min-w-[380px] bg-white dark:bg-[#0c0d0e] flex overflow-x-auto [@media(min-width:965px)]:overflow-x-hidden transition-colors
                    ${theaterMode ? '[@media(min-width:965px)]:pt-0' : ''}
                `}
            >
                <Sidebar mode="drawer" isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />

                <main
                    className={`flex-1 min-h-screen w-full max-w-full overflow-x-hidden transition-all ${theaterMode ? 'pt-0' : 'pt-[65px]'
                        }`}
                >
                    <div
                        className={`w-full h-full ${theaterMode
                            ? 'px-0 py-0 [@media(min-width:965px)]:max-w-none [@media(min-width:965px)]:mx-0'
                            : 'px-4 [@media(min-width:965px)]:px-6 py-4 max-w-[1600px] mx-auto'
                            }`}
                    >
                        {/* 單一播放器，桌機與手機共用；桌機左右佈局，手機貼齊滿版 */}
                        <div
                            className={`grid items-start ${chatOpen
                                ? 'grid-cols-1 [@media(min-width:965px)]:grid-cols-[minmax(0,1fr)_360px] xl:[@media(min-width:965px)]:grid-cols-[minmax(0,1fr)_380px]'
                                : 'grid-cols-1'
                                } ${theaterMode ? '[@media(min-width:965px)]:gap-0' : 'gap-6'}`}
                        >
                            <div className="flex-1 min-w-0">
                                <div
                                    className={`relative w-full [@media(min-width:965px)]:mx-0 [@media(min-width:965px)]:mt-0`}
                                >
                                    {isLoading ? (
                                        <RoomVideoPlayerSkeleton theater={theaterMode} />
                                    ) : (
                                        <RoomVideoPlayer
                                            src={playbackSrc}
                                            isLive={isLiveStreamUi}
                                            className="w-full"
                                            showChatReopenButton={!chatOpen}
                                            onChatReopen={() => setChatOpen(true)}
                                            chatReopenLabel={t('room.openChat')}
                                            isTheater={theaterMode}
                                            onToggleTheater={() => setTheaterMode((v) => !v)}
                                            theaterInfo={
                                                {
                                                    avatar: stream.avatar,
                                                    channelName: stream.channelName,
                                                    title: stream.title,
                                                    viewers: viewersCount,
                                                    followers: followCount,
                                                    startedAt: stream.startedAt,
                                                } as any
                                            }
                                            onStreamerClick={(e) =>
                                                void openViewerCard('desktop', e, streamerParticipantForViewerCard)
                                            }
                                            isFollowing={isLoggedIn ? isFollowing : false}
                                            isLiked={isLoggedIn ? isLiked : false}
                                            likeCount={isLoggedIn ? likeCount : undefined}
                                            onToggleFollow={isLoggedIn ? toggleFollow : undefined}
                                            onToggleLike={isLoggedIn ? toggleLike : undefined}
                                            isOwnStream={isOwnStream}
                                            playbackSessionId={playbackSessionKey}
                                        />
                                    )}
                                    <GiftFullscreenLottieOverlay
                                        item={giftLottiePlay}
                                        onDone={finishGiftLottie}
                                        scope="player"
                                    />
                                </div>

                                {/* 桌機資訊區塊（劇院模式隱藏） */}
                                <div className={`hidden overflow-visible [@media(min-width:965px)]:block ${theaterMode ? '[@media(min-width:965px)]:hidden' : ''} mt-4`}>
                                    {isLoading ? (
                                        <RoomInfoSkeleton />
                                    ) : (
                                        <>
                                            <h1
                                                className="text-xl font-semibold leading-snug text-[#0f0f0f] dark:text-white break-words"
                                                style={{ wordBreak: 'break-word' }}
                                            >
                                                {stream.title}
                                            </h1>

                                            <div className="relative z-20 mt-3 flex items-center gap-3 min-w-0">
                                                <button
                                                    type="button"
                                                    onClick={(e) => void openViewerCard('desktop', e, streamerParticipantForViewerCard)}
                                                    className="relative w-10 h-10 flex-shrink-0 rounded-full overflow-hidden bg-gray-100 dark:bg-white/10 flex items-center justify-center cursor-pointer hover:opacity-80 transition-opacity"
                                                    aria-label={stream.channelName}
                                                >
                                                    {stream.avatar ? (
                                                        <Image src={stream.avatar} alt={stream.channelName} fill className="pointer-events-none object-cover" unoptimized />
                                                    ) : (
                                                        <FaUser className="w-5 h-5 text-gray-400 dark:text-white/50" aria-hidden="true" />
                                                    )}
                                                </button>

                                                <div className="min-w-0 flex-shrink">
                                                    <div className="relative flex items-center gap-1 min-w-0">
                                                        <button
                                                            type="button"
                                                            onClick={(e) =>
                                                                void openViewerCard('desktop', e, streamerParticipantForViewerCard)
                                                            }
                                                            className="truncate text-base font-medium text-[#0f0f0f] dark:text-white cursor-pointer hover:opacity-80 transition-opacity"
                                                            aria-label={stream.channelName}
                                                        >
                                                            {stream.channelName}
                                                        </button>
                                                            {stream.isVerified && (
                                                                <MdVerified
                                                                    className="w-4 h-4 text-cyan-400 flex-shrink-0"
                                                                    title={t('catch.verified')}
                                                                />
                                                            )}
                                                            {(() => {
                                                                const v = String(stream.vipLevel ?? '').toLowerCase()
                                                                if (v !== 'vip' && v !== 'vvip') return null
                                                                return (
                                                                    <Image
                                                                        src={v === 'vvip' ? '/images/vvip.png' : '/images/vip.png'}
                                                                        alt=""
                                                                        width={28}
                                                                        height={28}
                                                                        className="w-7 h-7 flex-shrink-0 object-contain"
                                                                    />
                                                                )
                                                            })()}

                                                    </div>
                                                </div>

                                                <ul className="ml-auto inline-flex flex-shrink-0 items-center gap-2 overflow-visible">
                                                    <li className="overflow-visible">
                                                        <LikeFollowActionGroup
                                                            variant="desktop"
                                                            showFollow={false}
                                                            showFollowCount
                                                            followCountReadOnly={isOwnStream}
                                                            showLike={isLoggedIn && !isOwnStream}
                                                            isFollowing={isFollowing}
                                                            isLiked={isLiked}
                                                            followCount={followCount}
                                                            likeCount={likeCount}
                                                            followTitle={
                                                                isFollowing ? t('room.unfollow') : t('room.follow')
                                                            }
                                                            likeTitle={isLiked ? t('room.unlike') : t('room.like')}
                                                            onToggleLike={
                                                                isLoggedIn && !isOwnStream ? toggleLike : undefined
                                                            }
                                                            onToggleFollow={
                                                                isLoggedIn && !isOwnStream ? toggleFollow : undefined
                                                            }
                                                            flyingStars={flyingStars}
                                                            flyingHearts={flyingHearts}
                                                        />
                                                    </li>
                                                    <li>
                                                        <DesktopActionPill
                                                            title={t('room.share')}
                                                            onClick={openShare}
                                                            icon={<FiShare2 className="w-5 h-5" />}
                                                        >
                                                            <span>{t('room.share')}</span>
                                                        </DesktopActionPill>
                                                    </li>
                                                </ul>
                                            </div>

                                            <RoomStreamDescription
                                                className="relative z-0 mt-2"
                                                metaLine={streamDescriptionMeta.collapsed}
                                                expandedMetaLine={streamDescriptionMeta.expanded}
                                                description={stream.description ?? ''}
                                                category={stream.category}
                                            />
                                        </>
                                    )}
                                </div>
                            </div>

                            {chatOpen && !chatPopoutOpen ? (
                                <aside className="hidden [@media(min-width:965px)]:block w-[360px] xl:w-[380px] flex-shrink-0">
                                    {isLoading ? (
                                        <ChatCardSkeleton variant="desktop" theater={theaterMode} />
                                    ) : (
                                        <Container
                                            variant="desktop"
                                            theaterMode={theaterMode}
                                            t={t}
                                            chatCardRef={chatCardRefDesktop}
                                            chatMessagesRef={chatMessagesRefDesktop}
                                            chatInputBarRef={chatInputBarRefDesktop}
                                            chatWriteAreaRef={chatWriteAreaRefDesktop}
                                            participantsBtnRef={participantsBtnRefDesktop}
                                            settingsBtnRef={settingsBtnRefDesktop}
                                            adminBtnRef={adminBtnRefDesktop}
                                            participantsOpen={participantsOpen}
                                            settingsOpen={settingsOpen}
                                            adminOpen={adminOpen}
                                            isSuperAdmin={isSuperAdmin}
                                            onCloseChat={() => setChatOpen(false)}
                                            onParticipantsToggle={() => {
                                                if (participantsOpen) closeParticipants()
                                                else openParticipants()
                                            }}
                                            onSettingsToggle={() => {
                                                if (settingsOpen) closeSettings()
                                                else openSettings()
                                            }}
                                            onAdminToggle={() => {
                                                if (adminOpen) closeAdmin()
                                                else openAdmin()
                                            }}
                                            participantsPopover={
                                                participantsOpen ? (
                                                    <ParticipantsPopoverViewerCard
                                                        mode="desktop"
                                                        panelRef={participantsRefDesktop}
                                                        maxH={participantsMaxHDesktop}
                                                        shiftX={participantsShiftXDesktop}
                                                        t={t}
                                                        participantsClosing={participantsClosing}
                                                        participantsEntering={participantsEntering}
                                                        onClose={closeParticipants}
                                                        sections={participantSections}
                                                        onUserClick={(e: ReactMouseEvent<HTMLButtonElement>, u: ParticipantItem) => openViewerCard('desktop', e, u)}
                                                        streamerId={stream.userId}
                                                        viewerCard={viewerCard}
                                                        viewerCardRef={viewerCardRef}
                                                        viewerCardDepth={viewerCardDepth}
                                                        viewerCardBanStatus={viewerCardBanStatus}
                                                        userAdminInfo={userAdminInfo}
                                                        onCloseViewerCard={() => {
                                                            setViewerCard(null)
                                                            setViewerCardBanStatus(null)
                                                            setUserAdminInfo(null)
                                                        }}
                                                        onBackToRoot={() => setViewerCardDepth('root')}
                                                        onGift={(u: ParticipantItem) => {
                                                            openGift(participantToGiftRecipient(u))
                                                            setViewerCard(null)
                                                        }}
                                                        onBan={handleBanClick}
                                                        onUnban={handleUnban}
                                                        onAddMod={handleAddMod}
                                                        onRemoveMod={handleRemoveMod}
                                                        isModerator={isModerator}
                                                        isStreamer={isOwnStream}
                                                        isSuperAdmin={isSuperAdmin}
                                                        currentUserId={currentUserId}
                                                    />
                                                ) : undefined
                                            }
                                            settingsPanel={
                                                settingsOpen ? (
                                                    <SettingsPanel
                                                        panelRef={settingsRefDesktop}
                                                        maxH={settingsMaxHDesktop}
                                                        t={t}
                                                        settingsClosing={settingsClosing}
                                                        settingsEntering={settingsEntering}
                                                        settingsView={settingsView}
                                                        setSettingsView={setSettingsView}
                                                        closeSettings={closeSettings}
                                                        clearChat={clearChat}
                                                        popoutChat={popoutChat}
                                                        chatFontLevel={chatFontLevel}
                                                        setChatFontLevel={setChatFontLevel}
                                                        chatFontSizePx={chatFontSizePx}
                                                        chatLineHeightPx={chatLineHeightPx}
                                                        emojiShow={emojiShow}
                                                        setEmojiShow={setEmojiShow}
                                                        streamerName={stream.channelName}
                                                        highlightMentions={highlightMentions}
                                                        setHighlightMentions={setHighlightMentions}
                                                        hideSystemMessages={hideSystemMessages}
                                                        setHideSystemMessages={setHideSystemMessages}
                                                        showGiftMessages={showGiftMessages}
                                                        setShowGiftMessages={setShowGiftMessages}
                                                        showUserPreferences={isLoggedIn}
                                                    />
                                                ) : undefined
                                            }
                                            adminPanel={
                                                adminOpen ? (
                                                    <AdminPanel
                                                        panelRef={adminRefDesktop}
                                                        maxH={adminMaxHDesktop}
                                                        t={t}
                                                        adminClosing={adminClosing}
                                                        adminEntering={adminEntering}
                                                        chatDisabled={chatDisabled}
                                                        onToggleChat={handleToggleChat}
                                                        onClose={closeAdmin}
                                                    />
                                                ) : undefined
                                            }
                                            chatFontSizePx={chatFontSizePx}
                                            chatLineHeightPx={chatLineHeightPx}
                                            tipBelowOnlyClass={tipBelowOnlyClass}
                                            pinnedMessage={
                                                pinnedMessage ? (
                                                    <PinnedMessage
                                                        message={pinnedMessage}
                                                        mode="desktop"
                                                        streamerId={stream.userId}
                                                        currentUserId={currentUserId}
                                                        openChatViewerCard={openChatViewerCard}
                                                        openMentionViewerCard={openMentionViewerCard}
                                                        onUnpin={handleUnpinMessage}
                                                        isModerator={isModerator}
                                                        emojiItems={emojiDefaultItems}
                                                        getUserAvatar={(userId) => {
                                                            if (!userId) return null
                                                            const fromParticipants = participantsInfo.get(userId)?.avatar
                                                            if (fromParticipants) return fromParticipants
                                                            if (userId === stream.userId) return stream.avatar ?? null
                                                            return null
                                                        }}
                                                        modUserIds={modUserIds}
                                                        superAdminUserIds={superAdminUserIds}
                                                        t={t}
                                                        chatFontSizePx={chatFontSizePx}
                                                        chatLineHeightPx={chatLineHeightPx}
                                                        showGiftMessages={showGiftMessages}
                                                    />
                                                ) : undefined
                                            }
                                            messagesContent={
                                                <MessageList
                                                    messages={messages}
                                                    mode="desktop"
                                                    streamerId={stream.userId}
                                                    currentUserId={currentUserId}
                                                    mentionMessageRefs={mentionMessageRefs}
                                                    isMessageMentioningMe={isMessageMentioningMe}
                                                    openChatViewerCard={openChatViewerCard}
                                                    openMentionViewerCard={openMentionViewerCard}
                                                    onPinMessage={handlePinMessage}
                                                    onUnpinMessage={handleUnpinMessage}
                                                    onDeleteMessage={handleDeleteMessageClick}
                                                    isModerator={isModerator}
                                                    highlightMentions={highlightMentions}
                                                    hideSystemMessages={hideSystemMessages}
                                                    showGiftMessages={showGiftMessages}
                                                    emojiItems={emojiDefaultItems}
                                                    getUserAvatar={(userId) => {
                                                        if (!userId) return null
                                                        const fromParticipants = participantsInfo.get(userId)?.avatar
                                                        if (fromParticipants) return fromParticipants
                                                        if (userId === stream.userId) return stream.avatar ?? null
                                                        return null
                                                    }}
                                                    modUserIds={modUserIds}
                                                    superAdminUserIds={superAdminUserIds}
                                                    t={t}
                                                />
                                            }
                                            messagesOverlay={
                                                <MessagesOverlay
                                                    mode="desktop"
                                                    isAtBottom={isAtBottomDesktop}
                                                    latestMessage={chatScrollPreviewMessage}
                                                    onScrollToBottom={() => scrollToBottom('desktop')}
                                                    streamerId={stream.userId}
                                                    modUserIds={modUserIds}
                                                    superAdminUserIds={superAdminUserIds}
                                                    unreadMentionCount={unreadMentionMessages.size}
                                                    onScrollToMention={() => {
                                                        const firstUnreadId = Array.from(unreadMentionMessages)[0]
                                                        if (firstUnreadId) {
                                                            scrollToMentionMessage(firstUnreadId, 'desktop')
                                                        }
                                                    }}
                                                    chatViewerCard={chatViewerCard}
                                                    chatViewerCardRef={chatViewerCardRef}
                                                    viewerCardDepth="root"
                                                    viewerCardBanStatus={viewerCardBanStatus}
                                                    bannedUserIds={bannedUserIds}
                                                    userAdminInfo={userAdminInfo}
                                                    onCloseViewerCard={() => {
                                                        setChatViewerCard(null)
                                                        setViewerCardBanStatus(null)
                                                    }}
                                                    onBackToRoot={() => { }}
                                                    onGift={(u: ParticipantItem) => {
                                                        openGift(participantToGiftRecipient(u))
                                                        setChatViewerCard(null)
                                                    }}
                                                    onBan={handleBanClick}
                                                    onUnban={handleUnban}
                                                    onAddMod={handleAddMod}
                                                    onRemoveMod={handleRemoveMod}
                                                    isModerator={isModerator}
                                                    isStreamer={isOwnStream}
                                                    isSuperAdmin={isSuperAdmin}
                                                    currentUserId={currentUserId}
                                                    t={t}
                                                />
                                            }
                                            floatingOverlay={
                                                viewerCard?.mode === 'desktop' && viewerCard ? (
                                                    <ViewerCard
                                                        cardRef={viewerCardRef}
                                                        viewerCard={viewerCard}
                                                        viewerCardDepth={viewerCardDepth}
                                                        t={t}
                                                        onClose={() => {
                                                            setViewerCard(null)
                                                            setViewerCardBanStatus(null)
                                                            setUserAdminInfo(null)
                                                        }}
                                                        onBackToRoot={() => setViewerCardDepth('root')}
                                                        onGift={(u: ParticipantItem) => {
                                                            openGift(participantToGiftRecipient(u))
                                                            setViewerCard(null)
                                                        }}
                                                        isModerator={isModerator}
                                                        isStreamer={isOwnStream}
                                                        isSuperAdmin={isSuperAdmin}
                                                        onBan={handleBanClick}
                                                        onUnban={handleUnban}
                                                        isBanned={resolveViewerCardIsBanned(
                                                            viewerCard.user.id,
                                                            bannedUserIds,
                                                            viewerCardBanStatus
                                                        )}
                                                        currentUserId={currentUserId}
                                                        userAdminInfo={userAdminInfo}
                                                        onAddMod={handleAddMod}
                                                        onRemoveMod={handleRemoveMod}
                                                        streamerId={stream.userId}
                                                    />
                                                ) : undefined
                                            }
                                            inputContent={
                                                <InputArea
                                                    variant="desktop"
                                                    chatWriteAreaRef={chatWriteAreaRefDesktop}
                                                    emojiBtnRef={emojiBtnRefDesktop}
                                                    emojiPanelRef={emojiPanelRefDesktop}
                                                    chatCardRef={chatCardRefDesktop}
                                                    chatInputBarRef={chatInputBarRefDesktop}
                                                    draft={chatDraftDesktop}
                                                    setDraft={setChatDraftDesktop}
                                                    emojiOpen={emojiOpen}
                                                    emojiMode={emojiMode}
                                                    emojiTab={emojiTab}
                                                    setEmojiTab={setEmojiTab}
                                                    emojiDefaultItems={emojiDefaultItems}
                                                    emojiClosing={emojiClosing}
                                                    emojiEntering={emojiEntering}
                                                    onSend={() => sendChat('desktop')}
                                                    onEmojiToggle={() => {
                                                        if (emojiOpen) closeEmoji()
                                                        else openEmoji('desktop')
                                                    }}
                                                    onEmojiClose={() => closeEmoji()}
                                                    onGiftClick={() => openGift()}
                                                    placeholder={currentUserId ? t('room.chatPlaceholder') : t('room.loginRequired')}
                                                    disabled={!currentUserId}
                                                    banInfo={banInfo}
                                                    mentionUsers={mentionUsers}
                                                    onCommand={handleUserCommand}
                                                    chatDisabled={chatDisabled}
                                                    isSuperAdmin={isSuperAdmin}
                                                    tipAboveOnlyClass={tipAboveOnlyClass}
                                                    t={t}
                                                    showGiftButton={!!currentUserId && !isOwnStream}
                                                />
                                            }
                                        />
                                    )}
                                </aside>
                            ) : chatOpen && chatPopoutOpen ? (
                                <aside className="hidden [@media(min-width:965px)]:block w-[360px] xl:w-[380px] flex-shrink-0">
                                    <div className="relative bg-white dark:bg-[#0c0d0e] rounded-xl border border-gray-200 dark:border-gray-800 flex flex-col items-center justify-center p-6 min-h-[520px]">
                                        <p className="text-sm text-gray-600 dark:text-gray-400 mb-4 text-center">
                                            {t('room.chat.popoutActive')}
                                        </p>
                                        <button
                                            type="button"
                                            onClick={restoreChat}
                                            className="px-4 py-2 bg-gray-900 dark:bg-gray-100 hover:bg-gray-800 dark:hover:bg-gray-200 text-white dark:text-gray-900 rounded-lg transition-colors"
                                        >
                                            {t('room.chat.restoreChat')}
                                        </button>
                                    </div>
                                </aside>
                            ) : null}
                        </div>

                        {/* 聊天室重新開啟 via 播放器右上角按鈕 */}

                        {/* 手機模式: 分頁 + 底部面板 */}
                        <div className="[@media(min-width:965px)]:hidden">
                            <div className="h-[calc(100vh-15rem)] flex flex-col overflow-hidden">
                                {/* 手機模式: 操作列 */}
                                <div
                                    className={`-mx-4 px-4 pt-2 pb-2 border-b border-gray-200 dark:border-gray-800 relative ${theaterMode ? 'hidden' : ''
                                        }`}
                                >
                                    {isLoading ? (
                                        <MobileActionBarSkeleton />
                                    ) : (
                                        <ul className="relative z-[1] inline-flex items-center justify-between gap-1.5 w-full">
                                            <li className="flex-shrink-0 text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
                                                {isLiveStreamUi ? (
                                                    <>
                                                        <span className="inline-flex items-center gap-1 align-middle text-[#b00020] dark:text-[#ff1744]">
                                                            <FaUser className="w-3.5 h-3.5" aria-hidden="true" />
                                                            <span>{viewersCount.toLocaleString()}</span>
                                                        </span>
                                                        <span className="mx-1.5">·</span>
                                                        <span
                                                            title={streamStartedAtTitle || undefined}
                                                            className="cursor-default"
                                                        >
                                                            {formatLiveDuration(stream.startedAt, liveNowMs) ||
                                                                String(stream.startedAt ?? '')}
                                                        </span>
                                                    </>
                                                ) : (
                                                    <>
                                                        <span className="inline-flex items-center gap-1 align-middle text-gray-600 dark:text-gray-300">
                                                            <FaUser className="w-3.5 h-3.5" aria-hidden="true" />
                                                            <span>{viewersCount.toLocaleString()}</span>
                                                        </span>
                                                        <span className="mx-1.5">·</span>
                                                        <span
                                                            title={streamStartedAtTitle || undefined}
                                                            className="cursor-default"
                                                        >
                                                            {formatLastStreamAtLabel(streamStartedAt, liveNowMs)}
                                                        </span>
                                                    </>
                                                )}
                                            </li>
                                            <li className="flex items-center gap-1.5 ml-auto">
                                                <LikeFollowActionGroup
                                                    variant="compact"
                                                    showFollow={false}
                                                    showFollowCount
                                                    followCountReadOnly={isOwnStream}
                                                    showLike={isLoggedIn && !isOwnStream}
                                                    isFollowing={isFollowing}
                                                    isLiked={isLiked}
                                                    followCount={followCount}
                                                    likeCount={likeCount}
                                                    followTitle={
                                                        isFollowing ? t('room.unfollow') : t('room.follow')
                                                    }
                                                    likeTitle={isLiked ? t('room.unlike') : t('room.like')}
                                                    onToggleLike={
                                                        isLoggedIn && !isOwnStream ? toggleLike : undefined
                                                    }
                                                    onToggleFollow={
                                                        isLoggedIn && !isOwnStream ? toggleFollow : undefined
                                                    }
                                                    flyingStars={flyingStars}
                                                    flyingHearts={flyingHearts}
                                                />
                                                <button
                                                    type="button"
                                                    onClick={openShare}
                                                    tip={t('room.share')}
                                                    aria-label={t('room.share')}
                                                    className={tipButtonClass}
                                                >
                                                    <span className="inline-flex w-9 h-9 rounded-[10px] items-center justify-center border-0 bg-[#f2f2f2] dark:bg-white/[0.12] text-[#0f0f0f] dark:text-[#f1f1f1] transition-colors hover:bg-[#e5e5e5] dark:hover:bg-white/[0.18] active:bg-[#d9d9d9] dark:active:bg-white/20">
                                                        <FiShare2 className="w-5 h-5" aria-hidden="true" />
                                                    </span>
                                                </button>
                                            </li>
                                        </ul>
                                    )}
                                </div>

                                {/* 手機模式: 直播資訊（劇院模式隱藏） */}
                                <div className={`mt-4 flex items-center gap-4 ${theaterMode ? 'hidden' : ''}`}>
                                    {isLoading ? (
                                        <MobileInfoSkeleton />
                                    ) : (
                                        <>
                                            <button
                                                type="button"
                                                onClick={(e) => void openViewerCard('mobile', e, streamerParticipantForViewerCard)}
                                                className="relative w-12 h-12 rounded-full overflow-hidden border border-gray-200 dark:border-gray-700 bg-gray-100 dark:bg-white/10 flex-shrink-0 flex items-center justify-center cursor-pointer hover:opacity-80 transition-opacity"
                                                aria-label={stream.channelName}
                                            >
                                                {stream.avatar ? (
                                                    <Image src={stream.avatar} alt={stream.channelName} fill className="pointer-events-none object-cover" unoptimized />
                                                ) : (
                                                    <FaUser className="w-5 h-5 text-gray-400 dark:text-white/50" aria-hidden="true" />
                                                )}
                                            </button>
                                            <div className="min-w-0 flex-1">
                                                <div className="flex items-start gap-2">
                                                    <div className="min-w-0 flex-1">
                                                        <div className="inline-block text-base font-semibold text-gray-900 dark:text-white leading-[1.4]" style={{ wordBreak: 'break-all' }}>
                                                            <span className="inline whitespace-nowrap mr-1.5">
                                                                <div className="relative inline-flex items-center gap-1.5">
                                                                    <button
                                                                        type="button"
                                                                        onClick={(e) =>
                                                                            void openViewerCard('mobile', e, streamerParticipantForViewerCard)
                                                                        }
                                                                        className="font-semibold text-gray-900 dark:text-white cursor-pointer hover:opacity-80 transition-opacity"
                                                                        aria-label={stream.channelName}
                                                                    >
                                                                        {stream.channelName}
                                                                    </button>
                                                                    {stream.isVerified && (
                                                                        <MdVerified
                                                                            className="w-4 h-4 text-cyan-400 flex-shrink-0"
                                                                            title={t('catch.verified')}
                                                                        />
                                                                    )}
                                                                    {(() => {
                                                                        const v = String(stream.vipLevel ?? '').toLowerCase()
                                                                        if (v !== 'vip' && v !== 'vvip') return null
                                                                        return (
                                                                            <Image
                                                                                src={v === 'vvip' ? '/images/vvip.png' : '/images/vip.png'}
                                                                                alt=""
                                                                                width={36}
                                                                                height={36}
                                                                                className="w-9 h-9 flex-shrink-0 object-contain"
                                                                            />
                                                                        )
                                                                    })()}

                                                                </div>
                                                            </span>
                                                            <span className="mx-1.5 text-gray-400 dark:text-gray-500">·</span>
                                                            <span>{stream.title}</span>
                                                        </div>
                                                    </div>
                                                </div>
                                            </div>
                                        </>
                                    )}
                                </div>

                                {!isLoading && !theaterMode && (
                                    <RoomStreamDescription
                                        className="mt-2 [@media(min-width:965px)]:hidden"
                                        metaLine=""
                                        expandedMetaLine=""
                                        description={stream.description ?? ''}
                                        category={stream.category}
                                    />
                                )}

                                <div
                                    className={`${theaterMode ? 'mt-0' : 'mt-5'} flex-1 min-h-0 flex flex-col`}
                                >
                                    {isLoading ? (
                                        <ChatCardSkeleton variant="mobile" theater={theaterMode} />
                                    ) : chatPopoutOpen ? (
                                        <div className="flex-1 min-h-0 flex flex-col items-center justify-center p-4 bg-white dark:bg-[#0c0d0e] rounded-xl border border-gray-200 dark:border-gray-800">
                                            <p className="text-sm text-gray-600 dark:text-gray-400 mb-4 text-center">
                                                {t('room.chat.popoutActive')}
                                            </p>
                                            <button
                                                type="button"
                                                onClick={restoreChat}
                                                className="px-4 py-2 bg-gray-900 dark:bg-gray-100 hover:bg-gray-800 dark:hover:bg-gray-200 text-white dark:text-gray-900 rounded-lg transition-colors"
                                            >
                                                {t('room.chat.restoreChat')}
                                            </button>
                                        </div>
                                    ) : chatOpen ? (
                                        <Container
                                            variant="mobile"
                                            theaterMode={theaterMode}
                                            t={t}
                                            chatCardRef={chatCardRefMobile}
                                            chatMessagesRef={chatMessagesRefMobile}
                                            chatInputBarRef={chatInputBarRefMobile}
                                            chatWriteAreaRef={chatWriteAreaRefMobile}
                                            participantsBtnRef={participantsBtnRefMobile}
                                            settingsBtnRef={settingsBtnRefMobile}
                                            adminBtnRef={adminBtnRefMobile}
                                            participantsOpen={participantsOpen}
                                            settingsOpen={settingsOpen}
                                            adminOpen={adminOpen}
                                            isSuperAdmin={isSuperAdmin}
                                            onCloseChat={() => { }}
                                            onParticipantsToggle={() => {
                                                if (participantsOpen) closeParticipants()
                                                else openParticipants()
                                            }}
                                            onSettingsToggle={() => {
                                                if (settingsOpen) closeSettings()
                                                else openSettings()
                                            }}
                                            onAdminToggle={() => {
                                                if (adminOpen) closeAdmin()
                                                else openAdmin()
                                            }}
                                            participantsPopover={
                                                participantsOpen ? (
                                                    <ParticipantsPopoverViewerCard
                                                        mode="mobile"
                                                        panelRef={participantsRefMobile}
                                                        maxH={participantsMaxHMobile}
                                                        shiftX={participantsShiftXMobile}
                                                        t={t}
                                                        participantsClosing={participantsClosing}
                                                        participantsEntering={participantsEntering}
                                                        onClose={closeParticipants}
                                                        sections={participantSections}
                                                        onUserClick={(e: ReactMouseEvent<HTMLButtonElement>, u: ParticipantItem) => openViewerCard('mobile', e, u)}
                                                        streamerId={stream.userId}
                                                        viewerCard={viewerCard}
                                                        viewerCardRef={viewerCardRef}
                                                        viewerCardDepth={viewerCardDepth}
                                                        viewerCardBanStatus={viewerCardBanStatus}
                                                        userAdminInfo={userAdminInfo}
                                                        onCloseViewerCard={() => {
                                                            setViewerCard(null)
                                                            setViewerCardBanStatus(null)
                                                            setUserAdminInfo(null)
                                                        }}
                                                        onBackToRoot={() => setViewerCardDepth('root')}
                                                        onGift={(u: ParticipantItem) => {
                                                            openGift(participantToGiftRecipient(u))
                                                            setViewerCard(null)
                                                        }}
                                                        onBan={handleBanClick}
                                                        onUnban={handleUnban}
                                                        onAddMod={handleAddMod}
                                                        onRemoveMod={handleRemoveMod}
                                                        isModerator={isModerator}
                                                        isStreamer={isOwnStream}
                                                        isSuperAdmin={isSuperAdmin}
                                                        currentUserId={currentUserId}
                                                    />
                                                ) : undefined
                                            }
                                            settingsPanel={
                                                settingsOpen ? (
                                                    <SettingsPanel
                                                        panelRef={settingsRefMobile}
                                                        maxH={settingsMaxHMobile}
                                                        t={t}
                                                        settingsClosing={settingsClosing}
                                                        settingsEntering={settingsEntering}
                                                        settingsView={settingsView}
                                                        setSettingsView={setSettingsView}
                                                        closeSettings={closeSettings}
                                                        clearChat={clearChat}
                                                        popoutChat={popoutChat}
                                                        chatFontLevel={chatFontLevel}
                                                        setChatFontLevel={setChatFontLevel}
                                                        chatFontSizePx={chatFontSizePx}
                                                        chatLineHeightPx={chatLineHeightPx}
                                                        emojiShow={emojiShow}
                                                        setEmojiShow={setEmojiShow}
                                                        isPopout={true}
                                                        streamerName={stream.channelName}
                                                        highlightMentions={highlightMentions}
                                                        setHighlightMentions={setHighlightMentions}
                                                        hideSystemMessages={hideSystemMessages}
                                                        setHideSystemMessages={setHideSystemMessages}
                                                        showGiftMessages={showGiftMessages}
                                                        setShowGiftMessages={setShowGiftMessages}
                                                        showUserPreferences={isLoggedIn}
                                                    />
                                                ) : undefined
                                            }
                                            adminPanel={
                                                adminOpen ? (
                                                    <AdminPanel
                                                        panelRef={adminRefMobile}
                                                        maxH={adminMaxHMobile}
                                                        t={t}
                                                        adminClosing={adminClosing}
                                                        adminEntering={adminEntering}
                                                        chatDisabled={chatDisabled}
                                                        onToggleChat={handleToggleChat}
                                                        onClose={closeAdmin}
                                                    />
                                                ) : undefined
                                            }
                                            chatFontSizePx={chatFontSizePx}
                                            chatLineHeightPx={chatLineHeightPx}
                                            tipBelowOnlyClass={tipBelowOnlyClass}
                                            pinnedMessage={
                                                pinnedMessage ? (
                                                    <PinnedMessage
                                                        message={pinnedMessage}
                                                        mode="mobile"
                                                        streamerId={stream.userId}
                                                        currentUserId={currentUserId}
                                                        openChatViewerCard={openChatViewerCard}
                                                        openMentionViewerCard={openMentionViewerCard}
                                                        onUnpin={handleUnpinMessage}
                                                        isModerator={isModerator}
                                                        emojiItems={emojiDefaultItems}
                                                        getUserAvatar={(userId) => {
                                                            if (!userId) return null
                                                            const fromParticipants = participantsInfo.get(userId)?.avatar
                                                            if (fromParticipants) return fromParticipants
                                                            if (userId === stream.userId) return stream.avatar ?? null
                                                            return null
                                                        }}
                                                        modUserIds={modUserIds}
                                                        superAdminUserIds={superAdminUserIds}
                                                        t={t}
                                                        chatFontSizePx={chatFontSizePx}
                                                        chatLineHeightPx={chatLineHeightPx}
                                                        showGiftMessages={showGiftMessages}
                                                    />
                                                ) : undefined
                                            }
                                            messagesContent={
                                                <MessageList
                                                    messages={messages}
                                                    mode="mobile"
                                                    streamerId={stream.userId}
                                                    currentUserId={currentUserId}
                                                    mentionMessageRefs={mentionMessageRefs}
                                                    isMessageMentioningMe={isMessageMentioningMe}
                                                    openChatViewerCard={openChatViewerCard}
                                                    openMentionViewerCard={openMentionViewerCard}
                                                    onPinMessage={handlePinMessage}
                                                    onUnpinMessage={handleUnpinMessage}
                                                    onDeleteMessage={handleDeleteMessageClick}
                                                    isModerator={isModerator}
                                                    highlightMentions={highlightMentions}
                                                    hideSystemMessages={hideSystemMessages}
                                                    showGiftMessages={showGiftMessages}
                                                    emojiItems={emojiDefaultItems}
                                                    getUserAvatar={(userId) => {
                                                        if (!userId) return null
                                                        const fromParticipants = participantsInfo.get(userId)?.avatar
                                                        if (fromParticipants) return fromParticipants
                                                        if (userId === stream.userId) return stream.avatar ?? null
                                                        return null
                                                    }}
                                                    modUserIds={modUserIds}
                                                    superAdminUserIds={superAdminUserIds}
                                                    t={t}
                                                />
                                            }
                                            messagesOverlay={
                                                <MessagesOverlay
                                                    mode="mobile"
                                                    isAtBottom={isAtBottomMobile}
                                                    latestMessage={chatScrollPreviewMessage}
                                                    onScrollToBottom={() => scrollToBottom('mobile')}
                                                    streamerId={stream.userId}
                                                    modUserIds={modUserIds}
                                                    superAdminUserIds={superAdminUserIds}
                                                    unreadMentionCount={unreadMentionMessages.size}
                                                    onScrollToMention={() => {
                                                        const firstUnreadId = Array.from(unreadMentionMessages)[0]
                                                        if (firstUnreadId) {
                                                            scrollToMentionMessage(firstUnreadId, 'mobile')
                                                        }
                                                    }}
                                                    chatViewerCard={chatViewerCard}
                                                    chatViewerCardRef={chatViewerCardRef}
                                                    viewerCardDepth={viewerCardDepth}
                                                    viewerCardBanStatus={viewerCardBanStatus}
                                                    bannedUserIds={bannedUserIds}
                                                    userAdminInfo={userAdminInfo}
                                                    onCloseViewerCard={() => {
                                                        setChatViewerCard(null)
                                                        setViewerCardBanStatus(null)
                                                        setViewerCardDepth('root')
                                                        setUserAdminInfo(null)
                                                    }}
                                                    onBackToRoot={() => {
                                                        setViewerCardDepth('root')
                                                    }}
                                                    onGift={(u: ParticipantItem) => {
                                                        openGift(participantToGiftRecipient(u))
                                                        setChatViewerCard(null)
                                                    }}
                                                    onBan={handleBanClick}
                                                    onUnban={handleUnban}
                                                    onAddMod={handleAddMod}
                                                    onRemoveMod={handleRemoveMod}
                                                    isModerator={isModerator}
                                                    isStreamer={isOwnStream}
                                                    isSuperAdmin={isSuperAdmin}
                                                    currentUserId={currentUserId}
                                                    t={t}
                                                />
                                            }
                                            floatingOverlay={
                                                viewerCard?.mode === 'mobile' && viewerCard ? (
                                                    <ViewerCard
                                                        cardRef={viewerCardRef}
                                                        viewerCard={viewerCard}
                                                        viewerCardDepth={viewerCardDepth}
                                                        t={t}
                                                        onClose={() => {
                                                            setViewerCard(null)
                                                            setViewerCardBanStatus(null)
                                                            setUserAdminInfo(null)
                                                        }}
                                                        onBackToRoot={() => setViewerCardDepth('root')}
                                                        onGift={(u: ParticipantItem) => {
                                                            openGift(participantToGiftRecipient(u))
                                                            setViewerCard(null)
                                                        }}
                                                        isModerator={isModerator}
                                                        isStreamer={isOwnStream}
                                                        isSuperAdmin={isSuperAdmin}
                                                        onBan={handleBanClick}
                                                        onUnban={handleUnban}
                                                        isBanned={resolveViewerCardIsBanned(
                                                            viewerCard.user.id,
                                                            bannedUserIds,
                                                            viewerCardBanStatus
                                                        )}
                                                        currentUserId={currentUserId}
                                                        userAdminInfo={userAdminInfo}
                                                        onAddMod={handleAddMod}
                                                        onRemoveMod={handleRemoveMod}
                                                        streamerId={stream.userId}
                                                    />
                                                ) : undefined
                                            }
                                            inputContent={
                                                <InputArea
                                                    variant="mobile"
                                                    chatWriteAreaRef={chatWriteAreaRefMobile}
                                                    emojiBtnRef={emojiBtnRefMobile}
                                                    emojiPanelRef={emojiPanelRefMobile}
                                                    chatCardRef={chatCardRefMobile}
                                                    chatInputBarRef={chatInputBarRefMobile}
                                                    draft={chatDraftMobile}
                                                    setDraft={setChatDraftMobile}
                                                    emojiOpen={emojiOpen}
                                                    emojiMode={emojiMode}
                                                    emojiTab={emojiTab}
                                                    setEmojiTab={setEmojiTab}
                                                    emojiDefaultItems={emojiDefaultItems}
                                                    emojiClosing={emojiClosing}
                                                    emojiEntering={emojiEntering}
                                                    onSend={() => sendChat('mobile')}
                                                    onEmojiToggle={() => {
                                                        if (emojiOpen) closeEmoji()
                                                        else openEmoji('mobile')
                                                    }}
                                                    onEmojiClose={() => closeEmoji()}
                                                    onGiftClick={() => openGift()}
                                                    placeholder={currentUserId ? t('room.chatPlaceholder') : t('room.loginRequired')}
                                                    disabled={!currentUserId}
                                                    banInfo={banInfo}
                                                    mentionUsers={mentionUsers}
                                                    onCommand={handleUserCommand}
                                                    chatDisabled={chatDisabled}
                                                    isSuperAdmin={isSuperAdmin}
                                                    tipAboveOnlyClass={tipAboveOnlyClass}
                                                    t={t}
                                                    showGiftButton={!!currentUserId && !isOwnStream}
                                                />
                                            }
                                        />
                                    ) : null}
                                </div>
                            </div>
                        </div>
                    </div>
                </main>
                <BackToTop />
            </div>

            <SharePanel
                isOpen={isShareOpen}
                onClose={() => setIsShareOpen(false)}
                videoUrl={shareUrl}
                videoTitle={stream.title}
                embedIframeHtml={embedIframeHtml}
                lockBodyScroll={false}
            />

            {/* 禮物 modal */}
            {giftOpen && (
                <GiftModal
                    giftRef={giftRef}
                    giftClosing={giftClosing}
                    giftEntering={giftEntering}
                    closeGift={() => closeGift()}
                    giftSelected={giftSelected}
                    setGiftSelected={setGiftSelected}
                    giftRecipient={giftRecipient}
                    streamFallback={{
                        channelName: stream.channelName,
                        userId: stream.userId,
                        displayId: initialRoomPublicId ?? null,
                    }}
                    giftCount={giftCount}
                    setGiftCount={setGiftCount}
                    onSendGift={sendGift}
                    giftSending={giftSending}
                    coinBalance={coinBalance}
                    coinBalanceLoading={coinBalanceLoading}
                />
            )}

            {banModalOpen && banTargetUser && (
                <BanModal
                    banRef={banRef}
                    isOpen={banModalOpen}
                    userName={banTargetUser.name}
                    userId={banTargetUser.id}
                    roomId={roomId}
                    onClose={() => closeBan()}
                    onBan={handleBan}
                    t={t}
                    banClosing={banClosing}
                    banEntering={banEntering}
                />
            )}
            {confirmModalOpen && (
                <ConfirmModal
                    confirmRef={confirmRef}
                    isOpen={confirmModalOpen}
                    title={confirmTitle}
                    message={confirmMessage}
                    confirmText={t('common.confirm')}
                    cancelText={t('common.cancel')}
                    onClose={() => closeConfirm()}
                    onConfirm={handleConfirm}
                    confirmClosing={confirmClosing}
                    confirmEntering={confirmEntering}
                    severity={confirmSeverity}
                />
            )}
        </>
    )
}


