'use client'

import { useEffect, useRef, useState, useMemo, useCallback } from 'react'
import { useTranslation } from 'react-i18next'

// _components
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 SettingsPanel from '@/app/room/_components/chat/SettingsPanel'
import AdminPanel from '@/app/room/_components/chat/AdminPanel'
import {
    sendChat as sendChatUtil,
    type SendChatParams,
    isEnterRoomSystemMessage,
} from '@/app/room/_components/chat/utils'
import { encryptWsPayload } from '../../../../utils/wsCrypto'
import {
    buildSendGiftWsPayload,
    GIFT_SEND_COUNT_MAX,
    wsGiftDataToChatMessage,
    appendGiftChatMessage,
} from '@/lib/gifts/wsGift'
import { useUserCoinBalance } from '@/lib/users/useUserCoinBalance'
import { popoutChatFontSizePx } from '@/lib/chat/chatFontLevel'
import type { ChatSettingsView } from '@/lib/chat/chatSettingsView'
import { filterVisibleChatMessages } from '@/lib/chat/filterVisibleChatMessages'
import { buildChatWsUrl, parseChatWsMessage, sendChatJoin } from '@/lib/chat/wsChat'
import { applyModAdded, applyModRemoved } from '@/lib/chat/modRoleState'
import { useSyncParticipantModStatus } from '@/lib/chat/useSyncParticipantModStatus'
import type { ObsChatEmbedOptions } from '@/lib/embed/obsChatEmbed'
import { resolveObsChatFontSizePx } from '@/lib/embed/obsChatEmbed'
import type {
    ChatMessage,
    EmojiInfo,
    GiftRecipient,
    MentionInfo,
    ParticipantItem,
    ParticipantSection,
    ViewerCardState,
    UserAdminInfo,
} from '@/app/room/_components/types'
import {
    buildParticipantIdentity,
    participantToGiftRecipient,
    resolveGiftRecipientDisplayId,
} from '@/app/room/_components/types'
import BanModal from '@/app/room/_components/chat/BanModal'
import { CHAT_EMOJI_ITEMS } from '@/app/room/_components/chat/chatEmojis'
import {
    appendOptimisticBanSystemMessage,
    buildOptimisticBanSystemMessage,
    canViewModeratorOnlySystemMessage,
    mergeIncomingBanSystemMessage,
    normalizeBanSystemMessage,
    resolveViewerCardIsBanned,
} from '@/app/room/_components/chat/banChatHelpers'
import ChatCardSkeleton from '@/app/room/_components/Skeletons/ChatCardSkeleton'

// components
import GiftModal from '@/components/GiftModal'
import ConfirmModal from '@/components/ConfirmModal'

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

interface ChatPopoutPageClientProps {
    roomId: string
    /** 網址列上的房間 key（display_id 優先），與 roomId（UUID）可不同 */
    roomPublicId: string
    streamUserId: string
    /** 主播 users.display_id（有設定時；未連線聊天 WS 時仍用於參與者列表） */
    streamOwnerDisplayId?: string | null
    streamChannelName: string
    streamAvatar: string
    currentUserId?: string | null
    currentUserDisplayName?: string | null
    embed?: boolean
    /** OBS Browser Source 透明聊天疊層 */
    obsOverlay?: ObsChatEmbedOptions | null
}

