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

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

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

    public var passphrase: String?

    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 performBackupIfAutomaticIsEnabled() {
        guard settings.value.automaticBackups == true else { return }
        performBackup()
    }

    public func 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) {
        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] = Backup(
                    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] = Backup(
                    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] = Backup(
                    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] = Backup(
                        id: metadata.identifier,
                        date: metadata.modifiedDate,
                        size: metadata.size
                    )
                }
            } else {
                settings.value.backups[.drive] = nil
            }
        }
    }

    private func performBackup(data: 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)
        } 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)
                }

                // try? FileManager.default.removeItem(at: url)
            }
        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)
                }

                // try? FileManager.default.removeItem(at: url)
            }
        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)
                }

                // try? FileManager.default.removeItem(at: url)
            }
        case .sftp:
            do {
                try sftpService.uploadBackup(url)
            } catch {
                print(error.localizedDescription)
            }
        }
    }
}