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

import ExpoModulesCore

enum NetworkTaskSessionType: String, Enumerable {
  case background
  case foreground
}

protocol NetworkTaskDelegate: AnyObject {
  func urlSession(
    _ session: URLSession,
    task: URLSessionTask,
    didSendBodyData bytesSent: Int64,
    totalBytesSent: Int64,
    totalBytesExpectedToSend: Int64
  )

  func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data)

  func urlSession(
    _ session: URLSession,
    downloadTask: URLSessionDownloadTask,
    didWriteData bytesWritten: Int64,
    totalBytesWritten: Int64,
    totalBytesExpectedToWrite: Int64
  )

  func urlSession(
    _ session: URLSession,
    downloadTask: URLSessionDownloadTask,
    didFinishDownloadingTo location: URL
  )

  func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
}

extension NetworkTaskDelegate {
  func urlSession(
    _ session: URLSession,
    task: URLSessionTask,
    didSendBodyData bytesSent: Int64,
    totalBytesSent: Int64,
    totalBytesExpectedToSend: Int64
  ) {}

  func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {}

  func urlSession(
    _ session: URLSession,
    downloadTask: URLSessionDownloadTask,
    didWriteData bytesWritten: Int64,
    totalBytesWritten: Int64,
    totalBytesExpectedToWrite: Int64
  ) {}

  func urlSession(
    _ session: URLSession,
    downloadTask: URLSessionDownloadTask,
    didFinishDownloadingTo location: URL
  ) {}
}

final class NetworkTaskSessionDispatcher: NSObject, URLSessionDataDelegate, URLSessionDownloadDelegate {
  static let shared = NetworkTaskSessionDispatcher()

  private var delegates: [String: NetworkTaskDelegate] = [:]
  private let lock = NSLock()
  private lazy var sessionHandler = ExpoAppDelegateSubscriberRepository.getSubscriberOfType(
    FileSystemBackgroundSessionHandler.self
  )

  func register(
    delegate: NetworkTaskDelegate,
    for task: URLSessionTask,
    in session: URLSession
  ) -> String {
    let key = makeKey(session: session, task: task)
    lock.lock()
    delegates[key] = delegate
    lock.unlock()
    return key
  }

  func unregister(key: String?) {
    guard let key else {
      return
    }
    lock.lock()
    delegates.removeValue(forKey: key)
    lock.unlock()
  }

  func urlSession(
    _ session: URLSession,
    task: URLSessionTask,
    didSendBodyData bytesSent: Int64,
    totalBytesSent: Int64,
    totalBytesExpectedToSend: Int64
  ) {
    delegate(for: session, task: task)?.urlSession(
      session,
      task: task,
      didSendBodyData: bytesSent,
      totalBytesSent: totalBytesSent,
      totalBytesExpectedToSend: totalBytesExpectedToSend
    )
  }

  func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
    delegate(for: session, task: dataTask)?.urlSession(session, dataTask: dataTask, didReceive: data)
  }

  func urlSession(
    _ session: URLSession,
    downloadTask: URLSessionDownloadTask,
    didWriteData bytesWritten: Int64,
    totalBytesWritten: Int64,
    totalBytesExpectedToWrite: Int64
  ) {
    delegate(for: session, task: downloadTask)?.urlSession(
      session,
      downloadTask: downloadTask,
      didWriteData: bytesWritten,
      totalBytesWritten: totalBytesWritten,
      totalBytesExpectedToWrite: totalBytesExpectedToWrite
    )
  }

  func urlSession(
    _ session: URLSession,
    downloadTask: URLSessionDownloadTask,
    didFinishDownloadingTo location: URL
  ) {
    delegate(for: session, task: downloadTask)?.urlSession(
      session,
      downloadTask: downloadTask,
      didFinishDownloadingTo: location
    )
  }

  func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
    let key = makeKey(session: session, task: task)
    delegate(for: session, task: task)?.urlSession(session, task: task, didCompleteWithError: error)
    unregister(key: key)
  }

  func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    guard let identifier = session.configuration.identifier else {
      return
    }
    sessionHandler?.invokeCompletionHandler(forSessionIdentifier: identifier)
  }

  private func delegate(for session: URLSession, task: URLSessionTask) -> NetworkTaskDelegate? {
    lock.lock()
    defer {
      lock.unlock()
    }
    return delegates[makeKey(session: session, task: task)]
  }

  private func makeKey(session: URLSession, task: URLSessionTask) -> String {
    let sessionIdentifier = session.configuration.identifier ?? "foreground"
    return "\(sessionIdentifier):\(task.taskIdentifier)"
  }
}

