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

import ExpoModulesCore

/// Manages active download tasks and their delegates, and acts as the shared URLSession delegate
/// that multiplexes callbacks to per-task `DownloadDelegate` instances.
///
/// All access to the internal dictionaries is serialized through a serial dispatch queue
/// so that concurrent calls from URLSession delegate callbacks and the main thread are safe.
@available(iOS 14, tvOS 14, *)
class DownloadTaskStore: NSObject, URLSessionDownloadDelegate {
  private let queue = DispatchQueue(label: "expo.modules.filesystem.DownloadTaskStore")
  private var tasks: [String: URLSessionDownloadTask] = [:]
  private var delegatesByTaskId: [Int: DownloadDelegate] = [:]
  private var session: URLSession?

  deinit {
    session?.invalidateAndCancel()
  }

  func store(task: URLSessionDownloadTask, delegate: DownloadDelegate, forUuid uuid: String) {
    queue.sync {
      tasks[uuid] = task
      delegatesByTaskId[task.taskIdentifier] = delegate
    }
  }

  func cancel(uuid: String) {
    queue.sync {
      tasks[uuid]?.cancel()
    }
  }

  func remove(uuid: String) {
    queue.sync {
      if let task = tasks.removeValue(forKey: uuid) {
        delegatesByTaskId.removeValue(forKey: task.taskIdentifier)
      }
    }
  }

  private func delegate(for task: URLSessionTask) -> DownloadDelegate? {
    queue.sync {
      delegatesByTaskId[task.taskIdentifier]
    }
  }

  func makeDownloadTask(with request: URLRequest) -> URLSessionDownloadTask {
    queue.sync {
      let currentSession = session ?? {
        let newSession = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
        session = newSession
        return newSession
      }()
      return currentSession.downloadTask(with: request)
    }
  }

  // MARK: - URLSessionDownloadDelegate

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

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

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

// MARK: - downloadFileWithStore

/// Executes a file download with optional progress reporting and cancellation.
///
/// When `downloadUuid` is non-nil, uses a shared delegate-based `URLSession` that reports progress
/// and supports cancellation via the `downloadStore`. When nil, uses `URLSession.shared` with
/// a simple completion handler.
@available(iOS 14, tvOS 14, *)
func downloadFileWithStore(
  url: URL,
  to: FileSystemPath,
  options: DownloadOptions?,
  downloadUuid: String?,
  downloadStore: DownloadTaskStore,
  promise: Promise,
  sendEvent: @escaping (String, [String: Any]) -> Void
) throws {
  try to.validatePermission(.write)

  var request = URLRequest(url: url)

  if let headers = options?.headers {
    headers.forEach { key, value in
      request.addValue(value, forHTTPHeaderField: key)
    }
  }

  if let downloadUuid {
    let delegate = DownloadDelegate(
      uuid: downloadUuid,
      sourceUrl: url,
      destination: to,
      options: options,
      promise: promise,
      sendEvent: sendEvent,
      cleanup: {
        downloadStore.remove(uuid: downloadUuid)
      }
    )
    let task = downloadStore.makeDownloadTask(with: request)
    downloadStore.store(task: task, delegate: delegate, forUuid: downloadUuid)
    task.resume()
  } else {
    let downloadTask = URLSession.shared.downloadTask(with: request) { urlOrNil, responseOrNil, errorOrNil in
      guard errorOrNil == nil else {
        return promise.reject(UnableToDownloadException(errorOrNil?.localizedDescription ?? "unspecified error"))
      }
      guard let httpResponse = responseOrNil as? HTTPURLResponse else {
        return promise.reject(UnableToDownloadException("no response"))
      }
      guard httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 else {
        return promise.reject(UnableToDownloadException("response has status \(httpResponse.statusCode)"))
      }
      guard let fileURL = urlOrNil else {
        return promise.reject(UnableToDownloadException("no file url"))
      }

      do {
        try to.withCorrectTypeAndScopedAccess(permission: .write) {
          let destination: URL
          if let to = to as? FileSystemDirectory {
            let filename = httpResponse.suggestedFilename ?? url.lastPathComponent
            destination = to.url.appendingPathComponent(filename)
          } else {
            destination = to.url
          }
          if FileManager.default.fileExists(atPath: destination.path) {
            if options?.idempotent == true {
              try FileManager.default.removeItem(at: destination)
            } else {
              throw DestinationAlreadyExistsException()
            }
          }
          try FileManager.default.moveItem(at: fileURL, to: destination)
          promise.resolve(destination.absoluteString)
        }
      } catch {
        promise.reject(error)
      }
    }
    downloadTask.resume()
  }
}

// MARK: - DownloadDelegate

/// URLSession delegate that reports download progress via the module event system
/// and resolves the JS promise on completion.
///
/// Progress events are throttled to at most one every 100 ms to avoid flooding the JS thread.
/// A final event is always sent when `totalBytesWritten == totalBytesExpectedToWrite`.
@available(iOS 14, tvOS 14, *)
class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
  private let uuid: String
  private let sourceUrl: URL
  private let destination: FileSystemPath
  private let options: DownloadOptions?
  private let promise: Promise
  private let sendEvent: (String, [String: Any]) -> Void
  private let cleanup: () -> Void

