import Combine
import Foundation
import os

// MARK: - SearchViewModel

/// Search view model with **cached** state.
///
/// Uses `ObservableObject` + `@Published` (NOT `@Observable`) to avoid
/// SwiftUI observation feedback loops when embedded inside a NavigationStack
/// within the tab pager. `@Observable` + `@State` caused infinite body
/// re-evaluations of SearchView (hundreds per second → 99 % CPU freeze).
/// `ObservableObject` + `@StateObject` matches ChatListViewModel and
/// SettingsViewModel which are both stable.
@MainActor
final class SearchViewModel: ObservableObject {

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

    // MARK: - State

    @Published var searchQuery = ""

    @Published private(set) var searchResults: [SearchUser] = []
    @Published private(set) var isSearching = false
    @Published private(set) var recentSearches: [RecentSearch] = []

    private var searchTask: Task<Void, Never>?
    private var lastSearchedText = ""
    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: - Search Logic

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

    private func onSearchQueryChanged() {
        searchTask?.cancel()
        searchTask = nil

        let normalized = SearchParityPolicy.normalizedQuery(searchQuery)
        if normalized.isEmpty {
            searchResults = []
            isSearching = false
            lastSearchedText = ""
            return
        }

        if normalized == lastSearchedText {
            return
        }

        isSearching = true

        // Debounce 1 second (like Android)
        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 == normalized else {
                return
            }

            let connState = self.searchDispatcher.connectionState
            let hash = SessionManager.shared.privateKeyHash ?? self.searchDispatcher.privateHash

            guard connState == .authenticated, let hash else {
                self.isSearching = false
                return
            }

            self.lastSearchedText = currentQuery

            var packet = PacketSearch()
            packet.privateKey = hash
            packet.search = currentQuery

            self.searchDispatcher.sendSearchPacket(packet)
        }
    }

    func clearSearch() {
        searchQuery = ""
        searchResults = []
        isSearching = false
        lastSearchedText = ""
        searchTask?.cancel()
        searchTask = nil
    }

    // MARK: - Search Callback

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

        searchHandlerToken = searchDispatcher.addSearchResultHandler { [weak self] packet in
            DispatchQueue.main.async { [weak self] in
                guard let self else { return }
                let query = SearchParityPolicy.normalizedQuery(self.searchQuery)
                guard !query.isEmpty else {
                    self.isSearching = false
                    return
                }

                let localMatches = SearchParityPolicy.localAugmentedUsers(
                    query: query,
                    currentPublicKey: SessionManager.shared.currentPublicKey,
                    dialogs: Array(DialogRepository.shared.dialogs.values)
                )
                self.searchResults = SearchParityPolicy.mergeServerAndLocal(
                    server: packet.users,
                    local: localMatches
                )
                self.isSearching = false

                // Update dialog info from server results
                for user in packet.users {
                    DialogRepository.shared.updateUserInfo(
                        publicKey: user.publicKey,
                        title: user.title,
                        username: user.username,
                        verified: user.verified,
                        online: user.online
                    )
                }
            }
        }
    }

    // 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
            }
    }
}
