import Foundation
import UIKit

final class PushTokenManager {
    static let shared = PushTokenManager()

    private let queue = DispatchQueue(label: "org.yobble.push-token", qos: .utility)
    private let sessionsService: SessionsService
    private var currentFCMToken: String?
    private var lastSentTokens: [String: String]
    private var loginsRequiringSync: Set<String> = []
    private var isUpdating = false
    private var pendingUpdate = false
    private var retryWorkItem: DispatchWorkItem?
    private var notificationTokens: [NSObjectProtocol] = []

    private enum Keys {
        static let storedToken = "push.current_fcm_token"
        static let sentTokens = "push.last_sent_tokens"
        static let currentUser = "currentUser"
    }

    private enum Constants {
        static let retryDelay: TimeInterval = 20
    }

    private init(sessionsService: SessionsService = SessionsService()) {
        self.sessionsService = sessionsService
        let defaults = UserDefaults.standard
        self.currentFCMToken = defaults.string(forKey: Keys.storedToken)
        self.lastSentTokens = defaults.dictionary(forKey: Keys.sentTokens) as? [String: String] ?? [:]
        observeNotifications()

        queue.async { [weak self] in
            guard let self else { return }
            if let login = self.currentLogin() {
                self.loginsRequiringSync.insert(login)
            }
            self.pendingUpdate = true
            self.tryUpdateTokenIfNeeded()
        }
    }

    deinit {
        notificationTokens.forEach { NotificationCenter.default.removeObserver($0) }
        notificationTokens.removeAll()
    }

    func registerFCMToken(_ token: String) {
        queue.async { [weak self] in
            guard let self else { return }
            guard self.currentFCMToken != token else { return }

            self.currentFCMToken = token
            UserDefaults.standard.set(token, forKey: Keys.storedToken)

            if let login = self.currentLogin() {
                self.loginsRequiringSync.insert(login)
            }

            self.pendingUpdate = true
            self.tryUpdateTokenIfNeeded()
        }
    }

    private func observeNotifications() {
        let center = NotificationCenter.default

        let accessTokenObserver = center.addObserver(forName: .accessTokenDidChange, object: nil, queue: nil) { [weak self] _ in
            guard let self else { return }
            self.queue.async {
                if let login = self.currentLogin() {
                    self.loginsRequiringSync.insert(login)
                } else {
                    self.loginsRequiringSync.removeAll()
                }
                self.pendingUpdate = true
                self.tryUpdateTokenIfNeeded()
            }
        }
        notificationTokens.append(accessTokenObserver)

        let didBecomeActiveObserver = center.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in
            guard let self else { return }
            self.queue.async {
                self.pendingUpdate = true
                self.tryUpdateTokenIfNeeded()
            }
        }
        notificationTokens.append(didBecomeActiveObserver)
    }

    private func tryUpdateTokenIfNeeded() {
        guard pendingUpdate else { return }
        guard !isUpdating else { return }
        guard let login = currentLogin() else { return }
        guard let token = currentFCMToken, !token.isEmpty else { return }

        let needsForcedSync = loginsRequiringSync.contains(login)
        if !needsForcedSync, let lastToken = lastSentTokens[login], lastToken == token {
            pendingUpdate = false
            return
        }

        pendingUpdate = false
        isUpdating = true
        retryWorkItem?.cancel()
        retryWorkItem = nil

        sessionsService.updatePushToken(token) { [weak self] result in
            guard let self else { return }
            self.queue.async {
                self.isUpdating = false

                switch result {
                case .success:
                    self.loginsRequiringSync.remove(login)
                    self.lastSentTokens[login] = token
                    UserDefaults.standard.set(self.lastSentTokens, forKey: Keys.sentTokens)
                    if AppConfig.DEBUG {
                        print("[PushTokenManager] Push token updated for @\(login)")
                    }
                case .failure(let error):
                    if AppConfig.DEBUG {
                        print("[PushTokenManager] Failed to update push token: \(error.localizedDescription)")
                    }
                    self.loginsRequiringSync.insert(login)
                    self.pendingUpdate = true
                    self.scheduleRetry()
                }

                self.tryUpdateTokenIfNeeded()
            }
        }
    }

    private func scheduleRetry() {
        guard retryWorkItem == nil else { return }
        let workItem = DispatchWorkItem { [weak self] in
            guard let self else { return }
            self.retryWorkItem = nil
            self.pendingUpdate = true
            self.tryUpdateTokenIfNeeded()
        }
        retryWorkItem = workItem
        queue.asyncAfter(deadline: .now() + Constants.retryDelay, execute: workItem)
    }

    private func currentLogin() -> String? {
        guard let login = UserDefaults.standard.string(forKey: Keys.currentUser), !login.isEmpty else {
            return nil
        }
        return login
    }
}
