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

@preconcurrency import ExpoModulesCore

private let fetchRequestQueue = DispatchQueue(label: "expo.modules.fetch.RequestQueue")
@MainActor internal var urlSessionConfigurationProvider: NSURLSessionConfigurationProvider?

@MainActor public final class ExpoFetchModule: @preconcurrency Module {
  private lazy var urlSession = createURLSession()
  private let urlSessionDelegate: URLSessionSessionDelegateProxy

  public required init(appContext: AppContext) {
    urlSessionDelegate = URLSessionSessionDelegateProxy(dispatchQueue: fetchRequestQueue)
    super.init(appContext: appContext)
  }

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

    OnDestroy {
      urlSession.invalidateAndCancel()
    }

    // swiftlint:disable:next closure_body_length
    Class(NativeResponse.self) {
      Constructor {
        return NativeResponse(dispatchQueue: fetchRequestQueue)
      }

      AsyncFunction("startStreaming") { (response: NativeResponse) -> Data? in
        return response.startStreaming()
      }.runOnQueue(fetchRequestQueue)

      AsyncFunction("cancelStreaming") { (response: NativeResponse, _ reason: String) in
        response.cancelStreaming()
      }.runOnQueue(fetchRequestQueue)

      Property("bodyUsed", \.bodyUsed)

      Property("_rawHeaders") { (response: NativeResponse) in
        return response.responseInit?.headers ?? []
      }

      Property("status") { (response: NativeResponse) in
        return response.responseInit?.status ?? -1
      }

      Property("statusText") { (response: NativeResponse) in
        return response.responseInit?.statusText ?? ""
      }

      Property("url") { (response: NativeResponse) in
        return response.responseInit?.url ?? ""
      }

      Property("redirected", \.redirected)

      AsyncFunction("arrayBuffer") { (response: NativeResponse, promise: Promise) in
        response.waitFor(states: [.bodyCompleted]) { _ in
          let data = response.sink.finalize()
          promise.resolve(data)
        }
      }.runOnQueue(fetchRequestQueue)

      AsyncFunction("text") { (response: NativeResponse, promise: Promise) in
        response.waitFor(states: [.bodyCompleted]) { _ in
          let data = response.sink.finalize()
          let text = String(decoding: data, as: UTF8.self)
          promise.resolve(text)
        }
      }.runOnQueue(fetchRequestQueue)
    }

    Class(NativeRequest.self) {
      Constructor { (nativeResponse: NativeResponse) in
        return NativeRequest(response: nativeResponse)
      }

      AsyncFunction("start") { (request: NativeRequest, url: URL, requestInit: NativeRequestInit, requestBody: Data?, promise: Promise) in
        request.start(
          urlSession: urlSession,
          urlSessionDelegate: urlSessionDelegate,
          url: url,
          requestInit: requestInit,
          requestBody: requestBody
        )
        request.response.waitFor(states: [.responseReceived, .errorReceived]) { state in
          if state == .responseReceived {
            promise.resolve()
          } else if state == .errorReceived {
            promise.reject(request.response.error ?? FetchUnknownException())
          }
        }
      }.runOnQueue(fetchRequestQueue)

      AsyncFunction("cancel") { (request: NativeRequest) in
        request.cancel(urlSessionDelegate: self.urlSessionDelegate)
      }.runOnQueue(fetchRequestQueue)
    }
  }

  private func createURLSession() -> URLSession {
    let config: URLSessionConfiguration
    if let urlSessionConfigurationProvider, let concreteConfig = urlSessionConfigurationProvider() {
      config = concreteConfig
    } else {
      config = URLSessionConfiguration.default
      config.httpShouldSetCookies = true
      config.httpCookieAcceptPolicy = .always
      config.httpCookieStorage = HTTPCookieStorage.shared

      let useWifiOnly = Bundle.main.infoDictionary?["ReactNetworkForceWifiOnly"] as? Bool ?? false
      if useWifiOnly {
        config.allowsCellularAccess = !useWifiOnly
      }
    }
    return URLSession(configuration: config, delegate: urlSessionDelegate, delegateQueue: nil)
  }
}
