import Foundation
import Combine

@MainActor
final class AppModel: ObservableObject {
    enum Tab: Hashable {
        case discover
        case matches
        case chats
        case profile
    }

    @Published var currentUser: User?
    @Published var discoverProfiles: [User] = []
    @Published var matches: [MatchSummary] = []
    @Published var chats: [ChatSummary] = []
    @Published var messagesByMatch: [Int: [ChatMessage]] = [:]
    @Published var selectedTab: Tab = .discover
    @Published var isLoading = false
    @Published var alertMessage: String?
    @Published var matchBanner: String?

    private let apiClient: APIClient
    private let userDefaults: UserDefaults
    @Published private(set) var token: String?

    init(apiClient: APIClient = APIClient(), userDefaults: UserDefaults = .standard) {
        self.apiClient = apiClient
        self.userDefaults = userDefaults
        restoreSession()
    }

    func bootstrap() async {
        guard token != nil else { return }
        await refreshAll()
    }

    func signUp(
        name: String,
        email: String,
        password: String,
        age: Int,
        city: String,
        bio: String,
        occupation: String,
        gender: String,
        interestedIn: String,
        prompt: String,
        interests: [String]
    ) async {
        await perform {
            let session = try await apiClient.signUp(
                name: name,
                email: email,
                password: password,
                age: age,
                city: city,
                bio: bio,
                occupation: occupation,
                gender: gender,
                interestedIn: interestedIn,
                prompt: prompt,
                interests: interests
            )
            applySession(session)
            await refreshAll()
        }
    }

    func login(email: String, password: String) async {
        await perform {
            let session = try await apiClient.login(email: email, password: password)
            applySession(session)
            await refreshAll()
        }
    }

    func refreshAll() async {
        guard let token else { return }

        await perform {
            async let meResponse = apiClient.fetchCurrentUser(token: token)
            async let discoverResponse = apiClient.fetchDiscover(token: token)
            async let matchesResponse = apiClient.fetchMatches(token: token)
            async let chatsResponse = apiClient.fetchChats(token: token)

            let me = try await meResponse
            let discover = try await discoverResponse
            let nextMatches = try await matchesResponse
            let nextChats = try await chatsResponse

            currentUser = me.user
            discoverProfiles = discover.profiles
            matches = nextMatches.matches
            chats = nextChats.chats
            persistSession()
        }
    }

    func handleSwipe(for user: User, direction: SwipeDirection) async {
        guard let token else { return }
        discoverProfiles.removeAll { $0.id == user.id }

        await perform(showSpinner: false) {
            let response = try await apiClient.sendSwipe(targetUserId: user.id, direction: direction, token: token)
            if response.didMatch {
                matchBanner = "It's a match with \(user.name)"
                let refreshedMatches = try await apiClient.fetchMatches(token: token)
                let refreshedChats = try await apiClient.fetchChats(token: token)
                matches = refreshedMatches.matches
                chats = refreshedChats.chats
            }
        }
    }

    func loadMessages(matchId: Int) async {
        guard let token else { return }

        await perform(showSpinner: false) {
            let response = try await apiClient.fetchMessages(matchId: matchId, token: token)
            messagesByMatch[matchId] = response.messages
        }
    }

    func sendMessage(matchId: Int, text: String) async {
        guard let token else { return }
        let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmedText.isEmpty else { return }

        await perform(showSpinner: false) {
            let envelope = try await apiClient.sendMessage(matchId: matchId, text: trimmedText, token: token)
            var currentMessages = messagesByMatch[matchId] ?? []
            currentMessages.append(envelope.message)
            messagesByMatch[matchId] = currentMessages
            let refreshedChats = try await apiClient.fetchChats(token: token)
            chats = refreshedChats.chats
            let refreshedMatches = try await apiClient.fetchMatches(token: token)
            matches = refreshedMatches.matches
        }
    }

    func updateProfile(_ user: User) async {
        guard let token else { return }

        await perform {
            let response = try await apiClient.updateProfile(token: token, user: user)
            currentUser = response.user
            persistSession()
        }
    }

    func logout() {
        currentUser = nil
        discoverProfiles = []
        matches = []
        chats = []
        messagesByMatch = [:]
        selectedTab = .discover
        token = nil
        userDefaults.removeObject(forKey: Storage.tokenKey)
        userDefaults.removeObject(forKey: Storage.userKey)
    }

    func dismissAlert() {
        alertMessage = nil
    }

    func dismissMatchBanner() {
        matchBanner = nil
    }

    private func applySession(_ session: SessionResponse) {
        token = session.token
        currentUser = session.user
        persistSession()
    }

    private func restoreSession() {
        token = userDefaults.string(forKey: Storage.tokenKey)

        guard
            let data = userDefaults.data(forKey: Storage.userKey),
            let user = try? JSONDecoder().decode(User.self, from: data)
        else {
            return
        }

        currentUser = user
    }

    private func persistSession() {
        userDefaults.set(token, forKey: Storage.tokenKey)

        if let currentUser, let encoded = try? JSONEncoder().encode(currentUser) {
            userDefaults.set(encoded, forKey: Storage.userKey)
        }
    }

    private func perform(showSpinner: Bool = true, _ operation: () async throws -> Void) async {
        if showSpinner {
            isLoading = true
        }

        defer {
            if showSpinner {
                isLoading = false
            }
        }

        do {
            try await operation()
        } catch {
            alertMessage = error.localizedDescription
        }
    }

    private enum Storage {
        static let tokenKey = "heartmatch.token"
        static let userKey = "heartmatch.user"
    }
}
