//
//  ChatView.swift
//  lichunWebsocket
//
//  Individual chat interface with message history and expandable input
//

import SwiftUI
import AlertToast

struct ChatView: View {
    @Environment(\.presentationMode) var presentationMode
    @EnvironmentObject var webSocketService: WebSocketService

    @State private var textFieldInput: String = ""
    @State private var refreshFlag = false
    @State private var showEnergyWarning = false
    @State private var appearedMessages: Set<String> = [] // Messages that have completed their appear animation
    @State private var initialMessageIds: Set<String> = [] // Messages present on first load (skip animation)
    @State private var keyboardHeight: CGFloat = 0
    @State private var isInputFocused: Bool = false
    @State private var scrollProxy: ScrollViewProxy?
    @State private var keyboardShowObserver: NSObjectProtocol?
    @State private var keyboardHideObserver: NSObjectProtocol?
    @State private var conversationErrorObserver: NSObjectProtocol?
    @State private var isInitialLoad = true
    @State private var hasLoadedConversation = false // Track if we've loaded the conversation
    @State private var pendingMessageTimeouts: [String: DispatchWorkItem] = [:] // Track timeouts for pending messages
    @State private var previousGameSpeed: Int? // Store speed before entering chat for restoration
    @State private var didAdjustSpeedForConversation = false

    // Typing indicator state
    @State private var showTypingIndicator = false
    @State private var typingIndicatorWorkItem: DispatchWorkItem?
    @State private var fallbackTypingWorkItem: DispatchWorkItem?
    @State private var typingTimeoutWorkItem: DispatchWorkItem? // Safety net: auto-hide after max duration
    @State private var lastMessageCount = 0 // Track message count to detect new AI messages

    // Server-driven typing delay state
    @State private var npcTypingObserver: NSObjectProtocol?
    @State private var typingIndicatorShownAt: Date? = nil   // When indicator started showing
    @State private var hiddenMessageIds: Set<String> = []     // NPC messages held during typing delay
    @State private var messageRevealWorkItem: DispatchWorkItem?

    // LOCAL pending messages - managed entirely in ChatView (not WebSocketService)
    // This separates optimistic UI from server-authoritative data
    @State private var localPendingMessages: [ConversationMessage] = []

    // Energy flash animation state
    @State private var energyFlash: Bool = false

    let characterID: String

    // MARK: - Computed Properties

    var character: Person? {
        webSocketService.player.r.first(where: { $0.id == characterID })
    }

    private var canSendMessage: Bool {
        webSocketService.person.calcEnergy >= 10
    }

    private var conversation: ConversationObj? {
        guard character != nil else { return nil }
        return webSocketService.player.activeConversations.last(where: { $0.character == characterID })
    }

    private var allMessages: [ConversationMessage] {
        // Combine server messages with local pending messages
        // Server messages are canonical, pending messages are local optimistic UI
        var messages = conversation?.sortedMessages ?? []

        // Append pending messages that haven't been confirmed by server
        for pendingMsg in localPendingMessages {
            // Check by tempId first (most reliable), then fall back to content match
            let alreadyInServer: Bool
            if let tempId = pendingMsg.tempId {
                alreadyInServer = messages.contains { $0.tempId == tempId } ||
                    messages.contains { $0.message == pendingMsg.message }
            } else {
                alreadyInServer = messages.contains { $0.message == pendingMsg.message }
            }
            if !alreadyInServer {
                messages.append(pendingMsg)
            }
        }

        return messages
    }

    private var isHeaderCompact: Bool {
        isInputFocused || keyboardHeight > 0
    }

    // MARK: - Body