  /// Minimum interval (in seconds) between progress events sent to JS.
  private let progressThrottleInterval: CFAbsoluteTime = 0.1
  private var lastProgressUpdate: CFAbsoluteTime = 0

  init(
    uuid: String,
    sourceUrl: URL,
    destination: FileSystemPath,
    options: DownloadOptions?,
    promise: Promise,
    sendEvent: @escaping (String, [String: Any]) -> Void,
    cleanup: @escaping () -> Void
  ) {
    self.uuid = uuid
    self.sourceUrl = sourceUrl
    self.destination = destination
    self.options = options
    self.promise = promise
    self.sendEvent = sendEvent
    self.cleanup = cleanup
  }

  // MARK: Progress

  func urlSession(
    _ session: URLSession,
    downloadTask: URLSessionDownloadTask,
    didWriteData bytesWritten: Int64,
    totalBytesWritten: Int64,
    totalBytesExpectedToWrite: Int64
  ) {
    let now = CFAbsoluteTimeGetCurrent()
    let timeSinceLastUpdate = now - lastProgressUpdate
    let isComplete = totalBytesWritten == totalBytesExpectedToWrite
    let shouldThrottle = timeSinceLastUpdate < progressThrottleInterval

    guard !shouldThrottle || isComplete else {
      return
    }

    lastProgressUpdate = now
    sendEvent("downloadProgress", [
      "uuid": uuid,
      "data": [
        "bytesWritten": totalBytesWritten,
        "totalBytes": totalBytesExpectedToWrite
      ]
    ])
  }

  // MARK: Completion

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

    guard let httpResponse = downloadTask.response as? HTTPURLResponse else {
      promise.reject(UnableToDownloadException("no response"))
      return
    }
    guard httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 else {
      promise.reject(UnableToDownloadException("response has status \(httpResponse.statusCode)"))
      return
    }

    do {
      let resolvedUrl = try destination.withCorrectTypeAndScopedAccess(permission: .write) {
        let dest: URL
        if let dir = destination as? FileSystemDirectory {
          let filename = httpResponse.suggestedFilename ?? sourceUrl.lastPathComponent
          dest = dir.url.appendingPathComponent(filename)
        } else {
          dest = destination.url
        }
        if FileManager.default.fileExists(atPath: dest.path) {
          if options?.idempotent == true {
            try FileManager.default.removeItem(at: dest)
          } else {
            throw DestinationAlreadyExistsException()
          }
        }
        try FileManager.default.moveItem(at: location, to: dest)
        return dest.absoluteString
      }
      promise.resolve(resolvedUrl)
    } catch {
      promise.reject(error)
    }
  }

  // MARK: Error

  func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
    guard let error else {
      return
    }
    cleanup()
    if (error as NSError).code == NSURLErrorCancelled {
      promise.reject(DownloadCancelledException())
    } else {
      promise.reject(UnableToDownloadException(error.localizedDescription))
    }
  }
}
