import Combine
import Foundation
import os

// MARK: - Dialogs Mode (All vs Requests)

/// Desktop parity: dialogs are split into "All" (iHaveSent) and "Requests" (only incoming).
enum DialogsMode: Hashable { case all, requests }

// MARK: - ChatListViewModel

@MainActor
final class ChatListViewModel: ObservableObject {

    private static let logger = Logger(subsystem: "com.rosetta.messenger", category: "ChatListVM")

    // MARK: - State

    @Published var isLoading = false
    @Published var dialogsMode: DialogsMode = .all
    /// NOT @Published — avoids 2× body re-renders per keystroke in ChatListView.
    /// Local filtering uses `searchText` param directly in ChatListSearchContent.
    var searchQuery = ""
    @Published var serverSearchResults: [SearchUser] = []
    @Published var isServerSearching = false
    @Published var recentSearches: [RecentSearch] = []

    private var searchTask: Task<Void, Never>?
    private var searchRetryTask: Task<Void, Never>?
    private var searchHandlerToken: UUID?
    private var recentSearchesCancellable: AnyCancellable?
    private let recentRepository = RecentSearchesRepository.shared
    private let searchDispatcher: SearchResultDispatching

    // MARK: - Init

    init(searchDispatcher: SearchResultDispatching? = nil) {
        self.searchDispatcher = searchDispatcher ?? LiveSearchResultDispatcher()
        configureRecentSearches()
        setupSearchCallback()
    }


    // MARK: - Dialog partitions (single pass, cached per observation cycle)

    private struct DialogPartition {
        var allPinned: [Dialog] = []
        var allUnpinned: [Dialog] = []
        var requests: [Dialog] = []
        var totalUnread: Int = 0
    }

    /// Cached partition — computed once, reused by all properties until dialogs change.
    private var _cachedPartition: DialogPartition?
    private var _cachedPartitionVersion: Int = -1

    private var partition: DialogPartition {
        let repo = DialogRepository.shared
        let currentVersion = repo.dialogsVersion
        if let cached = _cachedPartition, _cachedPartitionVersion == currentVersion {
            return cached
        }
        var result = DialogPartition()
        for dialog in repo.sortedDialogs {
            let isChat = dialog.iHaveSent || dialog.isSavedMessages || SystemAccounts.isSystemAccount(dialog.opponentKey)
            if isChat {
                if dialog.isPinned {
                    result.allPinned.append(dialog)
                } else {
                    result.allUnpinned.append(dialog)
                }
            } else {
                result.requests.append(dialog)
            }
            if !dialog.isMuted {
                result.totalUnread += dialog.unreadCount
            }
        }
        _cachedPartition = result
        _cachedPartitionVersion = currentVersion
        return result
    }

    var filteredDialogs: [Dialog] {
        let p = partition
        switch dialogsMode {
        case .all: return p.allPinned + p.allUnpinned
        case .requests: return p.requests
        }
    }

    var pinnedDialogs: [Dialog] { partition.allPinned }
    var unpinnedDialogs: [Dialog] { partition.allUnpinned }

    var requestsCount: Int { partition.requests.count }
    var hasRequests: Bool { requestsCount > 0 }

    var totalUnreadCount: Int { partition.totalUnread }
    var hasUnread: Bool { totalUnreadCount > 0 }

    // MARK: - Per-mode dialogs (for TabView pages)

    var allModeDialogs: [Dialog] { partition.allPinned + partition.allUnpinned }
    var allModePinned: [Dialog] { partition.allPinned }
    var allModeUnpinned: [Dialog] { partition.allUnpinned }

    var requestsModeDialogs: [Dialog] { partition.requests }

    // MARK: - Actions

    func setSearchQuery(_ query: String) {
        searchQuery = SearchParityPolicy.sanitizeInput(query)
        triggerServerSearch()
    }

    func deleteDialog(_ dialog: Dialog) {
        MessageRepository.shared.deleteDialog(dialog.opponentKey)
        DialogRepository.shared.deleteDialog(opponentKey: dialog.opponentKey)
    }

    func togglePin(_ dialog: Dialog) {
        DialogRepository.shared.togglePin(opponentKey: dialog.opponentKey)
    }

    func toggleMute(_ dialog: Dialog) {
        DialogRepository.shared.toggleMute(opponentKey: dialog.opponentKey)
    }

    func markAsRead(_ dialog: Dialog) {
        MessageRepository.shared.markIncomingAsRead(
            opponentKey: dialog.opponentKey,
            myPublicKey: SessionManager.shared.currentPublicKey
        )
        DialogRepository.shared.markAsRead(opponentKey: dialog.opponentKey)
    }

