import UIKit
import Models
import Combine
import Defaults
import Keychain
import SFTPFeature
import iCloudFeature
import DropboxFeature
import NetworkMonitor
import GoogleDriveFeature
import DependencyInjection
import XXClient
import XXMessengerClient

public final class BackupService {
    @Dependency var messenger: Messenger
    @Dependency var sftpService: SFTPService
    @Dependency var icloudService: iCloudInterface
    @Dependency var dropboxService: DropboxInterface
    @Dependency var networkManager: NetworkMonitoring
    @Dependency var keychainHandler: KeychainHandling
    @Dependency var driveService: GoogleDriveInterface

    @KeyObject(.email, defaultValue: nil) var email: String?
    @KeyObject(.phone, defaultValue: nil) var phone: String?
    @KeyObject(.username, defaultValue: nil) var username: String?

    var manager: XXClient.Backup?

    @KeyObject(.backupSettings, defaultValue: Data()) private var storedSettings: Data

    public var settingsPublisher: AnyPublisher<BackupSettings, Never> {
        settings.handleEvents(receiveSubscription: { [weak self] _ in
            guard let self = self else { return }

            let lastRefreshDate = self.settingsLastRefreshedDate ?? Date.distantPast

            if Date().timeIntervalSince(lastRefreshDate) < 10 { return }

            self.settingsLastRefreshedDate = Date()
            self.refreshConnections()
            self.refreshBackups()
        }).eraseToAnyPublisher()
    }

    private var connType: ConnectionType = .wifi
    private var settingsLastRefreshedDate: Date?
    private var cancellables = Set<AnyCancellable>()
    private lazy var settings = CurrentValueSubject<BackupSettings, Never>(.init(fromData: storedSettings))

    public init() {
        settings
            .dropFirst()
            .removeDuplicates()
            .sink { [unowned self] in storedSettings = $0.toData() }
            .store(in: &cancellables)

        networkManager.connType
            .receive(on: DispatchQueue.main)
            .sink { [unowned self] in connType = $0 }
            .store(in: &cancellables)
    }
}

extension BackupService {
    public func initializeBackup(passphrase: String) {
        manager = try! InitializeBackup.live(
            e2eId: messenger.e2e.get()!.getId(),
            udId: messenger.ud.get()!.getId(),
            password: passphrase,
            callback: .init(handle: { [weak self] backupData in
                self?.updateBackup(data: backupData)
            })
        )

        didUpdateFacts()
    }

    public func didUpdateFacts() {
        if let manager = manager {
            let currentFacts = try! JSONEncoder().encode(
                BackupParams(
                    username: username!,
                    email: email,
                    phone: phone
                )
            )

          print(">>> Will addJSON: \(String(data: currentFacts, encoding: .utf8)!)")
            manager.addJSON(String(data: currentFacts, encoding: .utf8)!)
        }

        performBackup()
    }

    public func performBackupIfAutomaticIsEnabled() {
        guard settings.value.automaticBackups == true else { return }
        performBackup()
    }

    public func performBackup() {
      print(">>> Did call performBackup()")

        guard let directoryUrl = try? FileManager.default.url(
            for: .applicationSupportDirectory,
            in: .userDomainMask,
            appropriateFor: nil,
            create: true
        ) else { fatalError("Couldn't generate the URL to persist the backup") }

        let fileUrl = directoryUrl
            .appendingPathComponent("backup")
            .appendingPathExtension("xxm")

        guard let data = try? Data(contentsOf: fileUrl) else {
            print(">>> Tried to backup arbitrarily but there was nothing to be backed up. Aborting...")
            return
        }

        performBackup(data: data)
    }

    public func updateBackup(data: Data) {
      print(">>> Did call updateBackup(data)")

        guard let directoryUrl = try? FileManager.default.url(
            for: .applicationSupportDirectory,
            in: .userDomainMask,
            appropriateFor: nil,
            create: true
        ) else { fatalError("Couldn't generate the URL to persist the backup") }

        let fileUrl = directoryUrl
            .appendingPathComponent("backup")
            .appendingPathExtension("xxm")

        do {
            try data.write(to: fileUrl)
        } catch {
            fatalError("Couldn't write backup to fileurl")
        }

        let isWifiOnly = settings.value.wifiOnlyBackup
        let isAutomaticEnabled = settings.value.automaticBackups
        let hasEnabledService = settings.value.enabledService != nil

        if isWifiOnly {
            guard connType == .wifi else { return }
        } else {
            guard connType != .unknown else { return }
        }

        if isAutomaticEnabled && hasEnabledService {
            performBackup()
        }
    }

    public func setBackupOnlyOnWifi(_ enabled: Bool) {
        settings.value.wifiOnlyBackup = enabled
    }

    public func setBackupAutomatically(_ enabled: Bool) {
        settings.value.automaticBackups = enabled

        guard enabled else { return }
        performBackup()
    }

    public func toggle(service: CloudService, enabling: Bool) {
        settings.value.enabledService = enabling ? service : nil
    }

