// Copyright 2015-present 650 Industries. All rights reserved.

import Foundation
import EXUpdates

@objc(EXHomeAppLoaderTaskDelegate)
public protocol HomeAppLoaderTaskDelegate: AnyObject {
  func homeAppLoaderTask(_: HomeAppLoaderTask, didFinishWithLauncher launcher: AppLauncher)
  func homeAppLoaderTask(_: HomeAppLoaderTask, didFinishWithError error: Error)
}

/**
 Derivation of AppLoaderTask from expo-updates that just does a simple one-loader using HomeAppLoader.
 */
@objc(EXHomeAppLoaderTask)
@objcMembers
public final class HomeAppLoaderTask: NSObject {
  private static let ErrorDomain = "HomeAppLoaderTask"

  public weak var delegate: HomeAppLoaderTaskDelegate?

  private let manifestAndAssetRequestHeaders: ManifestAndAssetRequestHeaders
  private let config: UpdatesConfig
  private let database: UpdatesDatabase
  private let directory: URL
  private let selectionPolicy: SelectionPolicy
  private let delegateQueue: DispatchQueue

  private let loaderTaskQueue: DispatchQueue

  public required init(
    manifestAndAssetRequestHeaders: ManifestAndAssetRequestHeaders,
    config: UpdatesConfig,
    database: UpdatesDatabase,
    directory: URL,
    selectionPolicy: SelectionPolicy,
    delegateQueue: DispatchQueue
  ) {
    self.manifestAndAssetRequestHeaders = manifestAndAssetRequestHeaders
    self.config = config
    self.database = database
    self.directory = directory
    self.selectionPolicy = selectionPolicy
    self.delegateQueue = delegateQueue
    self.loaderTaskQueue = DispatchQueue(label: "expo.loader.LoaderTaskQueue")
  }

  public func start() {
    self.loadHomeUpdate { remoteError, remoteUpdate in
      self.loaderTaskQueue.async {
        self.launchUpdate(remoteUpdate?.manifestUpdateResponsePart?.updateManifest, error: remoteError)
      }
    }
  }

  private func finish(withLauncher launcher: AppLauncher?, error: Error?) {
    dispatchPrecondition(condition: .onQueue(loaderTaskQueue))

    if let delegate = delegate {
      delegateQueue.async {
        // swiftlint:disable force_unwrapping
        if let launcher = launcher,
          launcher.launchAssetUrl != nil || launcher.launchedUpdate!.status == .StatusDevelopment {
          delegate.homeAppLoaderTask(self, didFinishWithLauncher: launcher)
        } else {
          delegate.homeAppLoaderTask(
            self,
            didFinishWithError: error ?? NSError(
              domain: HomeAppLoaderTask.ErrorDomain,
              code: 1031,
              userInfo: [
                NSLocalizedDescriptionKey: "HomeAppLoaderTask encountered an unexpected error and could not launch an update."
              ]
            )
          )
        }
        // swiftlint:enable force_unwrapping
      }
    }
  }

  private func runReaper(withLauncher launcher: AppLauncher) {
    if let launchedUpdate = launcher.launchedUpdate {
      UpdatesReaper.reapUnusedUpdates(
        withConfig: config,
        database: database,
        directory: directory,
        selectionPolicy: selectionPolicy,
        launchedUpdate: launchedUpdate
      )
    }
  }

  private func loadHomeUpdate(withCompletion completion: @escaping (_ remoteError: Error?, _ updateResponse: UpdateResponse?) -> Void) {
    HomeAppLoader(
      manifestAndAssetRequestHeaders: self.manifestAndAssetRequestHeaders,
      config: config,
      database: database,
      directory: directory,
      launchedUpdate: nil,
      completionQueue: loaderTaskQueue
    ).loadHome { _ in
      return true
    } asset: { _, _, _, _ in
      // no-op
    } success: { updateResponse in
      completion(nil, updateResponse)
    } error: { error in
      completion(error, nil)
    }
  }

  private func launchUpdate(_ updateBeingLaunched: Update?, error: Error?) {
    if let updateBeingLaunched = updateBeingLaunched {
      let launcher = AppLauncherWithDatabase(
        config: self.config,
        database: self.database,
        directory: self.directory,
        completionQueue: self.loaderTaskQueue
      )
      launcher.launchUpdate(withSelectionPolicy: self.selectionPolicy) { error, success in
        if success {
          self.finish(withLauncher: launcher, error: nil)
        } else {
          self.finish(withLauncher: nil, error: error)
          NSLog("Downloaded update but failed to launch: %@", error?.localizedDescription ?? "")
        }
        self.runReaper(withLauncher: launcher)
      }
    } else {
      // there's no update, there should be an error
      self.finish(withLauncher: nil, error: error)
    }
  }
}