    // MARK: - Server Search

    private func setupSearchCallback() {
        if let token = searchHandlerToken {
            searchDispatcher.removeSearchResultHandler(token)
        }

        Self.logger.debug("Setting up search callback")
        searchHandlerToken = searchDispatcher.addSearchResultHandler { [weak self] packet in
            DispatchQueue.main.async { [weak self] in
                guard let self else {
                    Self.logger.debug("Search callback: self is nil")
                    return
                }
                guard !self.searchQuery.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
                    self.isServerSearching = false
                    return
                }
                Self.logger.debug("📥 Search results received: \(packet.users.count) users")
                let query = SearchParityPolicy.normalizedQuery(self.searchQuery)
                let localMatches = SearchParityPolicy.localAugmentedUsers(
                    query: query,
                    currentPublicKey: SessionManager.shared.currentPublicKey,
                    dialogs: Array(DialogRepository.shared.dialogs.values)
                )
                self.serverSearchResults = SearchParityPolicy.mergeServerAndLocal(
                    server: packet.users,
                    local: localMatches
                )
                self.isServerSearching = false
                Self.logger.debug("📥 isServerSearching=\(self.isServerSearching), count=\(self.serverSearchResults.count)")
                for user in packet.users {
                    DialogRepository.shared.updateUserInfo(
                        publicKey: user.publicKey,
                        title: user.title,
                        username: user.username,
                        verified: user.verified,
                        online: user.online
                    )
                }
            }
        }
    }

    private func triggerServerSearch() {
        searchTask?.cancel()
        searchTask = nil
        searchRetryTask?.cancel()
        searchRetryTask = nil

        let trimmed = SearchParityPolicy.normalizedQuery(searchQuery)
        if trimmed.isEmpty {
            // Guard: only publish if value actually changes (avoids extra re-renders)
            if !serverSearchResults.isEmpty { serverSearchResults = [] }
            if isServerSearching { isServerSearching = false }
            return
        }

        // Guard: don't re-publish true when already true
        if !isServerSearching { isServerSearching = true }

        searchTask = Task { [weak self] in
            try? await Task.sleep(for: .seconds(1))
            guard let self, !Task.isCancelled else { return }

            let currentQuery = SearchParityPolicy.normalizedQuery(self.searchQuery)
            guard !currentQuery.isEmpty, currentQuery == trimmed else { return }

            self.sendSearchPacket(query: currentQuery)
        }
    }

    /// Sends PacketSearch if authenticated, otherwise waits for authentication (up to 10s).
    private func sendSearchPacket(query: String) {
        let connState = searchDispatcher.connectionState
        let hash = SessionManager.shared.privateKeyHash ?? searchDispatcher.privateHash

        guard connState == .authenticated, let hash else {
            // Not authenticated — wait for reconnect then send
            Self.logger.debug("Search deferred — waiting for authentication")
            searchRetryTask?.cancel()
            searchRetryTask = Task { [weak self] in
                // Poll every 500ms for up to 10s (covers 5s reconnect + handshake)
                for _ in 0..<20 {
                    try? await Task.sleep(for: .milliseconds(500))
                    guard let self, !Task.isCancelled else { return }
                    let current = SearchParityPolicy.normalizedQuery(self.searchQuery)
                    guard current == query else { return }  // Query changed, abort
                    if self.searchDispatcher.connectionState == .authenticated {
                        Self.logger.debug("Connection restored — sending pending search")
                        self.sendSearchPacket(query: query)
                        return
                    }
                }
                // Timed out
                guard let self else { return }
                Self.logger.warning("Search timed out waiting for authentication")
                self.isServerSearching = false
            }
            return
        }

        var packet = PacketSearch()
        packet.privateKey = hash
        packet.search = query
        Self.logger.debug("📤 Sending search packet for '\(query)'")
        searchDispatcher.sendSearchPacket(packet)
    }

    // MARK: - Recent Searches

    func addToRecent(_ user: SearchUser) {
        recentRepository.add(user)
    }

    func removeRecentSearch(publicKey: String) {
        recentRepository.remove(publicKey: publicKey)
    }

    func clearRecentSearches() {
        recentRepository.clearAll()
    }

    private func configureRecentSearches() {
        recentRepository.setAccount(SessionManager.shared.currentPublicKey)
        recentSearches = recentRepository.recentSearches
        recentSearchesCancellable = recentRepository.$recentSearches
            .receive(on: DispatchQueue.main)
            .sink { [weak self] list in
                self?.recentSearches = list
            }
    }
}