    var body: some View {
        // Safety check: Ensure character exists
        guard let character = character else {
            return AnyView(
                VStack(spacing: AppSpacing.lg) {
                    Image(systemName: "person.crop.circle.badge.exclamationmark")
                        .font(.system(size: 60))
                        .foregroundColor(AppColors.error)
                    Text("Character Not Found")
                        .font(.appTitle2)
                        .foregroundColor(AppColors.primaryText)
                    Text("This character may have been removed.")
                        .font(.appBody)
                        .foregroundColor(AppColors.secondaryText)
                    Button("Go Back") {
                        presentationMode.wrappedValue.dismiss()
                    }
                    .padding()
                    .background(AppColors.primary)
                    .foregroundColor(.white)
                    .cornerRadius(12)
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(AppColors.background)
            )
        }

        return AnyView(ZStack {
            // Warm, cozy background gradient (adaptive for dark mode)
            LinearGradient(
                colors: [
                    AppColors.surfaceSubtle,
                    AppColors.background,
                    AppColors.surfaceElevated
                ],
                startPoint: .top,
                endPoint: .bottom
            )
            .ignoresSafeArea()

            VStack(spacing: 0) {
                // Header (collapses when keyboard is open)
                ChatHeaderCard(
                    character: character,
                    isCompact: isHeaderCompact,
                    onDismiss: { presentationMode.wrappedValue.dismiss() },
                    energyFlash: $energyFlash
                )

                // Messages area
                ScrollViewReader { proxy in
                    ScrollView(showsIndicators: false) {
                        ChatMessagesList(
                            character: character,
                            characterID: characterID,
                            allMessages: allMessages,
                            hiddenMessageIds: hiddenMessageIds,
                            showTypingIndicator: showTypingIndicator,
                            hasConversation: conversation != nil,
                            hasActiveConversation: webSocketService.player.activeConversations.contains(where: { $0.character == character.id }),
                            appearedMessages: $appearedMessages,
                            initialMessageIds: $initialMessageIds,
                            onRetry: retryMessage,
                            onDelete: deleteFailedMessage,
                            onTapBackground: {}
                        )
                    }
                    .simultaneousGesture(
                        TapGesture().onEnded { _ in
                            hideKeyboard()

                            // Haptic feedback
                            let generator = UIImpactFeedbackGenerator(style: .light)
                            generator.impactOccurred()
                        }
                    )
                    .onChange(of: conversation?.conversation.count) { _ in
                        scrollToBottom(using: proxy)
                    }
                    .onAppear {
                        scrollProxy = proxy
                        setupKeyboardObservers()

                        // Snapshot existing message IDs so they appear instantly (no animation)
                        initialMessageIds = Set(allMessages.map { $0.id })
                        appearedMessages = initialMessageIds

                        // Initialize message count for typing indicator tracking
                        lastMessageCount = conversation?.conversation.count ?? 0

                        // Load conversation history from server (won't trigger AI response)
                        loadConversationIfNeeded()

                        // Mark conversation as read
                        markConversationAsRead()

                        // Perform initial scroll - multiple attempts to ensure it works
                        performInitialScroll(using: proxy)

                        // Listen for server-side message failures
                        setupConversationErrorObserver()

                        // Listen for server-driven typing indicator
                        setupNpcTypingObserver()

                        // Slow down game speed for focused conversation
                        slowDownForConversation()
                    }
                    .onDisappear {
                        removeKeyboardObservers()

                        // Clean up typing indicator
                        hideTypingIndicator()

                        // Remove conversation error observer
                        if let observer = conversationErrorObserver {
                            NotificationCenter.default.removeObserver(observer)
                            conversationErrorObserver = nil
                        }

                        // Remove NPC typing observer
                        if let obs = npcTypingObserver {
                            NotificationCenter.default.removeObserver(obs)
                            npcTypingObserver = nil
                        }
                        // Flush any held messages immediately when leaving
                        revealHiddenMessages()

                        // Restore previous game speed when leaving chat
                        restorePreviousSpeed()
                    }
                }

                // Input area
                DynamicTextInput(
                    text: $textFieldInput,
                    canSend: canSendMessage,
                    onSend: sendMessage,
                    isInputFocused: $isInputFocused
                )
            }
        }
        .onChange(of: webSocketService.player.activeConversations.count) { _ in
            // Reconcile when conversations change (e.g., new conversation created)
            reconcileLocalPendingWithServer()
            if let proxy = scrollProxy {
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                    scrollToBottom(using: proxy)
                }
            }
        }
        .onChange(of: webSocketService.player.r.count) { _ in
            // Scroll after relationships data loads
            if let proxy = scrollProxy {
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                    scrollToBottom(using: proxy)
                }
            }
        }
        .onChange(of: conversation?.conversation.count) { newCount in
            // Reconcile local pending messages with server data
            // This removes pending messages that server has confirmed and cancels their timeouts
            reconcileLocalPendingWithServer()

            if let messages = conversation?.conversation {
                // Check if a new message arrived from the NPC (AI response)
                let currentCount = newCount ?? 0
                if currentCount > lastMessageCount {
                    // Check if the newest message is from the NPC (not from player)
                    if let newestMessage = messages.last,
                       newestMessage.isSystemMessage {
                        cancelPendingTypingFallback()
                        hideTypingIndicator()
                    } else if let newestMessage = messages.last,
                              !newestMessage.isFromCurrentUser(playerCharacterId: characterID) {
                        // Calculate remaining typing delay
                        let typingDelay = calculateTypingDelay(for: newestMessage.message)
                        let elapsed = typingIndicatorShownAt.map { Date().timeIntervalSince($0) } ?? 0
                        let remaining = max(0, typingDelay - elapsed)

                        if remaining > 0.3 {
                            // Hold the message — keep typing indicator visible
                            hiddenMessageIds.insert(newestMessage.id)

                            messageRevealWorkItem?.cancel()
                            let reveal = DispatchWorkItem { [self] in
                                revealHiddenMessages()
                            }
                            messageRevealWorkItem = reveal
                            DispatchQueue.main.asyncAfter(deadline: .now() + remaining, execute: reveal)
                        } else {
                            // Delay already satisfied by AI generation time
                            hideTypingIndicator()
                        }
                    }
                }
                lastMessageCount = currentCount
            } else if showTypingIndicator {
                // Conversation became nil (e.g., state reset) — clean up indicator
                hideTypingIndicator()
            }
            // Scroll to bottom when new messages arrive
            if let proxy = scrollProxy {
                scrollToBottom(using: proxy)
            }
        }
        .toast(isPresenting: $showEnergyWarning) {
            AlertToast(
                displayMode: .hud,
                type: .error(AppColors.error),
                title: "Not enough energy",
                subTitle: "You need 10 energy to send a message"
            )
        })
    }

    // MARK: - Actions

    func sendMessage() {
        let messageText = textFieldInput.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !messageText.isEmpty else { return }

        if webSocketService.person.calcEnergy >= 10 {
            guard let char = character else { return }

            // Generate a unique tempId for this message
            let tempId = UUID().uuidString

            // Create pending message for optimistic UI
            let pendingMessage = ConversationMessage.pending(
                message: messageText,
                senderId: webSocketService.person.id,
                tempId: tempId
            )

            // Add to LOCAL pending messages (ChatView manages optimistic UI)
            localPendingMessages.append(pendingMessage)

            // Scroll to bottom immediately after adding message
            if let proxy = scrollProxy {
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
                    scrollToBottom(using: proxy)
                }
            }

            // Send to server with tempId included
            let convEvent: [String: Any] = [
                "conversationEvent": "freeResponse",
                "characterID": char.id,
                "cType": "chat",
                "response": messageText,
                "tempId": tempId  // Include tempId for server reconciliation
            ]
            let message: [String: Any] = [
                "type": "conversation",
                "message": convEvent
            ]
            webSocketService.sendMessage(message: message)

            // Flash energy pill to indicate energy consumed
            withAnimation(.easeIn(duration: 0.1)) {
                energyFlash = true
            }
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                withAnimation(.easeOut(duration: 0.2)) {
                    energyFlash = false
                }
            }

            // Clear input with animation
            withAnimation {
                textFieldInput = ""
            }

            // Set 10-second timeout to mark as failed if no response
            let timeoutWork = DispatchWorkItem { [self] in
                markLocalMessageFailed(tempId: tempId, reason: "Message timed out")
            }
            pendingMessageTimeouts[tempId] = timeoutWork
            DispatchQueue.main.asyncAfter(deadline: .now() + 5.0, execute: timeoutWork)

            // Fallback: if server doesn't send npcTyping within 3 seconds, show indicator anyway
            // only while the player's message is still unconfirmed.
            let fallbackWork = DispatchWorkItem { [self] in
                fallbackTypingWorkItem = nil
                guard !showTypingIndicator else { return } // Server already triggered it
                scheduleTypingIndicator()
            }
            fallbackTypingWorkItem = fallbackWork
            DispatchQueue.main.asyncAfter(deadline: .now() + 3.0, execute: fallbackWork)

            // Keep keyboard open for continued conversation
        } else {
            showEnergyWarning = true
        }
    }

    /// Cancel pending timeout when message is confirmed
    private func cancelMessageTimeout(tempId: String) {
        pendingMessageTimeouts[tempId]?.cancel()
        pendingMessageTimeouts.removeValue(forKey: tempId)
    }

    private func cancelPendingTypingFallback() {
        fallbackTypingWorkItem?.cancel()
        fallbackTypingWorkItem = nil
    }

    // MARK: - Typing Indicator

    /// Schedule the typing indicator to appear with a natural delay
    private func scheduleTypingIndicator() {
        // Cancel any existing scheduled indicator
        cancelPendingTypingFallback()
        typingIndicatorWorkItem?.cancel()
        typingTimeoutWorkItem?.cancel()

        // Minimum 0.5s ensures user message is visible first (fade-in is 0.15s + scroll)
        // Random delay between 0.5 and 1.5 seconds to feel natural
        let delay = Double.random(in: 0.5...1.5)

        let workItem = DispatchWorkItem { [self] in
            withAnimation(.easeOut(duration: 0.15)) {
                showTypingIndicator = true
            }
            // Scroll to show the typing indicator
            if let proxy = scrollProxy {
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
                    scrollToBottom(using: proxy)
                }
            }

            // Safety net: auto-hide after 30 seconds if AI never responds
            let timeout = DispatchWorkItem { [self] in
                hideTypingIndicator()
            }
            typingTimeoutWorkItem = timeout
            DispatchQueue.main.asyncAfter(deadline: .now() + 30.0, execute: timeout)
        }

        typingIndicatorWorkItem = workItem
        DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem)
    }

    /// Hide the typing indicator when AI response arrives
    private func hideTypingIndicator() {
        cancelPendingTypingFallback()
        typingIndicatorWorkItem?.cancel()
        typingIndicatorWorkItem = nil
        typingTimeoutWorkItem?.cancel()
        typingTimeoutWorkItem = nil

        guard showTypingIndicator else { return }

        withAnimation(.easeOut(duration: 0.1)) {
            showTypingIndicator = false
        }
        typingIndicatorShownAt = nil
    }

    /// Calculate how long the NPC would take to "type" this message
    private func calculateTypingDelay(for text: String) -> TimeInterval {
        let charsPerSecond: Double = 5.0
        let minDelay: Double = 1.5
        let maxDelay: Double = 8.0

        let baseDelay = Double(text.count) / charsPerSecond
        let clamped = min(max(baseDelay, minDelay), maxDelay)

        // +/- 15% jitter for naturalness
        let jitter = clamped * Double.random(in: -0.15...0.15)
        return clamped + jitter
    }

    /// Reveal any NPC messages that were held during typing delay
    private func revealHiddenMessages() {
        messageRevealWorkItem?.cancel()
        messageRevealWorkItem = nil

        guard !hiddenMessageIds.isEmpty else {
            hideTypingIndicator()
            return
        }

        hiddenMessageIds.removeAll()
        hideTypingIndicator()

        if let proxy = scrollProxy {
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                scrollToBottom(using: proxy)
            }
        }
    }

    /// Retry sending a failed message
    private func retryMessage(_ failedMessage: ConversationMessage) {
        guard let char = character,
              let tempId = failedMessage.tempId else { return }

        // Remove the failed message from local pending
        localPendingMessages.removeAll { $0.tempId == tempId }

        // Generate a new tempId for the retry
        let newTempId = UUID().uuidString

        // Create new pending message
        let pendingMessage = ConversationMessage.pending(
            message: failedMessage.message,
            senderId: webSocketService.person.id,
            tempId: newTempId
        )

        // Add to local pending messages
        localPendingMessages.append(pendingMessage)

        // Send to server
        let convEvent: [String: Any] = [
            "conversationEvent": "freeResponse",
            "characterID": char.id,
            "cType": "chat",
            "response": failedMessage.message,
            "tempId": newTempId
        ]
        let message: [String: Any] = [
            "type": "conversation",
            "message": convEvent
        ]
        webSocketService.sendMessage(message: message)

        // Set timeout for retry
        let timeoutWork = DispatchWorkItem { [self] in
            markLocalMessageFailed(tempId: newTempId, reason: "Message timed out")
        }
        pendingMessageTimeouts[newTempId] = timeoutWork
        DispatchQueue.main.asyncAfter(deadline: .now() + 5.0, execute: timeoutWork)

        // Haptic feedback
        let generator = UINotificationFeedbackGenerator()
        generator.notificationOccurred(.success)
    }

    /// Delete a failed message
    private func deleteFailedMessage(_ failedMessage: ConversationMessage) {
        guard let tempId = failedMessage.tempId else { return }

        // Remove from local pending messages
        localPendingMessages.removeAll { $0.tempId == tempId }

        // Haptic feedback
        let generator = UIImpactFeedbackGenerator(style: .light)
        generator.impactOccurred()
    }

    // MARK: - Local Pending Message Management

    /// Mark a local pending message as failed
    @discardableResult
    private func markLocalMessageFailed(tempId: String, reason: String) -> Bool {
        if let index = localPendingMessages.firstIndex(where: { $0.tempId == tempId }) {
            localPendingMessages[index].status = .failed
            localPendingMessages[index].failureReason = reason
            return true
        }
        return false
    }

    /// Reconcile local pending messages with server data
    /// Removes pending messages that now appear in server data
    private func reconcileLocalPendingWithServer() {
        guard let serverMessages = conversation?.conversation else { return }

        // Remove any pending messages that are now in server data
        // Match by tempId first, then by content
        localPendingMessages.removeAll { pendingMsg in
            let matchedByTempId = pendingMsg.tempId != nil &&
                serverMessages.contains { $0.tempId == pendingMsg.tempId }
            let matchedByContent = serverMessages.contains { $0.message == pendingMsg.message }

            if matchedByTempId || matchedByContent {
                // Cancel timeout before removing
                if let tempId = pendingMsg.tempId {
                    cancelMessageTimeout(tempId: tempId)
                }
                cancelPendingTypingFallback()
                return true
            }
            return false
        }
    }

    /// Load conversation history from server (without triggering AI response)
    private func loadConversationIfNeeded() {
        // Only load once per view lifecycle
        guard !hasLoadedConversation else { return }
        hasLoadedConversation = true

        // Check if conversation needs to be loaded
        let existingConversation = webSocketService.player.activeConversations.first(where: { $0.character == characterID })
        let needsLoad = existingConversation == nil || existingConversation?.conversation.isEmpty == true

        if needsLoad {
            // Send init message to load conversation history
            // Server will NOT generate AI response since no 'response' field is provided
            let convEvent: [String: Any] = [
                "conversationEvent": "init",
                "characterID": characterID,
                "cType": "chat"
            ]
            let message: [String: Any] = [
                "type": "conversation",
                "message": convEvent
            ]
            webSocketService.sendMessage(message: message)
        }
    }

    /// Mark conversation as read on server and update local state
    private func markConversationAsRead() {
        guard let conv = conversation, conv.unread == true else { return }

        // Send mark as read to server
        let message: [String: Any] = [
            "type": "markConversationAsRead",
            "message": ["conversationId": conv.id]
        ]
        webSocketService.sendMessage(message: message)

        // Optimistically update local state
        if let index = webSocketService.player.activeConversations.firstIndex(where: { $0.character == characterID }) {
            webSocketService.player.activeConversations[index].unread = false
        }
    }

    func scrollToBottom(using scrollProxy: ScrollViewProxy, animated: Bool = true) {
        if animated {
            withAnimation(.easeOut(duration: 0.3)) {
                scrollProxy.scrollTo("bottom", anchor: .bottom)
            }
        } else {
            scrollProxy.scrollTo("bottom", anchor: .bottom)
        }
    }

    /// Performs multiple scroll attempts to ensure we reach the bottom on initial load
    private func performInitialScroll(using proxy: ScrollViewProxy) {
        // Immediate scroll without animation to start at bottom
        DispatchQueue.main.async {
            scrollToBottom(using: proxy, animated: false)
        }

        // Follow-up scrolls with increasing delays to handle lazy loading
        let delays: [Double] = [0.1, 0.25, 0.5, 0.8]
        for delay in delays {
            DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
                scrollToBottom(using: proxy, animated: delay > 0.3)
            }
        }

        // Mark initial load complete after scrolling
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            isInitialLoad = false
        }
    }

    func hideKeyboard() {
        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }

    private func setupNpcTypingObserver() {
        npcTypingObserver = NotificationCenter.default.addObserver(
            forName: NSNotification.Name("NPCTypingStarted"),
            object: nil, queue: .main
        ) { notification in
            guard let charID = notification.userInfo?["characterID"] as? String,
                  charID == characterID else { return }

            typingIndicatorShownAt = Date()

            // Cancel any pending fallback indicator
            typingIndicatorWorkItem?.cancel()
            typingIndicatorWorkItem = nil

            withAnimation(.easeOut(duration: 0.15)) {
                showTypingIndicator = true
            }
            if let proxy = scrollProxy {
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
                    scrollToBottom(using: proxy)
                }
            }

            // Safety timeout: 30 seconds
            typingTimeoutWorkItem?.cancel()
            let timeout = DispatchWorkItem { [self] in
                hideTypingIndicator()
                revealHiddenMessages()
            }
            typingTimeoutWorkItem = timeout
            DispatchQueue.main.asyncAfter(deadline: .now() + 30.0, execute: timeout)
        }
    }

    private func setupConversationErrorObserver() {
        conversationErrorObserver = NotificationCenter.default.addObserver(
            forName: NSNotification.Name("ConversationMessageFailed"),
            object: nil,
            queue: .main
        ) { notification in
            guard let userInfo = notification.userInfo,
                  let tempId = userInfo["tempId"] as? String else { return }
            let reason = userInfo["reason"] as? String ?? "Message failed"
            // Immediately mark as failed and cancel the timeout
            cancelMessageTimeout(tempId: tempId)
            let didMarkPendingMessage = markLocalMessageFailed(tempId: tempId, reason: reason)
            if !didMarkPendingMessage {
                ToastManager.shared.showError(reason, duration: 4.0)
            }
            hideTypingIndicator()
        }
    }

    private func setupKeyboardObservers() {
        keyboardShowObserver = NotificationCenter.default.addObserver(
            forName: UIResponder.keyboardWillShowNotification,
            object: nil,
            queue: .main
        ) { notification in
            guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }

            withAnimation(.easeOut(duration: 0.25)) {
                keyboardHeight = keyboardFrame.height
            }

            // Delay scroll until after keyboard animation completes
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                if let proxy = scrollProxy {
                    scrollToBottom(using: proxy)
                }
            }
        }

        keyboardHideObserver = NotificationCenter.default.addObserver(
            forName: UIResponder.keyboardWillHideNotification,
            object: nil,
            queue: .main
        ) { _ in
            withAnimation(.easeOut(duration: 0.25)) {
                keyboardHeight = 0
            }
        }
    }

    private func removeKeyboardObservers() {
        if let observer = keyboardShowObserver {
            NotificationCenter.default.removeObserver(observer)
        }
        if let observer = keyboardHideObserver {
            NotificationCenter.default.removeObserver(observer)
        }
        keyboardShowObserver = nil
        keyboardHideObserver = nil
    }

    // MARK: - Speed Management

    /// Speed levels from slowest to fastest
    private static let speedLevels = Constants.GameSpeed.allLevels

    /// Get index of a speed value in the speed levels array
    private func speedLevelIndex(_ speed: Int) -> Int {
        Self.speedLevels.firstIndex(of: speed) ?? 2 // Default to normal (500)
    }

    /// Slow down game speed when entering conversation for focused chatting
    private func slowDownForConversation() {
        guard !didAdjustSpeedForConversation else { return }

        let currentSpeed = webSocketService.player.gameSpeed
        guard currentSpeed > 0 else { return }

        // Only store and slow if not already at slowest
        guard currentSpeed != Constants.GameSpeed.slowest else { return }

        previousGameSpeed = currentSpeed
        didAdjustSpeedForConversation = true

        webSocketService.sendMessage(message: WebSocketCommands.speed(Constants.GameSpeed.slowest))
    }

    /// Restore previous game speed when exiting conversation
    private func restorePreviousSpeed() {
        defer {
            previousGameSpeed = nil
            didAdjustSpeedForConversation = false
        }

        guard didAdjustSpeedForConversation, let previous = previousGameSpeed else { return }
        guard webSocketService.player.gameSpeed != previous else { return }

        webSocketService.sendMessage(message: WebSocketCommands.speed(previous))
    }
}