export default function ChatPopoutPageClient({
    roomId,
    roomPublicId,
    streamUserId,
    streamOwnerDisplayId = null,
    streamChannelName,
    streamAvatar,
    currentUserId,
    currentUserDisplayName,
    embed = false,
    obsOverlay = null,
}: ChatPopoutPageClientProps) {
    const { t } = useTranslation()
    const { showToast } = useToast()

    // Loading state
    const [isLoading, setIsLoading] = useState(true)

    // Refs
    const chatCardRef = useRef<HTMLDivElement>(null)
    const chatMessagesRef = useRef<HTMLDivElement>(null)
    const chatInputBarRef = useRef<HTMLDivElement>(null)
    const chatWriteAreaRef = useRef<HTMLDivElement>(null)
    const participantsBtnRef = useRef<HTMLButtonElement>(null)
    const settingsBtnRef = useRef<HTMLButtonElement>(null)
    const adminBtnRef = useRef<HTMLButtonElement>(null)
    const participantsRef = useRef<HTMLDivElement>(null)
    const settingsRef = useRef<HTMLDivElement>(null)
    const adminRef = useRef<HTMLDivElement>(null)
    const emojiBtnRef = useRef<HTMLButtonElement>(null)
    const emojiPanelRef = useRef<HTMLDivElement>(null)
    const chatViewerCardRef = useRef<HTMLDivElement>(null)
    const viewerCardRef = useRef<HTMLDivElement>(null)
    const mentionMessageRefs = useRef<Map<string, HTMLDivElement>>(new Map())
    const wsRef = useRef<WebSocket | null>(null)
    const reconnectTimeoutRef = useRef<number | null>(null)
    const reconnectAttemptsRef = useRef(0)
    const superAdminUserIdsRef = useRef<Set<string>>(new Set())
    const modUserIdsRef = useRef<Set<string>>(new Set())

    // States
    const [messages, setMessages] = useState<ChatMessage[]>([])
    const [pinnedMessage, setPinnedMessage] = useState<ChatMessage | null>(null)
    const [chatDraft, setChatDraft] = useState('')
    const [participantsOpen, setParticipantsOpen] = useState(false)
    const [settingsOpen, setSettingsOpen] = useState(false)
    const [adminOpen, setAdminOpen] = useState(false)
    const [emojiOpen, setEmojiOpen] = useState(false)
    const [emojiMode, setEmojiMode] = useState<'desktop' | 'mobile'>('desktop')
    const [emojiTab, setEmojiTab] = useState<'recent' | 'subscription' | 'default'>('default')
    const [emojiClosing, setEmojiClosing] = useState(false)
    const [emojiEntering, setEmojiEntering] = useState(false)
    const [participantsClosing, setParticipantsClosing] = useState(false)
    const [participantsEntering, setParticipantsEntering] = useState(false)
    const [settingsClosing, setSettingsClosing] = useState(false)
    const [settingsEntering, setSettingsEntering] = useState(false)
    const [adminClosing, setAdminClosing] = useState(false)
    const [adminEntering, setAdminEntering] = useState(false)
    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 [settingsView, setSettingsView] = useState<ChatSettingsView>('root')
    const [chatFontLevel, setChatFontLevel] = useState(4)
    const [emojiShow, setEmojiShow] = useState(true)
    const [highlightMentions, setHighlightMentions] = useState(true)
    const [hideSystemMessages, setHideSystemMessages] = useState(false)
    const [showGiftMessages, setShowGiftMessages] = useState(true)
    const [chatDisabled, setChatDisabled] = useState(false)
    const [isAtBottom, setIsAtBottom] = useState(true)
    const [unreadMentionMessages, setUnreadMentionMessages] = useState<Set<string>>(new Set())
    const [viewerCard, setViewerCard] = useState<ViewerCardState | null>(null)
    const [chatViewerCard, setChatViewerCard] = useState<ViewerCardState | null>(null)
    const [viewerCardDepth, setViewerCardDepth] = useState<'root' | 'gift' | 'admin'>('root')
    const [viewerCardBanStatus, setViewerCardBanStatus] = useState<{ userId: string; isBanned: boolean } | null>(null)
    const [userAdminInfo, setUserAdminInfo] = useState<UserAdminInfo | null>(null)
    const [onlineParticipants, setOnlineParticipants] = useState<
        Array<{ userId: string | null; author: string; id?: string; publicPathKey?: string | null; displayId?: string | null }>
    >([])
    const [participantsInfo] = useState<Map<string, { avatar?: string | null }>>(new Map())
    const [isMod, setIsMod] = useState(false)
    const [isSuperAdmin, setIsSuperAdmin] = useState(false)
    const [banInfo, setBanInfo] = useState<{
        isPermanent: boolean
        expiresAt: number | null
        remainingMs: number | null
        reason?: string | null
    } | null>(null)
    const [bannedUserIds, setBannedUserIds] = useState<Set<string>>(new Set())
    const [modUserIds, setModUserIds] = useState<Set<string>>(new Set())
    const [superAdminUserIds, setSuperAdminUserIds] = useState<Set<string>>(new Set())

    useSyncParticipantModStatus(
        wsRef,
        onlineParticipants,
        streamUserId,
        roomId,
        setModUserIds,
        setSuperAdminUserIds,
        modUserIdsRef,
        superAdminUserIdsRef
    )

    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 [banModalOpen, setBanModalOpen] = useState(false)
    const [banClosing, setBanClosing] = useState(false)
    const [banEntering, setBanEntering] = useState(false)
    const [banTargetUser, setBanTargetUser] = useState<ParticipantItem | 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 participantsCloseTimerRef = useRef<number | null>(null)
    const settingsCloseTimerRef = useRef<number | null>(null)
    const adminCloseTimerRef = useRef<number | null>(null)

    const isModerator = useMemo(() => currentUserId === streamUserId || isMod || isSuperAdmin, [currentUserId, streamUserId, isMod, isSuperAdmin])
    const isStreamer = currentUserId === streamUserId
    const isModeratorRef = useRef(isModerator)
    useEffect(() => {
        isModeratorRef.current = isModerator
    }, [isModerator])

    const emojiDefaultItems = CHAT_EMOJI_ITEMS

    // Font sizes
    const chatFontSizePx = useMemo(() => {
        if (obsOverlay) return resolveObsChatFontSizePx(obsOverlay, chatFontLevel)
        return popoutChatFontSizePx(chatFontLevel)
    }, [chatFontLevel, obsOverlay])
    const chatLineHeightPx = useMemo(() => {
        return chatFontSizePx * 1.5
    }, [chatFontSizePx])
    const chatScrollPreviewMessage = useMemo(() => {
        const visible = filterVisibleChatMessages(messages, {
            hideSystemMessages,
            showGiftMessages,
            currentUserId,
            isModerator,
        })
        return visible.slice(-1)[0] ?? null
    }, [messages, hideSystemMessages, showGiftMessages, currentUserId, isModerator])

    // Tip classes
    const tipBelowOnlyClass = 'tip-below-only'
    const tipAboveOnlyClass = 'tip-above-only'

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

        const connectWebSocket = () => {
            if (isUnmounting) return
            try {
                const wsUrl = buildChatWsUrl()
                console.log('[Popout] Connecting to WebSocket:', wsUrl)
                const ws = new WebSocket(wsUrl)
                wsRef.current = ws

                ws.onopen = () => {
                    console.log('[Popout] WebSocket connected')
                    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 = parseChatWsMessage(event.data as string)
                        if (!data) return
                        if (data.type === 'message' || data.type === 'message_sent') {
                            const payload = data.data
                            if (!payload) return
                            const userId = payload.userId ?? null
                            let userType: 'streamer' | 'manager' | 'superAdmin' | undefined = payload.userType
                            if (!userType && userId) {
                                if (userId === streamUserId) {
                                    userType = 'streamer'
                                } else if (superAdminUserIdsRef.current.has(userId)) {
                                    userType = 'superAdmin'
                                } else if (modUserIdsRef.current.has(userId)) {
                                    userType = 'manager'
                                }
                            }
                            const message: ChatMessage = normalizeBanSystemMessage({
                                id: payload.id ?? `m-${Date.now()}`,
                                author: payload.author ?? '',
                                text: payload.text ?? '',
                                isSystem: payload.isSystem || false,
                                isModeratorOnly: payload.isModeratorOnly || false,
                                userId,
                                userType,
                                time: payload.createdAtMs
                                    ? new Date(payload.createdAtMs).toLocaleTimeString([], {
                                          hour: '2-digit',
                                          minute: '2-digit',
                                      })
                                    : new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
                                mentions:
                                    Array.isArray(payload.mentions) && payload.mentions.length > 0
                                        ? (payload.mentions as MentionInfo[])
                                        : undefined,
                                emojis:
                                    Array.isArray(payload.emojis) && payload.emojis.length > 0
                                        ? (payload.emojis as EmojiInfo[])
                                        : undefined,
                                bannedUserId: payload.bannedUserId || undefined,
                                unbannedUserId: payload.unbannedUserId || undefined,
                                adminUserId: payload.adminUserId || undefined,
                                adminUserType: payload.adminUserType as ChatMessage['adminUserType'],
                                moddedUserId: payload.moddedUserId || undefined,
                                unmoddedUserId: payload.unmoddedUserId || undefined,
                                systemMessageType: payload.systemMessageType as ChatMessage['systemMessageType'],
                                systemMessageData: payload.systemMessageData as ChatMessage['systemMessageData'],
                                authorPublicPathKey:
                                    typeof payload.authorPublicPathKey === 'string' && payload.authorPublicPathKey.trim()
                                        ? payload.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) => {
                                if (data.type === 'message_sent') {
                                    // 若是自己發送的訊息，已經在本地插入 temp 訊息，這裡就直接忽略
                                    if (message.userId && message.userId === currentUserId) {
                                        return prev
                                    }
                                    const filtered = prev.filter((m) => {
                                        if (m.id.startsWith('temp-')) return false
                                        if (m.id === message.id) return false
                                        return true
                                    })
                                    return mergeIncomingBanSystemMessage(filtered, message)
                                }
                                return 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 =
                                        (currentUserId !== null &&
                                            currentUserId !== undefined &&
                                            currentUserId === streamUserId) ||
                                        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,
                                        currentUserId,
                                        isOwnStream: isStreamer,
                                        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,
                                        currentUserId,
                                        isOwnStream: isStreamer,
                                        modUserIdsRef,
                                        superAdminUserIdsRef,
                                        isModeratorRef,
                                    },
                                    {
                                        setModUserIds,
                                        setIsMod,
                                        setMessages,
                                        setPinnedMessage,
                                        setUserAdminInfo,
                                        setChatViewerCard,
                                    },
                                    removedUserId
                                )
                            }
                        } else if (data.type === 'ban_status') {
                            if (data.data) {
                                if (data.data.userId === currentUserId && data.data.isBanned) {
                                    const banData = data.data
                                    const now = Date.now()
                                    const expiresAt = banData.expiresAt ?? null
                                    const isPermanent = Boolean(banData.isPermanent) || expiresAt === null
                                    const remainingMs = isPermanent
                                        ? null
                                        : banData.remainingMs !== undefined
                                          ? banData.remainingMs ?? null
                                          : expiresAt != null && expiresAt > now
                                            ? expiresAt - now
                                            : 0
                                    if (!isPermanent && expiresAt != null && expiresAt <= now) {
                                        setBanInfo(null)
                                    } else {
                                        setBanInfo({
                                            isPermanent,
                                            expiresAt,
                                            remainingMs,
                                            reason: banData.reason ?? null,
                                        })
                                    }
                                }
                                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)
                                const adminUserId = String(data.data.userId ?? '').trim()
                                if (!adminUserId) return
                                if (data.data.isSuperAdmin) {
                                    setSuperAdminUserIds((prev) => {
                                        const updated = new Set(prev)
                                        updated.add(adminUserId)
                                        superAdminUserIdsRef.current = updated
                                        return updated
                                    })
                                } else {
                                    setSuperAdminUserIds((prev) => {
                                        const updated = new Set(prev)
                                        updated.delete(adminUserId)
                                        superAdminUserIdsRef.current = updated
                                        return updated
                                    })
                                }
                            }
                        } else if (data.type === 'participants_update') {
                            const participants = data.data?.participants ?? []
                            setOnlineParticipants(
                                participants.map((p) => ({
                                    userId: p.userId ?? null,
                                    author: String(p.author ?? ''),
                                    id: p.id,
                                    publicPathKey: p.publicPathKey ?? null,
                                }))
                            )
                        } 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)
                            }
                        } else if (data.type === 'ban_removed') {
                            setBanInfo(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 as
                                                              | 'streamer'
                                                              | 'manager'
                                                              | 'superAdmin'
                                                              | undefined,
                                                      }
                                                    : 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 as ChatMessage
                                setPinnedMessage(pinnedMsg)
                                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],
                                (userId) => {
                                if (!userId) return undefined
                                if (userId === streamUserId) return 'streamer'
                                if (superAdminUserIdsRef.current.has(userId)) return 'superAdmin'
                                if (modUserIdsRef.current.has(userId)) return 'manager'
                                return undefined
                            })
                            if (giftMessage) {
                                setMessages((prev) => appendGiftChatMessage(prev, 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 as Parameters<typeof wsGiftDataToChatMessage>[0],
                                (userId) => {
                                if (!userId) return undefined
                                if (userId === streamUserId) return 'streamer'
                                if (superAdminUserIdsRef.current.has(userId)) return 'superAdmin'
                                if (modUserIdsRef.current.has(userId)) return 'manager'
                                return undefined
                            })
                            if (giftMessage) {
                                setMessages((prev) => appendGiftChatMessage(prev, 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') {
                            if (data.banInfo) {
                                const info = data.banInfo
                                setBanInfo({
                                    isPermanent: Boolean(info.isPermanent),
                                    expiresAt: info.expiresAt ?? null,
                                    remainingMs: info.remainingMs ?? null,
                                    reason: info.reason ?? null,
                                })
                            }
                        }
                    } catch (error) {
                        console.error('[Popout] Failed to parse WebSocket message:', error)
                    }
                }

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

                ws.onclose = () => {
                    console.log('[Popout] 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('[Popout] 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, streamUserId, showToast, t, setCoinBalance])

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

    useEffect(() => {
        const container = chatMessagesRef.current
        if (!container) return
        const checkScrollPosition = () => {
            const { scrollTop, scrollHeight, clientHeight } = container
            const threshold = 50
            setIsAtBottom(scrollHeight - scrollTop - clientHeight < threshold)
        }
        container.addEventListener('scroll', checkScrollPosition)
        checkScrollPosition()
        return () => {
            container.removeEventListener('scroll', checkScrollPosition)
        }
    }, [messages])

    useEffect(() => {
        const el = chatMessagesRef.current
        if (!el) return
        const scrollToEnd = () => {
            el.scrollTop = el.scrollHeight
        }
        if (obsOverlay) {
            scrollToEnd()
            requestAnimationFrame(scrollToEnd)
            return
        }
        if (isAtBottom) {
            el.scrollTo({
                top: el.scrollHeight,
                behavior: 'smooth',
            })
        }
    }, [messages, isAtBottom, obsOverlay])

    const sendChat = () => {
        if (chatDisabled && !isSuperAdmin) return
        const params: SendChatParams = {
            writeAreaRef: chatWriteAreaRef,
            currentUserId,
            currentUserDisplayName,
            streamUserId,
            wsRef,
            setMessages,
            setChatDraftDesktop: setChatDraft,
            setChatDraftMobile: setChatDraft,
            chatWriteAreaRefDesktop: chatWriteAreaRef,
            chatWriteAreaRefMobile: chatWriteAreaRef,
            mode: 'desktop',
            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 scrollToBottom = (smooth = true) => {
        if (chatMessagesRef.current) {
            if (smooth) {
                chatMessagesRef.current.scrollTo({
                    top: chatMessagesRef.current.scrollHeight,
                    behavior: 'smooth'
                })
            } else {
                chatMessagesRef.current.scrollTop = chatMessagesRef.current.scrollHeight
            }
        }
    }

    const scrollToMentionMessage = useCallback((messageId: string) => {
        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 {
            if (chatMessagesRef.current) {
                chatMessagesRef.current.scrollTo({ top: chatMessagesRef.current.scrollHeight, behavior: 'smooth' })
            }
        }
        setUnreadMentionMessages((prev) => {
            const next = new Set(prev)
            next.delete(messageId)
            return next
        })
    }, [])

    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 openChatViewerCard = async (mode: 'desktop' | 'mobile', e: React.MouseEvent<HTMLButtonElement>, message: ChatMessage) => {
        if (!message.author) return
        const containerEl = chatMessagesRef.current
        if (!containerEl) return
        const btnEl = e.currentTarget
        const btnRect = btnEl.getBoundingClientRect()
        const chatCardEl = chatCardRef.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 isSuperAdminUser = message.userId ? superAdminUserIds.has(message.userId) : false
        const userType = message.userId === streamUserId ? 'streamer' : (message.userType || (isSuperAdminUser ? '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 === streamUserId ? 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(
                    JSON.stringify({
                        type: 'check_ban_status',
                        userId: user.id,
                    })
                )
                setViewerCardBanStatus(null)
            }
        }
        if ((isStreamer || isSuperAdmin) && user.id && !user.id.startsWith('guest_')) {
            if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
                wsRef.current.send(
                    JSON.stringify({
                        type: 'get_user_admin',
                        targetUserId: user.id,
                    })
                )
                setUserAdminInfo(null)
            }
        }
    }

    const openMentionViewerCard = async (mode: 'desktop' | 'mobile', e: React.MouseEvent<HTMLButtonElement>, user: ParticipantItem) => {
        const containerEl = chatMessagesRef.current
        if (!containerEl) return
        const btnEl = e.currentTarget
        const btnRect = btnEl.getBoundingClientRect()
        const chatCardEl = chatCardRef.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.role) {
            if (userWithAvatar.id === streamUserId) {
                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)
            }
        }
        if ((isStreamer || 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)
            }
        }
    }

    const openViewerCard = async (mode: 'desktop' | 'mobile', e: React.MouseEvent<HTMLButtonElement>, u: ParticipantItem) => {
        const panelEl = participantsRef.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 === streamUserId ? '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(
                    JSON.stringify({
                        type: 'check_ban_status',
                        userId: u.id,
                    })
                )
                setViewerCardBanStatus(null)
            }
        }
        if ((isStreamer || isSuperAdmin) && u.id && !u.id.startsWith('guest_')) {
            if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
                wsRef.current.send(
                    JSON.stringify({
                        type: 'get_user_admin',
                        targetUserId: u.id,
                    })
                )
                setUserAdminInfo(null)
            }
        }
    }

    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('聊天室連線已中斷，請重新整理頁面後再試', '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' : isStreamer ? '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('[Popout] Failed to ban user via WebSocket:', error)
            showToast('禁言失敗，請稍後再試', 'error')
        }
    }

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

        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' : isStreamer ? '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('[Popout] Failed to unban user via WebSocket:', error)
            showToast(t('room.ban.unbanFailed'), 'error')
        }
    }

    const handleAddMod = async (user: ParticipantItem) => {
        if (!isStreamer && !isSuperAdmin) return
        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,
                    currentUserId,
                    isOwnStream: isStreamer,
                    modUserIdsRef,
                    superAdminUserIdsRef,
                    isModeratorRef,
                },
                {
                    setModUserIds,
                    setIsMod,
                    setMessages,
                    setPinnedMessage,
                    setUserAdminInfo,
                    setChatViewerCard,
                },
                user.id
            )
        } catch (error) {
            console.error('[Popout] Failed to add mod via WebSocket:', error)
            showToast(t('room.viewerCard.addModFailed'), 'error')
        }
    }

    const handleRemoveMod = async (user: ParticipantItem) => {
        if (!isStreamer && !isSuperAdmin) return
        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,
                    currentUserId,
                    isOwnStream: isStreamer,
                    modUserIdsRef,
                    superAdminUserIdsRef,
                    isModeratorRef,
                },
                {
                    setModUserIds,
                    setIsMod,
                    setMessages,
                    setPinnedMessage,
                    setUserAdminInfo,
                    setChatViewerCard,
                },
                user.id
            )
        } catch (error) {
            console.error('[Popout] Failed to remove mod via WebSocket:', error)
            showToast(t('room.viewerCard.removeModFailed'), 'error')
        }
    }

    const openGift = (recipient?: GiftRecipient) => {
        if (giftCloseTimerRef.current) {
            window.clearTimeout(giftCloseTimerRef.current)
            giftCloseTimerRef.current = null
        }
        const baseRecipient =
            recipient ?? {
                name: streamChannelName,
                userId: streamUserId,
                avatarUrl: streamAvatar,
            }
        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
        )
    }

    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: streamChannelName,
                userId: streamUserId,
                avatarUrl: streamAvatar,
            } 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,
        streamAvatar,
        streamChannelName,
        streamUserId,
        t,
    ])

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

    const handlePinMessage = async (messageId: string) => {
        if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
            showToast(t('room.message.pinFailed'), 'error')
            return
        }
        wsRef.current.send(
            JSON.stringify({
                type: 'pin_message',
                messageId,
            })
        )
    }

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

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

    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 = useCallback((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 = useCallback(() => {
        if (confirmCallback) {
            confirmCallback()
        }
        closeConfirm()
    }, [confirmCallback, closeConfirm])

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

    const handleToggleChat = () => {
        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()
    }

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

    const closeParticipants = useCallback((immediate?: boolean) => {
        if (!participantsOpen) return
        if (immediate) {
            if (participantsCloseTimerRef.current) {
                window.clearTimeout(participantsCloseTimerRef.current)
                participantsCloseTimerRef.current = null
            }
            setParticipantsEntering(false)
            setParticipantsOpen(false)
            setParticipantsClosing(false)
            return
        }
        if (participantsClosing) return
        if (participantsCloseTimerRef.current) {
            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])

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

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

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

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

    useEffect(() => {
        if (!participantsOpen) return
        const onMouseDown = (e: globalThis.MouseEvent) => {
            const target = e.target as HTMLElement | null
            if (!target) return
            if (participantsRef.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 (!settingsOpen) return

        const onMouseDown = (e: globalThis.MouseEvent) => {
            const target = e.target as HTMLElement | null
            if (!target) return
            if (settingsRef.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 (adminRef.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 (!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 closeEmoji = useCallback((immediate?: boolean) => {
        setEmojiEntering(false)
        setEmojiClosing(true)
        if (immediate) {
            setEmojiOpen(false)
            setEmojiClosing(false)
        } else {
            setTimeout(() => {
                setEmojiOpen(false)
                setEmojiClosing(false)
            }, 200)
        }
    }, [])

    useEffect(() => {
        if (!emojiOpen) return
        const onMouseDown = (e: globalThis.MouseEvent) => {
            const target = e.target as HTMLElement | null
            if (!target) return
            if (emojiPanelRef.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, closeEmoji])

    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 participantSections: ParticipantSection[] = useMemo(() => {
        const sections: ParticipantSection[] = []
        const streamerOnline = onlineParticipants.find((p) => p.userId === streamUserId)
        const streamerIdentity = buildParticipantIdentity(
            streamUserId,
            streamerOnline?.publicPathKey,
            streamerOnline?.displayId ?? streamOwnerDisplayId ?? null,
        )
        const superAdminUsers: ParticipantItem[] = onlineParticipants
            .filter((p) => p.userId && p.userId !== streamUserId && 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: streamChannelName,
                    id: streamerIdentity.id,
                    displayId: streamerIdentity.displayId,
                    profileApiKey: roomPublicId,
                    avatarUrl: streamAvatar,
                    userType: 'streamer',
                    isBanned: bannedUserIds.has(streamUserId),
                },
            ],
        })
        const modUsers: ParticipantItem[] = onlineParticipants
            .filter((p) => p.userId && p.userId !== streamUserId && 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 !== streamUserId && !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
    }, [streamChannelName, streamAvatar, streamUserId, streamOwnerDisplayId, roomPublicId, onlineParticipants, participantsInfo, modUserIds, superAdminUserIds, bannedUserIds, t])

    return (
        <div
            className={
                obsOverlay
                    ? 'w-full h-full flex flex-col bg-transparent'
                    : embed
                      ? 'w-full h-full flex flex-col bg-transparent'
                      : 'w-full h-screen flex flex-col bg-white dark:bg-[#0c0d0e]'
            }
        >
            {isLoading && !obsOverlay ? (
                <ChatCardSkeleton variant="desktop" theater={false} isPopout={true} />
            ) : (
                <Container
                    variant="desktop"
                    theaterMode={false}
                    isPopout={true}
                    obsOverlay={obsOverlay}
                    t={t}
                    chatCardRef={chatCardRef}
                    chatMessagesRef={chatMessagesRef}
                    chatInputBarRef={chatInputBarRef}
                    chatWriteAreaRef={chatWriteAreaRef}
                    participantsBtnRef={participantsBtnRef}
                    settingsBtnRef={settingsBtnRef}
                    adminBtnRef={adminBtnRef}
                    participantsOpen={participantsOpen}
                    settingsOpen={settingsOpen}
                    adminOpen={adminOpen}
                    isSuperAdmin={isSuperAdmin}
                    onCloseChat={() => {
                        if (!embed) {
                            window.close()
                        }
                    }}
                    onParticipantsToggle={() => {
                        if (participantsOpen) {
                            closeParticipants()
                        } else {
                            if (settingsOpen) closeSettings(true)
                            if (adminOpen) closeAdmin(true)
                            openParticipants()
                        }
                    }}
                    onSettingsToggle={() => {
                        if (settingsOpen) {
                            closeSettings()
                        } else {
                            if (participantsOpen) closeParticipants(true)
                            if (adminOpen) closeAdmin(true)
                            openSettings()
                        }
                    }}
                    onAdminToggle={() => {
                        if (adminOpen) {
                            closeAdmin()
                        } else {
                            if (participantsOpen) closeParticipants(true)
                            if (settingsOpen) closeSettings(true)
                            openAdmin()
                        }
                    }}
                    participantsPopover={
                        participantsOpen ? (
                            <ParticipantsPopoverViewerCard
                                mode="desktop"
                                panelRef={participantsRef}
                                maxH={null}
                                shiftX={0}
                                t={t}
                                participantsClosing={participantsClosing}
                                participantsEntering={participantsEntering}
                                onClose={closeParticipants}
                                sections={participantSections}
                                onUserClick={(e, u) => openViewerCard('desktop', e, u)}
                                streamerId={streamUserId}
                                viewerCard={viewerCard}
                                viewerCardRef={viewerCardRef}
                                viewerCardDepth={viewerCardDepth}
                                viewerCardBanStatus={viewerCardBanStatus}
                                userAdminInfo={userAdminInfo}
                                onCloseViewerCard={() => {
                                    setViewerCard(null)
                                    setViewerCardBanStatus(null)
                                    setUserAdminInfo(null)
                                }}
                                onBackToRoot={() => setViewerCardDepth('root')}
                                onGift={(u) => {
                                    openGift(participantToGiftRecipient(u))
                                    setViewerCard(null)
                                }}
                                onBan={handleBanClick}
                                onUnban={handleUnban}
                                onAddMod={handleAddMod}
                                onRemoveMod={handleRemoveMod}
                                isModerator={isModerator}
                                isStreamer={isStreamer}
                                isSuperAdmin={isSuperAdmin}
                                currentUserId={currentUserId}
                            />
                        ) : undefined
                    }
                    settingsPanel={
                        settingsOpen ? (
                            <SettingsPanel
                                panelRef={settingsRef}
                                maxH={null}
                                t={t}
                                settingsClosing={settingsClosing}
                                settingsEntering={settingsEntering}
                                settingsView={settingsView}
                                setSettingsView={setSettingsView}
                                closeSettings={closeSettings}
                                clearChat={clearChat}
                                popoutChat={() => { }}
                                chatFontLevel={chatFontLevel}
                                setChatFontLevel={setChatFontLevel}
                                chatFontSizePx={chatFontSizePx}
                                chatLineHeightPx={chatLineHeightPx}
                                emojiShow={emojiShow}
                                setEmojiShow={setEmojiShow}
                                isPopout={true}
                                streamerName={streamChannelName}
                                highlightMentions={highlightMentions}
                                setHighlightMentions={setHighlightMentions}
                                hideSystemMessages={hideSystemMessages}
                                setHideSystemMessages={setHideSystemMessages}
                                showGiftMessages={showGiftMessages}
                                setShowGiftMessages={setShowGiftMessages}
                                showUserPreferences={Boolean(currentUserId)}
                            />
                        ) : undefined
                    }
                    adminPanel={
                        adminOpen ? (
                            <AdminPanel
                                panelRef={adminRef}
                                maxH={null}
                                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={streamUserId}
                                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 === streamUserId) return streamAvatar ?? null
                                    return null
                                }}
                                modUserIds={modUserIds}
                                superAdminUserIds={superAdminUserIds}
                                t={t}
                                chatFontSizePx={chatFontSizePx}
                                chatLineHeightPx={chatLineHeightPx}
                                showGiftMessages={showGiftMessages}
                            />
                        ) : undefined
                    }
                    messagesContent={
                        <MessageList
                            messages={messages}
                            mode="desktop"
                            streamerId={streamUserId}
                            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 === streamUserId) return streamAvatar ?? null
                                return null
                            }}
                            modUserIds={modUserIds}
                            superAdminUserIds={superAdminUserIds}
                            obsOverlay={Boolean(obsOverlay)}
                            obsEmbedTheme={obsOverlay?.theme}
                            t={t}
                        />
                    }
                    messagesOverlay={
                        obsOverlay ? undefined : (
                        <MessagesOverlay
                            mode="desktop"
                            isAtBottom={isAtBottom}
                            latestMessage={chatScrollPreviewMessage}
                            onScrollToBottom={scrollToBottom}
                            streamerId={streamUserId}
                            modUserIds={modUserIds}
                            superAdminUserIds={superAdminUserIds}
                            unreadMentionCount={unreadMentionMessages.size}
                            onScrollToMention={() => {
                                const firstUnreadId = Array.from(unreadMentionMessages)[0]
                                if (firstUnreadId) {
                                    scrollToMentionMessage(firstUnreadId)
                                }
                            }}
                            chatViewerCard={chatViewerCard}
                            chatViewerCardRef={chatViewerCardRef}
                            viewerCardDepth="root"
                            viewerCardBanStatus={viewerCardBanStatus}
                            bannedUserIds={bannedUserIds}
                            userAdminInfo={userAdminInfo}
                            onCloseViewerCard={() => {
                                setChatViewerCard(null)
                                setViewerCardBanStatus(null)
                            }}
                            onBackToRoot={() => { }}
                            onGift={(u) => {
                                openGift(participantToGiftRecipient(u))
                                setViewerCard(null)
                            }}
                            onBan={handleBanClick}
                            onUnban={handleUnban}
                            onAddMod={handleAddMod}
                            onRemoveMod={handleRemoveMod}
                            isModerator={isModerator}
                            isStreamer={isStreamer}
                            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) => {
                                    openGift(participantToGiftRecipient(u))
                                    setViewerCard(null)
                                }}
                                isModerator={isModerator}
                                isStreamer={isStreamer}
                                isSuperAdmin={isSuperAdmin}
                                onBan={handleBanClick}
                                onUnban={handleUnban}
                                isBanned={resolveViewerCardIsBanned(
                                    viewerCard.user.id,
                                    bannedUserIds,
                                    viewerCardBanStatus
                                )}
                                currentUserId={currentUserId}
                                userAdminInfo={userAdminInfo}
                                onAddMod={handleAddMod}
                                onRemoveMod={handleRemoveMod}
                                streamerId={streamUserId}
                            />
                        ) : undefined
                    }
                    inputContent={
                        <InputArea
                            variant="desktop"
                            chatWriteAreaRef={chatWriteAreaRef}
                            emojiBtnRef={emojiBtnRef}
                            emojiPanelRef={emojiPanelRef}
                            chatCardRef={chatCardRef}
                            chatInputBarRef={chatInputBarRef}
                            draft={chatDraft}
                            setDraft={setChatDraft}
                            emojiOpen={emojiOpen}
                            emojiMode={emojiMode}
                            emojiTab={emojiTab}
                            setEmojiTab={setEmojiTab}
                            emojiDefaultItems={emojiDefaultItems}
                            emojiClosing={emojiClosing}
                            emojiEntering={emojiEntering}
                            onSend={sendChat}
                            onEmojiToggle={() => {
                                if (emojiOpen) {
                                    closeEmoji()
                                } else {
                                    setEmojiOpen(true)
                                    setEmojiMode('desktop')
                                    setEmojiEntering(true)
                                    setTimeout(() => setEmojiEntering(false), 200)
                                }
                            }}
                            onEmojiClose={closeEmoji}
                            onGiftClick={() => openGift()}
                            placeholder={currentUserId ? t('room.chatPlaceholder') : t('room.loginRequired')}
                            disabled={!currentUserId}
                            banInfo={banInfo}
                            mentionUsers={mentionUsers}
                            chatDisabled={chatDisabled}
                            isSuperAdmin={isSuperAdmin}
                            tipAboveOnlyClass={tipAboveOnlyClass}
                            t={t}
                            showGiftButton={!embed && !!currentUserId && !isStreamer}
                        />
                    }
                />
            )}
            {giftOpen && (
                <GiftModal
                    giftRef={giftRef}
                    giftClosing={giftClosing}
                    giftEntering={giftEntering}
                    closeGift={() => closeGift()}
                    giftSelected={giftSelected}
                    setGiftSelected={setGiftSelected}
                    giftRecipient={giftRecipient}
                    streamFallback={{
                        channelName: streamChannelName,
                        userId: streamUserId,
                        displayId: roomPublicId || 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}
                />
            )}
        </div>
    )
}


