import Foundation
import SwiftUI

@Observable
final class ChatViewModel {
    var messages: [Message] = []
    var isLoading = false
    var isSending = false
    var errorMessage: String?
    var searchQuery = ""
    var searchResults: [String] = []  // message IDs matching search
    var currentSearchIndex = 0

    private var notificationTask: Task<Void, Never>?

    func loadMessages(convId: String, chatClient: ChatClient) async {
        let email = await chatClient.email
        let cacheKey = await chatClient.cacheKey

        // 1. Load from cache
        let cachedDicts = MessageCache.load(email: email, convId: convId, cacheKey: cacheKey)
        let cached = cachedDicts?.compactMap { Message.fromCacheDict($0) } ?? []

        if !cached.isEmpty {
            // Cache hit — show immediately, no spinner
            messages = cached.sorted { $0.createdAt < $1.createdAt }
        } else {
            // No cache — show spinner (first open)
            isLoading = true
        }

        // 2. Determine after_ts from newest cached message
        let newestCached = messages.last

        // 3. Fetch from server
        let serverMessages: [Message]
        if let newest = newestCached {
            let afterTs = DateParsing.format(newest.createdAt)
            #if DEBUG
            print("DEBUG getMessages after_ts=\(afterTs)")
            #endif
            serverMessages = await chatClient.getMessages(convId: convId, limit: 50, afterTs: afterTs)
        } else {
            serverMessages = await chatClient.getMessages(convId: convId, limit: 50)
        }

        // 4. Merge
        if newestCached != nil {
            // Incremental: dedup by ID, append new, sort
            let existingIds = Set(messages.map(\.id))
            let newMessages = serverMessages.filter { !existingIds.contains($0.id) }
            if !newMessages.isEmpty {
                messages.append(contentsOf: newMessages)
                messages.sort { $0.createdAt < $1.createdAt }
            }
        } else {
            // Full fetch: replace
            messages = serverMessages
        }

        // 5. Sync deleted (only for incremental)
        if let newest = newestCached {
            let afterTs = DateParsing.format(newest.createdAt)
            #if DEBUG
            print("DEBUG get_deleted_since since_ts=\(afterTs)")
            #endif
            let deletedIds = await chatClient.getDeletedSince(convId: convId, sinceTs: afterTs)
            if !deletedIds.isEmpty {
                messages.removeAll { deletedIds.contains($0.id) }
            }
        }

        // 6. Loading done
        isLoading = false

        // 7. Save to cache
        await saveCache(convId: convId, chatClient: chatClient)

        // 8. Mark entire conversation as read (server-side bulk mark)
        // This handles messages not in cache (e.g. failed to decrypt or never fetched)
        await chatClient.markConversationRead(convId: convId)
        // Update local readBy for cached messages so cache reflects read state
        let currentUserId = await chatClient.userId ?? ""
        var anyUpdated = false
        for i in messages.indices {
            if !messages[i].isMine(currentUserId: currentUserId) && !messages[i].readBy.contains(currentUserId) {
                messages[i].readBy.insert(currentUserId)
                anyUpdated = true
            }
        }
        if anyUpdated {
            await saveCache(convId: convId, chatClient: chatClient)
        }
    }

    func loadOlderMessages(convId: String, chatClient: ChatClient) async {
        let older = await chatClient.getMessages(convId: convId, limit: 50, offset: messages.count)
        messages.insert(contentsOf: older, at: 0)
        await saveCache(convId: convId, chatClient: chatClient)
    }

    func sendMessage(convId: String, text: String, members: [ConversationMember],
                     chatClient: ChatClient, replyTo: String? = nil) async {
        guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }

        isSending = true
        errorMessage = nil

        let (success, msg, sentMessage) = await chatClient.sendMessage(
            convId: convId, text: text, members: members, replyTo: replyTo
        )

        isSending = false

