import HUD
import Shared
import Models
import SwiftCSV

import Combine
import Defaults
import XXModels
import Keychain
import Foundation
import Integration
import Permissions
import DropboxFeature
import VersionChecking
import CombineSchedulers
import DependencyInjection

struct Update {
    let content: String
    let urlString: String
    let positiveActionTitle: String
    let negativeActionTitle: String?
    let actionStyle: CapsuleButtonStyle
}

enum LaunchRoute {
    case chats
    case update(Update)
    case onboarding(String)
}

final class LaunchViewModel {
    @Dependency private var network: XXNetworking
    @Dependency private var versionChecker: VersionChecker
    @Dependency private var dropboxService: DropboxInterface
    @Dependency private var keychainHandler: KeychainHandling
    @Dependency private var permissionHandler: PermissionHandling

    @KeyObject(.username, defaultValue: nil) var username: String?
    @KeyObject(.biometrics, defaultValue: false) var isBiometricsOn: Bool

    var hudPublisher: AnyPublisher<HUDStatus, Never> {
        hudSubject.eraseToAnyPublisher()
    }

    var routePublisher: AnyPublisher<LaunchRoute, Never> {
        routeSubject.eraseToAnyPublisher()
    }

    var mainScheduler: AnySchedulerOf<DispatchQueue> = {
        DispatchQueue.main.eraseToAnyScheduler()
    }()

    var backgroundScheduler: AnySchedulerOf<DispatchQueue> = {
        DispatchQueue.global().eraseToAnyScheduler()
    }()

    var getSession: (String) throws -> SessionType = Session.init

    private var cancellables = Set<AnyCancellable>()
    private let routeSubject = PassthroughSubject<LaunchRoute, Never>()
    private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none)

    func viewDidAppear() {
        mainScheduler.schedule(after: .init(.now() + 1)) { [weak self] in
            guard let self = self else { return }

            self.hudSubject.send(.on)

            self.versionChecker().sink { [unowned self] in
                switch $0 {
                case .upToDate:
                    self.versionApproved()
                case .failure(let error):
                    self.versionFailed(error: error)
                case .updateRequired(let info):
                    self.versionUpdateRequired(info)
                case .updateRecommended(let info):
                    self.versionUpdateRecommended(info)
                }
            }.store(in: &self.cancellables)
        }
    }

    func versionApproved() {
        Task {
            do {
                network.writeLogs()
                let _ = try await fetchBannedList()

                network.updateNDF { [weak self] in
                    guard let self = self else { return }

                    switch $0 {
                    case .success(let ndf):
                        self.network.updateErrors()

                        guard self.network.hasClient else {
                            self.hudSubject.send(.none)
                            self.routeSubject.send(.onboarding(ndf))
                            self.dropboxService.unlink()
                            try? self.keychainHandler.clear()
                            return
                        }

                        guard self.username != nil else {
                            self.network.purgeFiles()
                            self.hudSubject.send(.none)
                            self.routeSubject.send(.onboarding(ndf))
                            self.dropboxService.unlink()
                            try? self.keychainHandler.clear()
                            return
                        }

                        self.backgroundScheduler.schedule { [weak self] in
                            guard let self = self else { return }

                            do {
                                let session = try self.getSession(ndf)
                                DependencyInjection.Container.shared.register(session as SessionType)
                                self.hudSubject.send(.none)
                                self.checkBiometrics()
                            } catch {
                                self.hudSubject.send(.error(HUDError(with: error)))
                            }
                        }
                    case .failure(let error):
                        self.hudSubject.send(.error(HUDError(with: error)))
                    }
                }
            } catch {
                self.hudSubject.send(.error(HUDError(with: error)))
            }
        }
    }

    func getContactWith(userId: Data) -> Contact? {
        guard let session = try? DependencyInjection.Container.shared.resolve() as SessionType,
              let contact = try? session.dbManager.fetchContacts(.init(id: [userId])).first else {
            return nil
        }

        return contact
    }

    func getGroupInfoWith(groupId: Data) -> GroupInfo? {
        guard let session: SessionType = try? DependencyInjection.Container.shared.resolve(),
              let info = try? session.dbManager.fetchGroupInfos(.init(groupId: groupId)).first else {
            return nil
        }

        return info
    }

    private func versionFailed(error: Error) {
        let title = Localized.Launch.Version.failed
        let content = error.localizedDescription
        let hudError = HUDError(content: content, title: title, dismissable: false)

        hudSubject.send(.error(hudError))
    }

    private func versionUpdateRequired(_ info: DappVersionInformation) {
        hudSubject.send(.none)

        let model = Update(
            content: info.minimumMessage,
            urlString: info.appUrl,
            positiveActionTitle: Localized.Launch.Version.Required.positive,
            negativeActionTitle: nil,
            actionStyle: .brandColored
        )

        routeSubject.send(.update(model))
    }

    private func versionUpdateRecommended(_ info: DappVersionInformation) {
        hudSubject.send(.none)

        let model = Update(
            content: Localized.Launch.Version.Recommended.title,
            urlString: info.appUrl,
            positiveActionTitle: Localized.Launch.Version.Recommended.positive,
            negativeActionTitle: Localized.Launch.Version.Recommended.negative,
            actionStyle: .simplestColoredRed
        )

        routeSubject.send(.update(model))
    }

    private func checkBiometrics() {
        if permissionHandler.isBiometricsAvailable && isBiometricsOn {
            permissionHandler.requestBiometrics { [weak self] in
                guard let self = self else { return }

                switch $0 {
                case .success(let granted):
                    guard granted else { return }
                    self.routeSubject.send(.chats)

                case .failure(let error):
                    self.hudSubject.send(.error(HUDError(with: error)))
                }
            }
        } else {
            self.routeSubject.send(.chats)
        }
    }

    private func fetchBannedList() async throws -> Data {
        let url = URL(string: "https://elixxir-bins.s3.us-west-1.amazonaws.com/client/bannedUsers/banned.csv")
        return try await withCheckedThrowingContinuation { continuation in
            URLSession.shared.dataTask(with: url!) { data, _, error in
                if let error = error {
                    return continuation.resume(throwing: error)
                }

                guard let data = data else { fatalError("?") }
                return continuation.resume(returning: data)
            }.resume()
        }
    }
}