// MARK: - Preview
#Preview {
    ChatViewPreview()
}

private struct ChatViewPreview: View {
    var body: some View {
        let service = WebSocketService(urlSession: URLSession.shared, delegateQueue: OperationQueue())

        let person = Person()
        person.id = "1"
        person.firstname = "Emma"
        person.lastname = "Chen"
        person.image = "https://api.dicebear.com/7.x/avataaars/svg?seed=Emma"
        person.affinity = 85
        person.relationships = ["friend"]

        service.player.r = [person]
        service.person.calcEnergy = 65
        service.person.money = 1200
        service.person.diamonds = 25

        let message1 = ConversationMessage(
            id: "1",
            message: "Hey! How are you?",
            sentiment: "positive",
            answerOptions: nil,
            sender: person.id,
            datetime: "2024-01-01 10:00:00.000000",
            date: "01-01",
            time: "10:00"
        )

        let message2 = ConversationMessage(
            id: "2",
            message: "I'm doing great! Thanks for asking.",
            sentiment: "positive",
            answerOptions: nil,
            sender: "player",
            datetime: "2024-01-01 10:01:00.000000",
            date: "01-01",
            time: "10:01"
        )

        let conv = ConversationObj(id: "conv1", cType: "chat", character: person.id, conversation: [message1, message2], question: 0)
        service.player.activeConversations = [conv]

        return ChatView(characterID: person.id)
            .environmentObject(service)
    }
}
