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

import ExpoModulesCore

@available(iOS 14, tvOS 14, *)
public final class FileSystemModule: Module {
  #if os(iOS)
  private lazy var filePickingHandler = FilePickingHandler(module: self)
  #endif

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

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

  var totalDiskSpace: Int64? {
    guard let path = documentDirectory?.path,
      let attributes = try? FileManager.default.attributesOfFileSystem(forPath: path) else {
      return nil
    }
    return attributes[.systemFreeSize] as? Int64
  }

  var availableDiskSpace: Int64? {
    guard let path = documentDirectory?.path,
      let attributes = try? FileManager.default.attributesOfFileSystem(forPath: path) else {
      return nil
    }
    return attributes[.systemFreeSize] as? Int64
  }

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

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

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

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

    Constant("appleSharedContainers") {
      return getAppleSharedContainers()
    }

    Property("totalDiskSpace") {
      return totalDiskSpace
    }

    Property("availableDiskSpace") {
      return availableDiskSpace
    }

    // swiftlint:disable:next closure_body_length
    AsyncFunction("downloadFileAsync") { (url: URL, to: FileSystemPath, options: DownloadOptions?, promise: Promise) in
      try to.validatePermission(.write)

      var request = URLRequest(url: url)

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

      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 {
          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)
          // TODO: Remove .url.absoluteString once returning shared objects works
          promise.resolve(destination.absoluteString)
        } catch {
          promise.reject(error)
        }
      }
      downloadTask.resume()
    }

    AsyncFunction("pickDirectoryAsync") { (initialUri: URL?, promise: Promise) in
      #if os(iOS)
      filePickingHandler.presentDocumentPicker(
        picker: createDirectoryPicker(initialUri: initialUri),
        isDirectory: true,
        initialUri: initialUri,
        mimeType: nil,
        promise: promise
      )
      #else
      promise.reject(FeatureNotAvailableOnPlatformException())
      #endif
    }.runOnQueue(.main)

    AsyncFunction("pickFileAsync") { (initialUri: URL?, mimeType: String?, promise: Promise) in
      #if os(iOS)
      filePickingHandler.presentDocumentPicker(
        picker: createFilePicker(initialUri: initialUri, mimeType: mimeType),
        isDirectory: false,
        initialUri: initialUri,
        mimeType: mimeType,
        promise: promise
      )
      #else
      promise.reject(FeatureNotAvailableOnPlatformException())
      #endif
    }.runOnQueue(.main)

    Function("info") { (url: URL) in
      let output = PathInfo()
      output.exists = false
      output.isDirectory = nil

      guard let fileSystemManager = appContext?.fileSystem else {
        return output
      }

      if fileSystemManager.getPathPermissions(url.path).contains(.read) {
        var isDirectory: ObjCBool = false
        if FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory) {
          output.exists = true
          output.isDirectory = isDirectory.boolValue
          return output
        }
      }
      return output
    }

    // swiftlint:disable:next closure_body_length
    Class(FileSystemFile.self) {
      Constructor { (url: URL) in
        return FileSystemFile(url: url.standardizedFileURL)
      }

      // we can't throw in a constructor, so this is a workaround
      Function("validatePath") { file in
        try file.validatePath()
      }

      // maybe asString, readAsString, readAsText, readText, ect.
      AsyncFunction("text") { file in
        return try file.text()
      }

      Function("textSync") { file in
        return try file.text()
      }

      AsyncFunction("base64") { file in
        return try file.base64()
      }

      Function("base64Sync") { file in
        return try file.base64()
      }

      AsyncFunction("bytes") { file in
        return try file.bytes()
      }

      Function("bytesSync") { file in
        return try file.bytes()
      }

      Function("open") { file in
        return try FileSystemFileHandle(file: file)
      }

      Function("info") { (file: FileSystemFile, options: InfoOptions?) in
        return try file.info(options: options ?? InfoOptions())
      }

      Function("write") { (file: FileSystemFile, content: Either<String, TypedArray>, options: WriteOptions?) in
        let append = options?.append ?? false
        if let content: String = content.get() {
          if options?.encoding == WriteEncoding.base64 {
            guard let data = Data(base64Encoded: content, options: .ignoreUnknownCharacters) else {
              throw UnableToWriteBase64DataException(file.url.absoluteString)
            }
            try file.write(data, append: append)
          } else {
            try file.write(content, append: append)
          }
        }
        if let content: TypedArray = content.get() {
          try file.write(content, append: append)
        }
      }

      Property("size") { file in
        try? file.size
      }

      Property("md5") { file in
        try? file.md5
      }

      Property("modificationTime") { file in
        try? file.modificationTime
      }

      Property("creationTime") { file in
        try? file.creationTime
      }

      Property("type") { file in
        file.type
      }

      Function("delete") { file in
        try file.delete()
      }

      Property("exists") { file in
        return file.exists
      }

      Function("create") { (file, options: CreateOptions?) in
        try file.create(options ?? CreateOptions())
      }

      Function("copy") { (file, to: FileSystemPath) in
        try file.copy(to: to)
      }

      Function("move") { (file, to: FileSystemPath) in
        try file.move(to: to)
      }

      Function("rename") { (file, newName: String) in
        try file.rename(newName)
      }

      Property("uri") { file in
        return file.url.absoluteString
      }
    }

    Class(FileSystemFileHandle.self) {
      Function("readBytes") { (fileHandle, bytes: Int) in
        try fileHandle.read(bytes)
      }

      Function("writeBytes") { (fileHandle, bytes: Data) in
        try fileHandle.write(bytes)
      }

      Function("close") { fileHandle in
        try fileHandle.close()
      }

      Property("offset") { fileHandle in
        fileHandle.offset
      }.set { (fileHandle, volume: UInt64) in
        fileHandle.offset = volume
      }

      Property("size") { fileHandle in
        fileHandle.size
      }
    }

    // swiftlint:disable:next closure_body_length
    Class(FileSystemDirectory.self) {
      Constructor { (url: URL) in
        return FileSystemDirectory(url: url.standardizedFileURL)
      }

      Function("info") { directory in
        try directory.info()
      }

      // we can't throw in a constructor, so this is a workaround
      Function("validatePath") { directory in
        try directory.validatePath()
      }

      Function("delete") { directory in
        try directory.delete()
      }

      Property("exists") { directory in
        return directory.exists
      }

      Function("create") { (directory, options: CreateOptions?) in
        try directory.create(options ?? CreateOptions())
      }

      Function("copy") { (directory, to: FileSystemPath) in
        try directory.copy(to: to)
      }

      Function("move") { (directory, to: FileSystemPath) in
        try directory.move(to: to)
      }

      Function("rename") { (directory, newName: String) in
        try directory.rename(newName)
      }

      // this function is internal and will be removed in the future (when returning arrays of shared objects is supported)
      Function("listAsRecords") { directory in
        try directory.listAsRecords()
      }

      Function("createFile") { (directory, name: String, content: String?) in
        let file = FileSystemFile(url: directory.url.appendingPathComponent(name))
        try file.create(CreateOptions())
        return file
      }

      Function("createDirectory") { (directory, name: String) in
        let newDirectory = FileSystemDirectory(url: directory.url.appendingPathComponent(name))
        try newDirectory.create(CreateOptions())
        return newDirectory
      }

      Property("uri") { directory in
        return directory.url.absoluteString
      }

      Property("size") { directory in
        return try? directory.size
      }
    }
  }

  private func getAppleSharedContainers() -> [String: String] {
    guard let appContext else {
      return [:]
    }
    var result: [String: String] = [:]
    for appGroup in appContext.appCodeSignEntitlements.appGroups ?? [] {
      if let directory = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) {
        result[appGroup] = directory.standardizedFileURL.path
      }
    }
    return result
  }
}