        if !success {
            errorMessage = msg
        } else if let sentMessage = sentMessage {
            // Append locally — don't reload from server (ratchet keys are one-time)
            if !messages.contains(where: { $0.id == sentMessage.id }) {
                messages.append(sentMessage)
            }
            await saveCache(convId: convId, chatClient: chatClient)
        }
    }

    func deleteMessage(messageId: String, convId: String, chatClient: ChatClient) async {
        let success = await chatClient.deleteMessage(messageId: messageId, convId: convId)
        if success {
            messages.removeAll { $0.id == messageId }
            await saveCache(convId: convId, chatClient: chatClient)
        }
    }

    func saveCache(convId: String, chatClient: ChatClient) async {
        let email = await chatClient.email
        let cacheKey = await chatClient.cacheKey
        let dicts = messages.map { $0.toCacheDict() }
        try? MessageCache.save(email: email, convId: convId, messages: dicts, cacheKey: cacheKey)
    }

    func search(query: String) {
        searchQuery = query
        if query.isEmpty {
            searchResults = []
            currentSearchIndex = 0
            return
        }
        let lower = query.lowercased()
        searchResults = messages.filter { $0.text?.lowercased().contains(lower) == true }.map(\.id)
        currentSearchIndex = searchResults.isEmpty ? 0 : searchResults.count - 1
    }

    func nextSearchResult() {
        guard !searchResults.isEmpty else { return }
        currentSearchIndex = (currentSearchIndex + 1) % searchResults.count
    }

    func prevSearchResult() {
        guard !searchResults.isEmpty else { return }
        currentSearchIndex = (currentSearchIndex - 1 + searchResults.count) % searchResults.count
    }

    func startNotificationListener(convId: String, chatClient: ChatClient) {
        notificationTask?.cancel()
        notificationTask = Task {
            for await notification in await chatClient.makeNotificationStream() {
                await handleNotification(notification, convId: convId, chatClient: chatClient)
            }
        }
    }

    @MainActor
    private func handleNotification(_ notification: ChatNotification, convId: String, chatClient: ChatClient) {
        switch notification {
        case .newMessage(let data):
            if data["conversation_id"] as? String == convId {
                Task {
                    if let message = await chatClient.decryptNotification(data) {
                        await MainActor.run {
                            // Deduplicate — sent messages are already appended locally
                            if !messages.contains(where: { $0.id == message.id }) {
                                messages.append(message)
                            }
                        }
                        await saveCache(convId: convId, chatClient: chatClient)
                        // Only mark as read if from someone else
                        let myId = await chatClient.userId ?? ""
                        if message.senderId != myId {
                            await chatClient.markRead(convId: convId, messageIds: [message.id])
                        }
                        await chatClient.flushSelfEncrypt()
                    }
                }
            }
        case .messageDeleted(let data):
            if let msgId = data["message_id"] as? String {
                messages.removeAll { $0.id == msgId }
                Task {
                    await saveCache(convId: convId, chatClient: chatClient)
                }
            }
        case .messagesRead(let data):
            if let readUserId = data["user_id"] as? String,
               let msgIds = data["message_ids"] as? [String] {
                for i in messages.indices {
                    if msgIds.contains(messages[i].id) {
                        messages[i].readBy.insert(readUserId)
                    }
                }
            }
        case .messageReacted(let data):
            if let msgId = data["message_id"] as? String,
               let reactUserId = data["user_id"] as? String,
               let reaction = data["reaction"] as? String,
               let action = data["action"] as? String,
               let idx = messages.firstIndex(where: { $0.id == msgId }) {
                if action == "add" {
                    let newReaction = MessageReaction(userId: reactUserId, reaction: reaction, createdAt: Date())
                    if !messages[idx].reactions.contains(where: { $0.userId == reactUserId && $0.reaction == reaction }) {
                        messages[idx].reactions.append(newReaction)
                    }
                } else {
                    messages[idx].reactions.removeAll { $0.userId == reactUserId && $0.reaction == reaction }
                }
                Task { await saveCache(convId: convId, chatClient: chatClient) }
            }
        case .messagePinned(let data):
            if let msgId = data["message_id"] as? String,
               let pinUserId = data["user_id"] as? String,
               let idx = messages.firstIndex(where: { $0.id == msgId }) {
                messages[idx].pinnedAt = Date()
                messages[idx].pinnedBy = pinUserId
                Task { await saveCache(convId: convId, chatClient: chatClient) }
            }
        case .messageUnpinned(let data):
            if let msgId = data["message_id"] as? String,
               let idx = messages.firstIndex(where: { $0.id == msgId }) {
                messages[idx].pinnedAt = nil
                messages[idx].pinnedBy = nil
                Task { await saveCache(convId: convId, chatClient: chatClient) }
            }
        case .messageDelivered(let data):
            // Delivery receipt — message was successfully received by recipient
            if let msgId = data["message_id"] as? String,
               let idx = messages.firstIndex(where: { $0.id == msgId }) {
                messages[idx].readBy.insert("__delivered__")
                Task { await saveCache(convId: convId, chatClient: chatClient) }
            }
        default:
            break
        }
    }

    func reactToMessage(messageId: String, convId: String, reaction: String,
                        currentUserId: String, chatClient: ChatClient) async {
        guard let idx = messages.firstIndex(where: { $0.id == messageId }) else { return }
        let existingReaction = messages[idx].reactions.first { $0.userId == currentUserId }
        let hasSameReaction = existingReaction?.reaction == reaction
        let savedReactions = messages[idx].reactions

        // Optimistic update
        if hasSameReaction {
            // Tapping same emoji — remove it
            messages[idx].reactions.removeAll { $0.userId == currentUserId }
        } else {
            // Remove any previous reaction from this user, then add new one
            messages[idx].reactions.removeAll { $0.userId == currentUserId }
            messages[idx].reactions.append(MessageReaction(userId: currentUserId, reaction: reaction, createdAt: Date()))
        }

        // If user had a different reaction, remove it on server first
        if let old = existingReaction, old.reaction != reaction {
            let _ = await chatClient.reactMessage(messageId: messageId, conversationId: convId,
                                                   reaction: old.reaction, action: "remove")
        }

        // Add or remove the target reaction on server
        let action = hasSameReaction ? "remove" : "add"
        let success = await chatClient.reactMessage(messageId: messageId, conversationId: convId,
                                                     reaction: reaction, action: action)
        if !success {
            // Revert on failure
            messages[idx].reactions = savedReactions
        }
        await saveCache(convId: convId, chatClient: chatClient)
    }

    func pinMessage(messageId: String, convId: String, pin: Bool,
                    chatClient: ChatClient) async {
        guard let idx = messages.firstIndex(where: { $0.id == messageId }) else { return }

        // Optimistic update
        if pin {
            messages[idx].pinnedAt = Date()
            messages[idx].pinnedBy = await chatClient.userId
        } else {
            messages[idx].pinnedAt = nil
            messages[idx].pinnedBy = nil
        }

        let success = await chatClient.pinMessage(messageId: messageId, conversationId: convId,
                                                   action: pin ? "pin" : "unpin")
        if !success {
            // Revert on failure
            if pin {
                messages[idx].pinnedAt = nil
                messages[idx].pinnedBy = nil
            }
        }
        await saveCache(convId: convId, chatClient: chatClient)
    }

    // MARK: - Forward Message

    func forwardMessage(message: Message, targetConvId: String,
                        targetMembers: [ConversationMember], chatClient: ChatClient) async -> Bool {
        var originalMsg: [String: Any] = [
            "text": message.text ?? "",
            "sender": message.senderUsername,
            "conversation_id": message.conversationId,
            "message_id": message.id,
        ]
        if let file = message.file {
            originalMsg["file"] = [
                "file_id": file.fileId,
                "aes_key": file.aesKey,
                "iv": file.iv,
                "filename": file.filename,
                "size": file.size,
                "mime_type": file.mimeType,
            ] as [String: Any]
        }
        if let image = message.image {
            var imgDict: [String: Any] = [
                "file_id": image.fileId,
                "aes_key": image.aesKey,
                "iv": image.iv,
                "filename": image.filename,
                "size": image.size,
            ]
            if let thumb = image.thumbnail { imgDict["thumbnail"] = thumb }
            originalMsg["image"] = imgDict
        }

        let (success, _, _) = await chatClient.forwardMessage(
            targetConvId: targetConvId, originalMsg: originalMsg,
            targetMembers: targetMembers
        )
        return success
    }

    func stop() {
        notificationTask?.cancel()
        notificationTask = nil
    }
}
