import AppCore
import Combine
import ComposableArchitecture
import Foundation
import XCTestDynamicOverlay
import XXMessengerClient
import XXModels

public struct RestoreState: Equatable {
  public enum Field: String, Hashable {
    case passphrase
  }

  public struct File: Equatable {
    public init(name: String, data: Data) {
      self.name = name
      self.data = data
    }

    public var name: String
    public var data: Data
  }

  public init(
    file: File? = nil,
    fileImportFailure: String? = nil,
    restoreFailures: [String] = [],
    focusedField: Field? = nil,
    isImportingFile: Bool = false,
    passphrase: String = "",
    isRestoring: Bool = false
  ) {
    self.file = file
    self.fileImportFailure = fileImportFailure
    self.restoreFailures = restoreFailures
    self.focusedField = focusedField
    self.isImportingFile = isImportingFile
    self.passphrase = passphrase
    self.isRestoring = isRestoring
  }

  public var file: File?
  public var fileImportFailure: String?
  public var restoreFailures: [String]
  @BindableState public var focusedField: Field?
  @BindableState public var isImportingFile: Bool
  @BindableState public var passphrase: String
  @BindableState public var isRestoring: Bool
}

public enum RestoreAction: Equatable, BindableAction {
  case importFileTapped
  case fileImport(Result<URL, NSError>)
  case restoreTapped
  case finished
  case failed([NSError])
  case binding(BindingAction<RestoreState>)
}

public struct RestoreEnvironment {
  public init(
    messenger: Messenger,
    db: DBManagerGetDB,
    loadData: URLDataLoader,
    now: @escaping () -> Date,
    mainQueue: AnySchedulerOf<DispatchQueue>,
    bgQueue: AnySchedulerOf<DispatchQueue>
  ) {
    self.messenger = messenger
    self.db = db
    self.loadData = loadData
    self.now = now
    self.mainQueue = mainQueue
    self.bgQueue = bgQueue
  }

  public var messenger: Messenger
  public var db: DBManagerGetDB
  public var loadData: URLDataLoader
  public var now: () -> Date
  public var mainQueue: AnySchedulerOf<DispatchQueue>
  public var bgQueue: AnySchedulerOf<DispatchQueue>
}

#if DEBUG
extension RestoreEnvironment {
  public static let unimplemented = RestoreEnvironment(
    messenger: .unimplemented,
    db: .unimplemented,
    loadData: .unimplemented,
    now: XCTUnimplemented("\(Self.self).now"),
    mainQueue: .unimplemented,
    bgQueue: .unimplemented
  )
}
#endif

public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironment>
{ state, action, env in
  switch action {
  case .importFileTapped:
    state.isImportingFile = true
    state.fileImportFailure = nil
    return .none

  case .fileImport(.success(let url)):
    state.isImportingFile = false
    do {
      state.file = .init(
        name: url.lastPathComponent,
        data: try env.loadData(url)
      )
      state.fileImportFailure = nil
    } catch {
      state.file = nil
      state.fileImportFailure = error.localizedDescription
    }
    return .none

  case .fileImport(.failure(let error)):
    state.isImportingFile = false
    state.file = nil
    state.fileImportFailure = error.localizedDescription
    return .none

  case .restoreTapped:
    guard let backupData = state.file?.data, backupData.count > 0 else { return .none }
    let backupPassphrase = state.passphrase
    state.isRestoring = true
    state.restoreFailures = []
    return Effect.result {
      do {
        let result = try env.messenger.restoreBackup(
          backupData: backupData,
          backupPassphrase: backupPassphrase
        )
        let facts = try env.messenger.ud.tryGet().getFacts()
        try env.db().saveContact(Contact(
          id: try env.messenger.e2e.tryGet().getContact().getId(),
          username: facts.get(.username)?.value,
          email: facts.get(.email)?.value,
          phone: facts.get(.phone)?.value,
          createdAt: env.now()
        ))
        try result.restoredContacts.forEach { contactId in
          if try env.db().fetchContacts(.init(id: [contactId])).isEmpty {
            try env.db().saveContact(Contact(
              id: contactId,
              createdAt: env.now()
            ))
          }
        }
        return .success(.finished)
      } catch {
        var errors = [error as NSError]
        do {
          try env.messenger.destroy()
        } catch {
          errors.append(error as NSError)
        }
        return .success(.failed(errors))
      }
    }
    .subscribe(on: env.bgQueue)
    .receive(on: env.mainQueue)
    .eraseToEffect()

  case .finished:
    state.isRestoring = false
    return .none

  case .failed(let errors):
    state.isRestoring = false
    state.restoreFailures = errors.map(\.localizedDescription)
    return .none

  case .binding(_):
    return .none
  }
}
.binding()