    public func authorize(service: CloudService, presenting screen: UIViewController) {
        switch service {
        case .drive:
            driveService.authorize(presenting: screen) { [weak self] _ in
                guard let self = self else { return }
                self.refreshConnections()
                self.refreshBackups()
            }
        case .icloud:
            if !icloudService.isAuthorized() {
                icloudService.openSettings()
            } else {
                refreshConnections()
                refreshBackups()
            }
        case .dropbox:
            if !dropboxService.isAuthorized() {
                dropboxService.authorize(presenting: screen)
                    .sink { [weak self] _ in
                        guard let self = self else { return }
                        self.refreshConnections()
                        self.refreshBackups()
                    }.store(in: &cancellables)
            }
        case .sftp:
            if !sftpService.isAuthorized() {
                sftpService.authorizeFlow((screen, { [weak self] in
                    guard let self = self else { return }
                    screen.navigationController?.popViewController(animated: true)
                    self.refreshConnections()
                    self.refreshBackups()
                }))
            }
        }
    }
}

extension BackupService {
    private func refreshConnections() {
        if icloudService.isAuthorized() && !settings.value.connectedServices.contains(.icloud) {
            settings.value.connectedServices.insert(.icloud)
        } else if !icloudService.isAuthorized() && settings.value.connectedServices.contains(.icloud) {
            settings.value.connectedServices.remove(.icloud)
        }

        if dropboxService.isAuthorized() && !settings.value.connectedServices.contains(.dropbox) {
            settings.value.connectedServices.insert(.dropbox)
        } else if !dropboxService.isAuthorized() && settings.value.connectedServices.contains(.dropbox) {
            settings.value.connectedServices.remove(.dropbox)
        }

        if sftpService.isAuthorized() && !settings.value.connectedServices.contains(.sftp) {
            settings.value.connectedServices.insert(.sftp)
        } else if !sftpService.isAuthorized() && settings.value.connectedServices.contains(.sftp) {
            settings.value.connectedServices.remove(.sftp)
        }

        driveService.isAuthorized { [weak settings] isAuthorized in
            guard let settings = settings else { return }

            if isAuthorized && !settings.value.connectedServices.contains(.drive) {
                settings.value.connectedServices.insert(.drive)
            } else if !isAuthorized && settings.value.connectedServices.contains(.drive) {
                settings.value.connectedServices.remove(.drive)
            }
        }
    }

    private func refreshBackups() {
        if icloudService.isAuthorized() {
            icloudService.downloadMetadata { [weak settings] in
                guard let settings = settings else { return }

                guard let metadata = try? $0.get() else {
                    settings.value.backups[.icloud] = nil
                    return
                }

                settings.value.backups[.icloud] = BackupModel(
                    id: metadata.path,
                    date: metadata.modifiedDate,
                    size: metadata.size
                )
            }
        }

        if sftpService.isAuthorized() {
            sftpService.fetchMetadata { [weak settings] in
                guard let settings = settings else { return }

                guard let metadata = try? $0.get()?.backup else {
                    settings.value.backups[.sftp] = nil
                    return
                }

                settings.value.backups[.sftp] = BackupModel(
                    id: metadata.id,
                    date: metadata.date,
                    size: metadata.size
                )
            }
        }

        if dropboxService.isAuthorized() {
            dropboxService.downloadMetadata { [weak settings] in
                guard let settings = settings else { return }

                guard let metadata = try? $0.get() else {
                    settings.value.backups[.dropbox] = nil
                    return
                }

                settings.value.backups[.dropbox] = BackupModel(
                    id: metadata.path,
                    date: metadata.modifiedDate,
                    size: metadata.size
                )
            }
        }

        driveService.isAuthorized { [weak settings] isAuthorized  in
            guard let settings = settings else { return }

            if isAuthorized {
                self.driveService.downloadMetadata {
                    guard let metadata = try? $0.get() else { return }

                    settings.value.backups[.drive] = BackupModel(
                        id: metadata.identifier,
                        date: metadata.modifiedDate,
                        size: metadata.size
                    )
                }
            } else {
                settings.value.backups[.drive] = nil
            }
        }
    }

    private func performBackup(data: Data) {
      print(">>> Did call performBackup(data)")

        guard let enabledService = settings.value.enabledService else {
            fatalError("Trying to backup but nothing is enabled")
        }

        let url = URL(fileURLWithPath: NSTemporaryDirectory())
            .appendingPathComponent(UUID().uuidString)

        do {
            try data.write(to: url, options: .atomic)
        } catch {
            print(">>> Couldn't write to temp: \(error.localizedDescription)")
            return
        }

        switch enabledService {
        case .drive:
            driveService.uploadBackup(url) {
                switch $0 {
                case .success(let metadata):
                    self.settings.value.backups[.drive] = .init(
                        id: metadata.identifier,
                        date: metadata.modifiedDate,
                        size: metadata.size
                    )
                case .failure(let error):
                    print(error.localizedDescription)
                }
            }
        case .icloud:
            icloudService.uploadBackup(url) {
                switch $0 {
                case .success(let metadata):
                    self.settings.value.backups[.icloud] = .init(
                        id: metadata.path,
                        date: metadata.modifiedDate,
                        size: metadata.size
                    )
                case .failure(let error):
                    print(error.localizedDescription)
                }
            }
        case .dropbox:
            dropboxService.uploadBackup(url) {
                switch $0 {
                case .success(let metadata):
                    self.settings.value.backups[.dropbox] = .init(
                        id: metadata.path,
                        date: metadata.modifiedDate,
                        size: metadata.size
                    )
                case .failure(let error):
                    print(error.localizedDescription)
                }
            }
        case .sftp:
            sftpService.uploadBackup(url: url) {
                switch $0 {
                case .success(let backup):
                    self.settings.value.backups[.sftp] = backup
                case .failure(let error):
                    print(error.localizedDescription)
                }
            }
        }
    }
}