final class NetworkTaskSessionManager {
  static let shared = NetworkTaskSessionManager()

  private let dispatcher = NetworkTaskSessionDispatcher.shared

  private lazy var foregroundSession = createForegroundSession()

  #if os(iOS)
  private lazy var backgroundSession = createBackgroundSession()
  #endif

  func session(for type: NetworkTaskSessionType) -> URLSession {
    switch type {
    case .foreground:
      return foregroundSession
    case .background:
      #if os(iOS)
      return backgroundSession
      #else
      return foregroundSession
      #endif
    }
  }

  func register(
    delegate: NetworkTaskDelegate,
    for task: URLSessionTask,
    in session: URLSession
  ) -> String {
    return dispatcher.register(delegate: delegate, for: task, in: session)
  }

  func unregister(key: String?) {
    dispatcher.unregister(key: key)
  }

  private func createForegroundSession() -> URLSession {
    let configuration = URLSessionConfiguration.default
    configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
    configuration.urlCache = nil
    return URLSession(configuration: configuration, delegate: dispatcher, delegateQueue: .main)
  }

  #if os(iOS)
  private func createBackgroundSession() -> URLSession {
    let configuration = URLSessionConfiguration.background(
      withIdentifier: Self.backgroundSessionIdentifier
    )
    configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
    configuration.urlCache = nil
    configuration.sessionSendsLaunchEvents = true
    configuration.isDiscretionary = false
    return URLSession(configuration: configuration, delegate: dispatcher, delegateQueue: .main)
  }

  private static let backgroundSessionIdentifier = {
    let bundleIdentifier = Bundle.main.bundleIdentifier ?? "expo.modules.filesystem"
    return "\(bundleIdentifier).expo-file-system.network-task.background"
  }()
  #endif
}

/**
 * A record type for download task options.
 */
struct DownloadTaskOptions: Record {
  @Field var headers: [String: String]?
  @Field var sessionType: NetworkTaskSessionType = .background
}

/**
 * A SharedObject that handles file downloads with pause/resume support and progress tracking.
 */
class FileSystemDownloadTask: SharedObject {
  private var downloadTask: URLSessionDownloadTask?
  private var delegateKey: String?
  private var sessionType: NetworkTaskSessionType = .background
  private(set) var isPausing = false

  func start(url: URL, to: FileSystemPath, options: DownloadTaskOptions?, promise: Promise) {
    isPausing = false
    sessionType = options?.sessionType ?? .background

    let session = NetworkTaskSessionManager.shared.session(for: sessionType)
    let delegate = DownloadTaskDelegate(sharedObject: self, destination: to, promise: promise)

    var request = URLRequest(url: url)
    if let headers = options?.headers {
      for (key, value) in headers {
        request.setValue(value, forHTTPHeaderField: key)
      }
    }

    let task = session.downloadTask(with: request)
    downloadTask = task
    delegateKey = NetworkTaskSessionManager.shared.register(delegate: delegate, for: task, in: session)
    task.resume()
  }

  func pause() async -> [String: String?] {
    isPausing = true
    guard let downloadTask else {
      return ["resumeData": nil]
    }

    let resumeData = await downloadTask.cancelByProducingResumeData()
    return ["resumeData": resumeData?.base64EncodedString()]
  }

  func resume(
    url: URL,
    to: FileSystemPath,
    resumeData: String,
    options: DownloadTaskOptions?,
    promise: Promise
  ) {
    isPausing = false
    sessionType = options?.sessionType ?? sessionType

    guard let data = Data(base64Encoded: resumeData) else {
      promise.reject(InvalidResumeDataException())
      return
    }

    let session = NetworkTaskSessionManager.shared.session(for: sessionType)
    let delegate = DownloadTaskDelegate(sharedObject: self, destination: to, promise: promise)
    let task = session.downloadTask(withResumeData: data)

    downloadTask = task
    delegateKey = NetworkTaskSessionManager.shared.register(delegate: delegate, for: task, in: session)
    task.resume()
  }

