import React
import StorytellerGAMIntegration
import StorytellerSDK

public typealias StorytellerGamAdRequestEnricher = StorytellerGAMModuleConfiguration.AdRequestConfiguration

@objc(StorytellerGamSdk)
public class StorytellerGamSdk: NSObject {

  @MainActor private static var adRequestEnricher: StorytellerGamAdRequestEnricher?

  /// Registers an iOS-native callback that can enrich each prepared GAM request before it is loaded.
  /// Register the callback before JavaScript calls `setupGAMModule`.
  @MainActor public static func registerAdRequestEnricher(_ enricher: @escaping StorytellerGamAdRequestEnricher) {
    adRequestEnricher = enricher
  }

  /// Removes the iOS-native GAM request enricher used by subsequent `setupGAMModule` calls.
  @MainActor public static func clearAdRequestEnricher() {
    adRequestEnricher = nil
  }

  @objc var onAdUnitRequest: (([String: Any]) -> Void)?

  @objc func disableGoogleAdsModule() {
    suspendPendingRequests()
    DispatchQueue.main.async {
      Storyteller.shared.modules.removeAll { $0 is StorytellerGAMModule || $0 is StorytellerAdMobModule }
    }
  }

  @objc func setupGAMModule(_ configuration: [String: Any]) {
    let requestGeneration = suspendPendingRequests()
    DispatchQueue.main.async {
      let storiesCustomNativeTemplateId = configuration["customNativeTemplateId"] as? [String: Any]
      let storiesTemplateId = storiesCustomNativeTemplateId?["stories"] as? String
      let clipsTemplateId = storiesCustomNativeTemplateId?["clips"] as? String
      let customKvps = (configuration["customKvps"] as? [String: String]) ?? [:]
      let bottomBannerAdUnitProvider = Self.staticAdUnitProvider(configuration["bottomBannerAdUnit"] as? String)

      let gamConfiguration = StorytellerGAMModuleConfiguration(
        adUnit: { [weak self] adRequest in
          guard let self else { return "" }
          return await self.requestAdUnit(for: adRequest)
        },
        bottomBannerAdUnit: bottomBannerAdUnitProvider,
        customNativeTemplateIds: .init(stories: storiesTemplateId, clips: clipsTemplateId),
        customKvps: { customKvps },
        configureAdRequest: Self.adRequestEnricher
      )

      let gamModule = StorytellerGAMModule(configuration: gamConfiguration)
      Storyteller.shared.modules.removeAll { $0 is StorytellerGAMModule || $0 is StorytellerAdMobModule }
      Storyteller.shared.modules.append(gamModule)
      self.resumePendingRequests(for: requestGeneration)
    }
  }

  @objc func setupAdMobModule(_ configuration: [String: Any]) {
    let requestGeneration = suspendPendingRequests()
    DispatchQueue.main.async {
      let customKvps = (configuration["customKvps"] as? [String: String]) ?? [:]
      let enableBannerAdPriority = (configuration["enableBannerAdPriority"] as? Bool) ?? false

      let adMobConfiguration = StorytellerAdMobModuleConfiguration(
        adUnit: Self.staticAdUnitProvider(configuration["nativeAdUnit"] as? String) ?? { _ in "" },
        bannerAdUnit: Self.staticAdUnitProvider(configuration["bannerAdUnit"] as? String),
        bottomBannerAdUnit: Self.staticAdUnitProvider(configuration["bottomBannerAdUnit"] as? String),
        customKvps: { customKvps },
        enableBannerAdPriority: enableBannerAdPriority
      )

      let adMobModule = StorytellerAdMobModule(configuration: adMobConfiguration)
      Storyteller.shared.modules.removeAll { $0 is StorytellerGAMModule || $0 is StorytellerAdMobModule }
      Storyteller.shared.modules.append(adMobModule)
      self.resumePendingRequests(for: requestGeneration)
    }
  }

  @objc func setAdUnitResult(_ result: [String: Any]) {
    guard let requestId = result["requestId"] as? String else {
      NSLog("StorytellerGamSdk setAdUnitResult missing requestId")
      return
    }

    let adUnit = (result["adUnitId"] as? String) ?? ""

    guard let pendingRequest = takePendingRequest(for: requestId) else {
      return
    }

    pendingRequest.complete(with: adUnit)
  }

  // MARK: - Pending Requests

  private static let requestTimeout: DispatchTimeInterval = .seconds(5)

