import UIKit
import Models
import Combine
import XXClient
import Defaults
import CloudFiles
import CloudFilesSFTP
import NetworkMonitor
import KeychainAccess
import XXMessengerClient
import DependencyInjection

public final class BackupService {
  @Dependency var messenger: Messenger
  @Dependency var networkManager: NetworkMonitoring

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

  public var backupsPublisher: AnyPublisher<[CloudService: Fetch.Metadata], Never> {
    backupSubject.eraseToAnyPublisher()
  }

  public var connectedServicesPublisher: AnyPublisher<Set<CloudService>, Never> {
    connectedServicesSubject.eraseToAnyPublisher()
  }

  public var settingsPublisher: AnyPublisher<CloudSettings, Never> {
    settings.handleEvents(receiveSubscription: { [weak self] _ in
      guard let self = self else { return }
      self.connectedServicesSubject.send(CloudFilesManager.all.linkedServices())
      self.fetchBackupOnAllProviders()
    }).eraseToAnyPublisher()
  }

  private var connType: ConnectionType = .wifi
  private var cancellables = Set<AnyCancellable>()
  private let connectedServicesSubject = CurrentValueSubject<Set<CloudService>, Never>([])
  private let backupSubject = CurrentValueSubject<[CloudService: Fetch.Metadata], Never>([:])
  private lazy var settings = CurrentValueSubject<CloudSettings, 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)
  }

  func didSetWiFiOnly(enabled: Bool) {
    settings.value.wifiOnlyBackup = enabled
  }

  func didSetAutomaticBackup(enabled: Bool) {
    settings.value.automaticBackups = enabled
    shouldBackupIfSetAutomatic()
  }

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

  func didForceBackup() {
    if let lastBackup = try? Data(contentsOf: getBackupURL()) {
      performUpload(of: lastBackup)
    }
  }

  public func didUpdateFacts() {
    storeFacts()
  }

  public func updateLocalBackup(_ data: Data) {
    do {
      try data.write(to: getBackupURL())
      shouldBackupIfSetAutomatic()
    } catch {
      fatalError("Couldn't write backup to fileurl")
    }
  }

  private func shouldBackupIfSetAutomatic() {
    guard let lastBackup = try? Data(contentsOf: getBackupURL()) else {
      print(">>> No stored backup so won't upload anything.")
      return
    }
    guard settings.value.automaticBackups else {
      print(">>> Backups are not set to automatic")
      return
    }
    guard settings.value.enabledService != nil else {
      print(">>> No service enabled to upload")
      return
    }
    if settings.value.wifiOnlyBackup {
      guard connType == .wifi else {
        print(">>> WiFi only backups, and connType != Wifi")
        return
      }
    } else {
      guard connType != .unknown else {
        print(">>> Connectivity is unknown")
        return
      }
    }
    performUpload(of: lastBackup)
  }

  // MARK: - Messenger

  func initializeBackup(passphrase: String) {
    do {
      try messenger.startBackup(
        password: passphrase,
        params: .init(
          username: username!,
          email: email,
          phone: phone
        )
      )
    } catch {
      print(">>> Exception when calling `messenger.startBackup`: \(error.localizedDescription)")
    }
  }

  func stopBackups() {
    if messenger.isBackupRunning() == true {
      do {
        try messenger.stopBackup()
      } catch {
        print(">>> Exception when calling `messenger.stopBackup`: \(error.localizedDescription)")
      }
    }
  }

  func storeFacts() {
    var facts: [String: String] = [:]
    facts["username"] = username!
    facts["email"] = email
    facts["phone"] = phone
    facts["timestamp"] = "\(Date.asTimestamp)"
    guard let backupManager = messenger.backup.get() else {
      print(">>> Tried to store facts in JSON but there's no backup manager instance")
      return
    }
    guard let data = try? JSONSerialization.data(withJSONObject: facts) else {
      print(">>> Tried to generate data with json dictionary but failed")
      return
    }
    guard let string = String(data: data, encoding: .utf8) else {
      print(">>> Tried to extract string from json dict object but failed")
      return
    }
    backupManager.addJSON(string)
  }

  // MARK: - CloudProviders

  func setupSFTP(host: String, username: String, password: String) {
    let sftpManager = CloudFilesManager.sftp(
      host: host,
      username: username,
      password: password,
      fileName: "backup.xxm"
    )

    CloudFilesManager.all[.sftp] = sftpManager

    do {
      try sftpManager.fetch { [weak self] in
        guard let self else { return }
        switch $0 {
        case .success(let metadata):
          self.backupSubject.value[.sftp] = metadata
        case .failure(let error):
          print(">>> Error fetching sftp: \(error.localizedDescription)")
        }
      }
    } catch {
      print(">>> Exception fetching sftp: \(error.localizedDescription)")
    }
  }

  func authorize(
    service: CloudService,
    presenting screen: UIViewController
  ) {
    guard let manager = CloudFilesManager.all[service] else {
      print(">>> Tried to link/auth but the enabled service is not set")
      return
    }
    do {
      try manager.link(screen) { [weak self] in
        guard let self else { return }
        switch $0 {
        case .success:
          self.connectedServicesSubject.value.insert(service)
          self.fetchBackupOnAllProviders()
        case .failure(let error):
          self.connectedServicesSubject.value.remove(service)
          print(">>> Failed to link/auth \(service): \(error.localizedDescription)")
        }
      }
    } catch {
      print(">>> Exception trying to link/auth \(service): \(error.localizedDescription)")
    }
  }

  func fetchBackupOnAllProviders() {
    CloudFilesManager.all.lastBackups { [weak self] in
      guard let self else { return }
      self.backupSubject.send($0)
    }
  }

  func performUpload(of data: Data) {
    guard let enabledService = settings.value.enabledService else {
      fatalError(">>> Trying to backup but nothing is enabled")
    }
    if enabledService == .sftp {
      let keychain = Keychain(service: "SFTP-XXM")
      guard let host = try? keychain.get("host"),
            let password = try? keychain.get("pwd"),
            let username = try? keychain.get("username") else {
        fatalError(">>> Tried to perform an sftp backup but its not configured")
      }
      CloudFilesManager.all[.sftp] = .sftp(
        host: host,
        username: username,
        password: password,
        fileName: "backup.xxm"
      )
    }
    guard let manager = CloudFilesManager.all[enabledService] else {
      print(">>> Tried to upload but the enabled service is not set")
      return
    }
    do {
      try manager.upload(data) { [weak self] in
        guard let self else { return }

        switch $0 {
        case .success(let metadata):
          self.backupSubject.value[enabledService] = .init(
            size: metadata.size,
            lastModified: metadata.lastModified
          )
        case .failure(let error):
          print(">>> Failed to perform a backup upload: \(error.localizedDescription)")
        }
      }
    } catch {
      print(">>> Exception performing a backup upload: \(error.localizedDescription)")
    }
  }

  private func getBackupURL() -> URL {
    guard let folderURL = try? FileManager.default.url(
      for: .applicationSupportDirectory,
      in: .userDomainMask,
      appropriateFor: nil,
      create: true
    ) else { fatalError(">>> Couldn't generate the URL for backup") }

    return folderURL
      .appendingPathComponent("backup")
      .appendingPathExtension("xxm")
  }
}