  func cancel() {
    isPausing = false
    downloadTask?.cancel()
    cleanup(unregisterDelegate: false)
  }

  func finishTask() {
    // Delegate is unregistered by NetworkTaskSessionDispatcher after didCompleteWithError returns.
    cleanup(unregisterDelegate: false)
  }

  override func sharedObjectWillRelease() {
    downloadTask?.cancel()
    cleanup(unregisterDelegate: false)
  }

  private func cleanup(unregisterDelegate: Bool) {
    if unregisterDelegate {
      NetworkTaskSessionManager.shared.unregister(key: delegateKey)
    }
    delegateKey = nil
    downloadTask = nil
  }
}

// MARK: - Download Task Delegate

private final class DownloadTaskDelegate: NSObject, NetworkTaskDelegate {
  private weak var sharedObject: FileSystemDownloadTask?
  private let destination: FileSystemPath
  private let promise: Promise
  private var lastProgressTime: TimeInterval = 0
  private let progressThrottleInterval: TimeInterval = 0.1

  init(sharedObject: FileSystemDownloadTask, destination: FileSystemPath, promise: Promise) {
    self.sharedObject = sharedObject
    self.destination = destination
    self.promise = promise
    super.init()
  }

  func urlSession(
    _ session: URLSession,
    downloadTask: URLSessionDownloadTask,
    didWriteData bytesWritten: Int64,
    totalBytesWritten: Int64,
    totalBytesExpectedToWrite: Int64
  ) {
    let currentTime = Date().timeIntervalSince1970
    let shouldEmit = currentTime - lastProgressTime >= progressThrottleInterval ||
      totalBytesWritten == totalBytesExpectedToWrite

    if shouldEmit {
      lastProgressTime = currentTime
      sharedObject?.emit(event: "progress", payload: [
        "bytesWritten": totalBytesWritten,
        "totalBytes": totalBytesExpectedToWrite,
      ])
    }
  }

  func urlSession(
    _ session: URLSession,
    downloadTask: URLSessionDownloadTask,
    didFinishDownloadingTo location: URL
  ) {
    do {
      if let httpResponse = downloadTask.response as? HTTPURLResponse,
         !(200...299).contains(httpResponse.statusCode) {
        promise.reject(UnableToDownloadException("server returned HTTP \(httpResponse.statusCode)"))
        return
      }

      let resolvedUrl = try destination.withCorrectTypeAndScopedAccess(permission: .write) {
        let destinationUrl: URL
        if let directory = destination as? FileSystemDirectory {
          let httpResponse = downloadTask.response as? HTTPURLResponse
          let filename = httpResponse?.suggestedFilename ??
            downloadTask.originalRequest?.url?.lastPathComponent ??
            "download"
          destinationUrl = directory.url.appendingPathComponent(filename)
        } else {
          destinationUrl = destination.url
        }

        if FileManager.default.fileExists(atPath: destinationUrl.path) {
          try FileManager.default.removeItem(at: destinationUrl)
        }

        try FileManager.default.createDirectory(
          at: destinationUrl.deletingLastPathComponent(),
          withIntermediateDirectories: true
        )
        try FileManager.default.moveItem(at: location, to: destinationUrl)
        return destinationUrl.absoluteString
      }
      promise.resolve(resolvedUrl)
    } catch {
      promise.reject(
        UnableToDownloadException("Failed to move downloaded file: \(error.localizedDescription)")
      )
    }
  }

  func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
    defer {
      sharedObject?.finishTask()
    }

    guard let error else {
      return
    }

    let nsError = error as NSError
    if nsError.domain == NSURLErrorDomain && nsError.code == NSURLErrorCancelled {
      if sharedObject?.isPausing == true {
        promise.resolve(nil)
      } else {
        promise.reject(DownloadCancelledException())
      }
      return
    }

    promise.reject(UnableToDownloadException(error.localizedDescription))
  }
}