  private func requestAdUnit(for adRequest: StorytellerAdRequestInfo) async -> String {
    guard let emitter = onAdUnitRequest else {
      NSLog("StorytellerGamSdk missing event emitter; returning empty ad unit")
      return ""
    }

    let requestId = Self.generateRequestId()
    let pendingRequest = PendingRequest()
    guard storePendingRequest(pendingRequest, for: requestId) else {
      return ""
    }

    let payload: [String: Any] = [
      "requestId": requestId,
      "adRequest": adRequest.toDictionary()
    ]

    DispatchQueue.main.async {
      emitter(payload)
    }

    DispatchQueue.global().asyncAfter(deadline: .now() + Self.requestTimeout) {
      pendingRequest.timeout()
    }

    let waitResult = await pendingRequest.wait()
    removePendingRequest(for: requestId)

    switch waitResult {
    case .success(let adUnitId):
      return adUnitId
    case .timedOut:
      NSLog("StorytellerGamSdk request timed out for requestId: \(requestId)")
      return ""
    }
  }

  private static func generateRequestId() -> String {
    UUID().uuidString
  }

  private static func staticAdUnitProvider(_ adUnit: String?) -> ((StorytellerAdRequestInfo) async -> String)? {
    guard let adUnit, !adUnit.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
      return nil
    }

    return { _ in adUnit }
  }

  private final class PendingRequest: @unchecked Sendable {
    enum WaitResult {
      case success(String)
      case timedOut
    }

    private let lock = NSLock()
    private var continuation: CheckedContinuation<WaitResult, Never>?
    private var result: WaitResult?

    func complete(with adUnitId: String) {
      complete(.success(adUnitId))
    }

    func timeout() {
      complete(.timedOut)
    }

    func wait() async -> WaitResult {
      await withCheckedContinuation { continuation in
        var completedResult: WaitResult?

        lock.lock()
        if let result {
          completedResult = result
        } else {
          self.continuation = continuation
        }
        lock.unlock()

        if let completedResult {
          continuation.resume(returning: completedResult)
        }
      }
    }

    private func complete(_ result: WaitResult) {
      let continuationToResume: CheckedContinuation<WaitResult, Never>?

      lock.lock()
      guard self.result == nil else {
        lock.unlock()
        return
      }
      self.result = result
      continuationToResume = continuation
      continuation = nil
      lock.unlock()

      continuationToResume?.resume(returning: result)
    }
  }

  private let pendingRequestsLock = NSLock()
  private var pendingRequests: [String: PendingRequest] = [:]
  private var pendingRequestGeneration = 0
  private var acceptsPendingRequests = true

  private func storePendingRequest(_ pendingRequest: PendingRequest, for requestId: String) -> Bool {
    pendingRequestsLock.lock()
    guard acceptsPendingRequests else {
      pendingRequestsLock.unlock()
      return false
    }
    pendingRequests[requestId] = pendingRequest
    pendingRequestsLock.unlock()
    return true
  }

  private func takePendingRequest(for requestId: String) -> PendingRequest? {
    pendingRequestsLock.lock()
    let request = pendingRequests.removeValue(forKey: requestId)
    pendingRequestsLock.unlock()
    return request
  }

  private func removePendingRequest(for requestId: String) {
    pendingRequestsLock.lock()
    pendingRequests.removeValue(forKey: requestId)
    pendingRequestsLock.unlock()
  }

  @discardableResult
  private func suspendPendingRequests() -> Int {
    pendingRequestsLock.lock()
    pendingRequestGeneration += 1
    acceptsPendingRequests = false
    let requestGeneration = pendingRequestGeneration
    let requests = Array(pendingRequests.values)
    pendingRequests.removeAll()
    pendingRequestsLock.unlock()

    requests.forEach { $0.complete(with: "") }
    return requestGeneration
  }

  private func resumePendingRequests(for requestGeneration: Int) {
    pendingRequestsLock.lock()
    if pendingRequestGeneration == requestGeneration {
      acceptsPendingRequests = true
    }
    pendingRequestsLock.unlock()
  }
}

private extension StorytellerAdRequestInfo {
  func toDictionary() -> [String: Any] {
    switch self {
    case .clips(let collection, let clip, let nextClip, let adIndex):
      var clipsData: [String: Any] = [
        "collection": collection,
        "clip": clip.toDictionary(),
        "adIndex": adIndex
      ]

      if let nextClip = nextClip {
        clipsData["nextClip"] = nextClip.toDictionary()
      }

      return ["clips": clipsData]

    case .stories(let placement, let categories, let story, let adIndex):
      let storiesData: [String: Any] = [
        "placement": placement,
        "categories": categories,
        "story": story.toDictionary(),
        "adIndex": adIndex
      ]

      return ["stories": storiesData]

    @unknown default:
      NSLog("StorytellerGamSdk received unsupported ad request type")
      return [:]
    }
  }
}

private extension StorytellerAdRequestInfo.ItemInfo {
  func toDictionary() -> [String: Any] {
    [
      "contentId": contentId,
      "categories": categories.map { category in
        [
          "name": category.name,
          "externalId": category.externalId
        ]
      }
    ]
  }
}
