import UIKit
import Models
import Defaults
import Integration
import DependencyInjection

public final class PushHandler: PushHandling {
    private enum Constants {
        static let appGroup = "group.elixxir.messenger"
        static let usernamesSetting = "isShowingUsernames"
    }

    @KeyObject(.pushNotifications, defaultValue: false) var isPushEnabled: Bool

    let requestAuth: RequestAuth
    public static let defaultRequestAuth = UNUserNotificationCenter.current().requestAuthorization
    public typealias RequestAuth = (UNAuthorizationOptions, @escaping (Bool, Error?) -> Void) -> Void

    public var pushExtractor: PushExtractor
    public var contentsBuilder: ContentsBuilder
    public var applicationState: () -> UIApplication.State

    public init(
        requestAuth: @escaping RequestAuth = defaultRequestAuth,
        pushExtractor: PushExtractor = .live,
        contentsBuilder: ContentsBuilder = .live,
        applicationState: @escaping () -> UIApplication.State = { UIApplication.shared.applicationState }
    ) {
        self.requestAuth = requestAuth
        self.pushExtractor = pushExtractor
        self.contentsBuilder = contentsBuilder
        self.applicationState = applicationState
    }

    public func registerToken(_ token: Data) {
        do {
            let session = try DependencyInjection.Container.shared.resolve() as SessionType
            try session.registerNotifications(token)
        } catch {
            isPushEnabled = false
        }
    }

    public func requestAuthorization(
        _ completion: @escaping (Result<Bool, Error>) -> Void
    ) {
        let options: UNAuthorizationOptions = [.alert, .sound, .badge]

        requestAuth(options) { granted, error in
            guard let error = error else {
                completion(.success(granted))
                return
            }

            completion(.failure(error))
        }
    }

    public func handlePush(
        _ userInfo: [AnyHashable: Any],
        _ completion: @escaping (UIBackgroundFetchResult) -> Void
    ) {
        do {
            guard
                let pushes = try pushExtractor.extractFrom(userInfo).get(),
                applicationState() == .background,
                pushes.isEmpty == false
            else {
                completion(.noData)
                return
            }

            let content = contentsBuilder.build("New Messages Available", pushes.first!)
            let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
            let request = UNNotificationRequest(identifier: Bundle.main.bundleIdentifier!, content: content, trigger: trigger)

            UNUserNotificationCenter.current().add(request) { error in
                if error == nil {
                    completion(.newData)
                } else {
                    completion(.failed)
                }
            }
        } catch {
            completion(.failed)
        }
    }

    public func handlePush(
        _ request: UNNotificationRequest,
        _ completion: @escaping (UNNotificationContent) -> Void
    ) {
        guard let pushes = try? pushExtractor.extractFrom(request.content.userInfo).get(), !pushes.isEmpty,
              let defaults = UserDefaults(suiteName: Constants.appGroup) else {
            return
        }

        guard let showSender = defaults.value(forKey: Constants.usernamesSetting) as? Bool, showSender == true else {
            pushes.map { ($0.type.unknownSenderContent!, $0) }
                .map(contentsBuilder.build)
                .forEach { completion($0) }
            return
        }

        let dbManager = GRDBDatabaseManager()
        try? dbManager.setup()

        let tuples: [(String, Push)] = pushes.compactMap {
            guard let userId = $0.source, let contact: Contact = try? dbManager.fetch(.withUserId(userId)).first else {
                return ($0.type.unknownSenderContent!, $0)
            }

            let name = contact.nickname ?? contact.username
            return ($0.type.knownSenderContent(name)!, $0)
        }

        tuples
            .map(contentsBuilder.build)
            .forEach { completion($0) }
    }

    public func handleAction(
        _ router: PushRouter,
        _ userInfo: [AnyHashable : Any],
        _ completion: @escaping () -> Void
    ) {
        guard let typeString = userInfo["type"] as? String,
              let type = PushType(rawValue: typeString) else {
            completion()
            return
        }

        let route: PushRouter.Route

        switch type {
        case .e2e:
            guard let source = userInfo["source"] as? Data else {
                completion()
                return
            }

            route = .contactChat(id: source)

        case .group:
            guard let source = userInfo["source"] as? Data else {
                completion()
                return
            }

            route = .groupChat(id: source)

        case .request, .groupRq:
            route = .requests

        case .silent, .`default`:
            fatalError("Silent/Default push types should be filtered at this point")

        case .reset, .endFT, .confirm:
            route = .requests
        }

        router.navigateTo(route, completion)
    }
}