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

import ExpoModulesCore
import Photos

private let EVENT_DOWNLOAD_PROGRESS = "expo-file-system.downloadProgress"
private let EVENT_UPLOAD_PROGRESS = "expo-file-system.uploadProgress"

public final class FileSystemLegacyModule: Module {
  private var sessionTaskDispatcher: EXSessionTaskDispatcher!
  private lazy var taskHandlersManager = EXTaskHandlersManager()
  private lazy var resourceManager = PHAssetResourceManager()

  private lazy var backgroundSession = createUrlSession(type: .background, delegate: sessionTaskDispatcher)
  private lazy var foregroundSession = createUrlSession(type: .foreground, delegate: sessionTaskDispatcher)

  private var documentDirectory: URL? {
    return appContext?.config.documentDirectory
  }

  private var cacheDirectory: URL? {
    return appContext?.config.cacheDirectory
  }

  public func definition() -> ModuleDefinition {
    Name("ExponentFileSystem")

    Constant("documentDirectory") {
      return documentDirectory?.absoluteString
    }

    Constant("cacheDirectory") {
      return cacheDirectory?.absoluteString
    }

    Constant("bundleDirectory") {
      return Bundle.main.bundlePath
    }

    Events(EVENT_DOWNLOAD_PROGRESS, EVENT_UPLOAD_PROGRESS)
    
    OnCreate {
      Task { @MainActor in
        sessionTaskDispatcher = EXSessionTaskDispatcher(
          sessionHandler: ExpoAppDelegateSubscriberRepository.getSubscriberOfType(FileSystemBackgroundSessionHandler.self)
        )
      }
    }

    AsyncFunction("getInfoAsync") { (url: URL, options: InfoOptions, promise: Promise) in
      let optionsDict = options.toDictionary(appContext: appContext)
      switch url.scheme {
      case "file":
        EXFileSystemLocalFileHandler.getInfoForFile(
          url,
          withOptions: optionsDict,
          resolver: promise.legacyResolver,
          rejecter: promise.legacyRejecter
        )
      case "assets-library", "ph":
        EXFileSystemAssetLibraryHandler.getInfoForFile(
          url,
          withOptions: optionsDict,
          resolver: promise.legacyResolver,
          rejecter: promise.legacyRejecter
        )
      default:
        throw UnsupportedSchemeException(url.scheme)
      }
    }

    AsyncFunction("readAsStringAsync") { (url: URL, options: ReadingOptions) -> String in
      try ensurePathPermission(appContext, path: url.path, flag: .read)

      if options.encoding == .base64 {
        return try readFileAsBase64(path: url.path, options: options)
      }
      do {
        return try String(contentsOfFile: url.path, encoding: options.encoding.toStringEncoding() ?? .utf8)
      } catch {
        throw FileNotReadableException(url.path)
      }
    }

    AsyncFunction("writeAsStringAsync") { (url: URL, string: String, options: WritingOptions) in
      try ensurePathPermission(appContext, path: url.path, flag: .write)

      let data: Data?
      if options.encoding == .base64 {
        data = Data(base64Encoded: string, options: .ignoreUnknownCharacters)
      } else {
        data = string.data(using: options.encoding.toStringEncoding() ?? .utf8)
      }

      guard let data else {
        throw FileNotWritableException(url.path)
      }

      do {
        if options.append {
          if !FileManager.default.fileExists(atPath: url.path) {
            try data.write(to: url, options: .atomic)
          } else {
            let fileHandle = try FileHandle(forWritingTo: url)
            defer {
              fileHandle.closeFile()
            }
            fileHandle.seekToEndOfFile()
            fileHandle.write(data)
          }
        } else {
          try data.write(to: url, options: .atomic)
        }
      } catch {
        throw FileNotWritableException(url.path)
          .causedBy(error)
      }
    }

    AsyncFunction("deleteAsync") { (url: URL, options: DeletingOptions) in
      guard url.isFileURL else {
        throw InvalidFileUrlException(url)
      }
      try ensurePathPermission(appContext, path: url.appendingPathComponent("..").path, flag: .write)
      try removeFile(path: url.path, idempotent: options.idempotent)
    }

    AsyncFunction("moveAsync") { (options: RelocatingOptions) in
      let (fromUrl, toUrl) = try options.asTuple()

      guard fromUrl.isFileURL else {
        throw InvalidFileUrlException(fromUrl)
      }
      guard toUrl.isFileURL else {
        throw InvalidFileUrlException(toUrl)
      }

      try ensurePathPermission(appContext, path: fromUrl.appendingPathComponent("..").path, flag: .write)
      try ensurePathPermission(appContext, path: toUrl.path, flag: .write)
      try removeFile(path: toUrl.path, idempotent: true)
      try FileManager.default.moveItem(atPath: fromUrl.path, toPath: toUrl.path)
    }

    AsyncFunction("copyAsync") { (options: RelocatingOptions, promise: Promise) in
      let (fromUrl, toUrl) = try options.asTuple()

      if isPHAsset(path: fromUrl.absoluteString) {
        copyPHAsset(fromUrl: fromUrl, toUrl: toUrl, with: resourceManager, promise: promise)
        return
      }

      try ensurePathPermission(appContext, path: fromUrl.path, flag: .read)
      try ensurePathPermission(appContext, path: toUrl.path, flag: .write)

      if fromUrl.scheme == "file" {
        EXFileSystemLocalFileHandler.copy(
          from: fromUrl,
          to: toUrl,
          resolver: promise.legacyResolver,
          rejecter: promise.legacyRejecter
        )
      } else if ["ph", "assets-library"].contains(fromUrl.scheme) {
        EXFileSystemAssetLibraryHandler.copy(
          from: fromUrl,
          to: toUrl,
          resolver: promise.legacyResolver,
          rejecter: promise.legacyRejecter
        )
      } else {
        throw InvalidFileUrlException(fromUrl)
      }
    }

    AsyncFunction("makeDirectoryAsync") { (url: URL, options: MakeDirectoryOptions) in
      guard url.isFileURL else {
        throw InvalidFileUrlException(url)
      }

      try ensurePathPermission(appContext, path: url.path, flag: .write)
      try FileManager.default.createDirectory(at: url, withIntermediateDirectories: options.intermediates, attributes: nil)
    }

    AsyncFunction("readDirectoryAsync") { (url: URL) -> [String] in
      guard url.isFileURL else {
        throw InvalidFileUrlException(url)
      }
      try ensurePathPermission(appContext, path: url.path, flag: .read)

      return try FileManager.default.contentsOfDirectory(atPath: url.path)
    }

    AsyncFunction("downloadAsync") { (sourceUrl: URL, localUrl: URL, options: DownloadOptionsLegacy, promise: Promise) in
      try ensureFileDirectoryExists(localUrl)
      try ensurePathPermission(appContext, path: localUrl.path, flag: .write)

      if sourceUrl.isFileURL {
        try ensurePathPermission(appContext, path: sourceUrl.path, flag: .read)
        EXFileSystemLocalFileHandler.copy(
          from: sourceUrl,
          to: localUrl,
          resolver: promise.legacyResolver,
          rejecter: promise.legacyRejecter
        )
        return
      }
      let session = options.sessionType == .background ? backgroundSession : foregroundSession
      let request = createUrlRequest(url: sourceUrl, headers: options.headers)
      let downloadTask = session.downloadTask(with: request)
      let taskDelegate = EXSessionDownloadTaskDelegate(
        resolve: promise.legacyResolver,
        reject: promise.legacyRejecter,
        localUrl: localUrl,
        shouldCalculateMd5: options.md5
      )

      sessionTaskDispatcher.register(taskDelegate, for: downloadTask)
      downloadTask.resume()
    }

    AsyncFunction("uploadAsync") { (targetUrl: URL, localUrl: URL, options: UploadOptions, promise: Promise) in
      guard localUrl.isFileURL else {
        throw InvalidFileUrlException(localUrl)
      }
      guard FileManager.default.fileExists(atPath: localUrl.path) else {
        throw FileNotExistsException(localUrl.path)
      }
      let session = options.sessionType == .background ? backgroundSession : foregroundSession
      let task = try createUploadTask(session: session, targetUrl: targetUrl, sourceUrl: localUrl, options: options)
      let taskDelegate = EXSessionUploadTaskDelegate(resolve: promise.legacyResolver, reject: promise.legacyRejecter)

      sessionTaskDispatcher.register(taskDelegate, for: task)
      task.resume()
    }

    AsyncFunction("uploadTaskStartAsync") { (targetUrl: URL, localUrl: URL, uuid: String, options: UploadOptions, promise: Promise) in
      let session = options.sessionType == .background ? backgroundSession : foregroundSession
      let task = try createUploadTask(session: session, targetUrl: targetUrl, sourceUrl: localUrl, options: options)
      let onSend: EXUploadDelegateOnSendCallback = { [weak self] _, _, totalBytesSent, totalBytesExpectedToSend in
        self?.sendEvent(EVENT_UPLOAD_PROGRESS, [
          "uuid": uuid,
          "data": [
            "totalBytesSent": totalBytesSent,
            "totalBytesExpectedToSend": totalBytesExpectedToSend
          ]
        ])
      }
      let taskDelegate = EXSessionCancelableUploadTaskDelegate(
        resolve: promise.legacyResolver,
        reject: promise.legacyRejecter,
        onSendCallback: onSend,
        resumableManager: taskHandlersManager,
        uuid: uuid
      )

      sessionTaskDispatcher.register(taskDelegate, for: task)
      taskHandlersManager.register(task, uuid: uuid)
      task.resume()
    }

    // swiftlint:disable:next line_length closure_body_length
    AsyncFunction("downloadResumableStartAsync") { (sourceUrl: URL, localUrl: URL, uuid: String, options: DownloadOptionsLegacy, resumeDataString: String?, promise: Promise) in
      try ensureFileDirectoryExists(localUrl)
      try ensurePathPermission(appContext, path: localUrl.path, flag: .write)

      let session = options.sessionType == .background ? backgroundSession : foregroundSession
      let onWrite: EXDownloadDelegateOnWriteCallback = { [weak self] _, _, totalBytesWritten, totalBytesExpectedToWrite in
        self?.sendEvent(EVENT_DOWNLOAD_PROGRESS, [
          "uuid": uuid,
          "data": [
            "totalBytesWritten": totalBytesWritten,
            "totalBytesExpectedToWrite": totalBytesExpectedToWrite
          ]
        ])
      }
      let task: URLSessionDownloadTask

      if let resumeDataString, let resumeData = Data(base64Encoded: resumeDataString) {
        task = session.downloadTask(withResumeData: resumeData)
      } else {
        let request = createUrlRequest(url: sourceUrl, headers: options.headers)
        task = session.downloadTask(with: request)
      }

      let taskDelegate = EXSessionResumableDownloadTaskDelegate(
        resolve: promise.legacyResolver,
        reject: promise.legacyRejecter,
        localUrl: localUrl,
        shouldCalculateMd5: options.md5,
        onWriteCallback: onWrite,
        resumableManager: taskHandlersManager,
        uuid: uuid
      )

      sessionTaskDispatcher.register(taskDelegate, for: task)
      taskHandlersManager.register(task, uuid: uuid)
      task.resume()
    }

    AsyncFunction("downloadResumablePauseAsync") { (id: String) -> [String: String?] in
      guard let task = taskHandlersManager.downloadTask(forId: id) else {
        throw DownloadTaskNotFoundException(id)
      }
      let resumeData = await task.cancelByProducingResumeData()

      return [
        "resumeData": resumeData?.base64EncodedString()
      ]
    }

    AsyncFunction("networkTaskCancelAsync") { (id: String) in
      taskHandlersManager.task(forId: id)?.cancel()
    }

    AsyncFunction("getFreeDiskStorageAsync") { () -> Int64 in
    // Uses required reason API based on the following reason: E174.1 85F4.1
#if !os(tvOS)
      let resourceValues = try getResourceValues(from: documentDirectory, forKeys: [.volumeAvailableCapacityForImportantUsageKey])
      guard let availableCapacity = resourceValues?.volumeAvailableCapacityForImportantUsage else {
        throw CannotDetermineDiskCapacity()
      }
      return availableCapacity
#else
      let resourceValues = try getResourceValues(from: cacheDirectory, forKeys: [.volumeAvailableCapacityKey])
      guard let availableCapacity = resourceValues?.volumeAvailableCapacity else {
        throw CannotDetermineDiskCapacity()
      }
      return Int64(availableCapacity)
#endif
    }

    AsyncFunction("getTotalDiskCapacityAsync") { () -> Int in
        // Uses required reason API based on the following reason: E174.1 85F4.1
      let resourceValues = try getResourceValues(from: documentDirectory, forKeys: [.volumeTotalCapacityKey])

      guard let totalCapacity = resourceValues?.volumeTotalCapacity else {
        throw CannotDetermineDiskCapacity()
      }
      return totalCapacity
    }
  }
}
