import Foundation
import React
import UIKit
import UserNotifications

@objcMembers
public final class SirrusNotificationResponseBridge: NSObject {
  private static let pendingEventKey = "sirrus_pending_notification_event"

  @discardableResult
  @nonobjc
  public static func handleLaunchOptions(
    _ launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    guard
      let userInfo = launchOptions?[.remoteNotification] as? [AnyHashable: Any],
      isSirrusUserInfo(userInfo)
    else {
      return false
    }

    storePendingEvent(buildOpenedEvent(from: userInfo), source: "launchOptions")
    return true
  }

  @discardableResult
  public static func handleNotificationResponse(
    _ response: UNNotificationResponse
  ) -> Bool {
    let userInfo = response.notification.request.content.userInfo
    guard isSirrusUserInfo(userInfo) else {
      return false
    }

    let event = buildInteractionEvent(from: response)
    storePendingEvent(event, source: "AppDelegate")
    return true
  }

  public static func isSirrusNotificationResponse(
    _ response: UNNotificationResponse
  ) -> Bool {
    isSirrusUserInfo(response.notification.request.content.userInfo)
  }

  private static func storePendingEvent(_ event: [String: Any], source: String) {
    UserDefaults.standard.set(event, forKey: pendingEventKey)
    UserDefaults.standard.synchronize()

#if DEBUG
    let payload = event["payload"] as? [String: Any]
    NSLog(
      "SirrusSDK: Stored notification response from %@. type=%@ actionId=%@ messageId=%@",
      source,
      event["type"] as? String ?? "",
      event["actionId"] as? String ?? "",
      payload?["messageId"] as? String ?? ""
    )
#endif
  }

  private static func buildInteractionEvent(
    from response: UNNotificationResponse
  ) -> [String: Any] {
    let userInfo = response.notification.request.content.userInfo
    let actions = actionsValue(userInfo)
    let selectedAction = selectedAction(
      from: actions,
      responseActionIdentifier: response.actionIdentifier
    )
    let deeplinkUrl =
      selectedAction?["deeplinkUrl"] as? String ??
      stringValue(
        userInfo,
        keys: [
          "sirrus_deeplink_url",
          "sirrusDeeplinkUrl",
          "deeplinkUrl",
          "deepLinkUrl",
          "deepLinkUri",
          "deep_link_uri",
          "link",
        ]
      )
    let actionId = selectedAction.map(actionIdentifier) ?? response.actionIdentifier

    var event: [String: Any] = [
      "type": response.actionIdentifier == UNNotificationDefaultActionIdentifier ? "opened" : "action_pressed",
      "actionId": response.actionIdentifier == UNNotificationDefaultActionIdentifier ? "" : actionId,
      "deeplinkUrl": deeplinkUrl ?? "",
      "payload": buildPayload(from: userInfo),
    ]

    if let selectedAction = selectedAction {
      event["action"] = selectedAction
    }

    return event
  }

  private static func buildOpenedEvent(from userInfo: [AnyHashable: Any]) -> [String: Any] {
    [
      "type": "opened",
      "actionId": "",
      "deeplinkUrl": stringValue(userInfo, keys: ["sirrus_deeplink_url", "sirrusDeeplinkUrl", "deeplinkUrl", "deepLinkUrl", "deepLinkUri", "deep_link_uri", "link"]) ?? "",
      "payload": buildPayload(from: userInfo),
    ]
  }

  private static func buildPayload(from userInfo: [AnyHashable: Any]) -> [String: Any] {
    let rawPayload = normalizedUserInfo(userInfo)

    return [
      "title": stringValue(userInfo, keys: ["sirrus_title", "title"]) ?? apsAlertValue(userInfo, key: "title") ?? "",
      "body": stringValue(userInfo, keys: ["sirrus_body", "body"]) ?? apsAlertValue(userInfo, key: "body") ?? "",
      "subtitle": stringValue(userInfo, keys: ["sirrus_subtitle", "subtitle"]) ?? apsAlertValue(userInfo, key: "subtitle") ?? "",
      "messageId": stringValue(userInfo, keys: ["sirrus_message_id", "sirrusMessageId", "messageId", "message_id", "gcm.message_id", "google.message_id"]) ?? "",
      "campaignId": stringValue(userInfo, keys: ["sirrus_campaign_id", "sirrusCampaignId", "campaignId", "campaign_id"]) ?? "",
      "templateId": stringValue(userInfo, keys: ["sirrus_template_id", "sirrusTemplateId", "templateId", "template_id"]) ?? "",
      "provider": stringValue(userInfo, keys: ["sirrus_provider", "provider"]) ?? "apns",
      "deeplinkUrl": stringValue(userInfo, keys: ["sirrus_deeplink_url", "sirrusDeeplinkUrl", "deeplinkUrl", "deepLinkUrl", "deepLinkUri", "deep_link_uri", "link"]) ?? "",
      "imageUrl": mediaUrlValue(userInfo, mediaType: "image") ?? "",
      "gifUrl": stringValue(userInfo, keys: ["sirrus_gif_url", "sirrusGifUrl", "gifUrl", "gif_url"]) ?? "",
      "videoUrl": stringValue(userInfo, keys: ["sirrus_video_url", "sirrusVideoUrl", "videoUrl", "video_url"]) ?? "",
      "actions": actionsValue(userInfo),
      "data": rawPayload,
      "rawPayload": rawPayload,
    ]
  }

  private static func normalizedUserInfo(_ userInfo: [AnyHashable: Any]) -> [String: Any] {
    var normalized: [String: Any] = [:]

    for (key, value) in userInfo {
      guard let key = key as? String else {
        continue
      }

      if let normalizedValue = normalizedJsonValue(value) {
        normalized[key] = normalizedValue
      }
    }

    return normalized
  }

  private static func normalizedJsonValue(_ value: Any) -> Any? {
    if let value = value as? String {
      return value
    }

    if let value = value as? NSNumber {
      return value
    }

    if let value = value as? Bool {
      return value
    }

    if let value = value as? [AnyHashable: Any] {
      return normalizedUserInfo(value)
    }

    if let value = value as? [Any] {
      return value.compactMap { normalizedJsonValue($0) }
    }

    return nil
  }

  private static func isSirrusUserInfo(_ userInfo: [AnyHashable: Any]) -> Bool {
    let source = stringValue(userInfo, keys: ["source", "sdk_source", "vendor", "provider"])?.lowercased()
    return source == "martech" ||
      stringValue(userInfo, keys: ["sirrus_campaign_id", "sirrusCampaignId"]) != nil ||
      stringValue(userInfo, keys: ["sirrus_template_id", "sirrusTemplateId"]) != nil
  }

  private static func mediaUrlValue(
    _ userInfo: [AnyHashable: Any],
    mediaType: String
  ) -> String? {
    if mediaType == "image" {
      return stringValue(
        userInfo,
        keys: [
          "sirrus_image_url",
          "sirrusImageUrl",
          "sirrus_attachment_url",
          "sirrusAttachmentUrl",
          "imageUrl",
          "image_url",
          "image",
          "mediaUrl",
          "media_url",
        ]
      ) ??
        stringValue(nestedDictionary(userInfo, key: "fcm_options"), keys: ["image", "imageUrl"]) ??
        stringValue(nestedDictionary(userInfo, key: "fcmOptions"), keys: ["image", "imageUrl"])
    }

    return nil
  }

  private static func nestedDictionary(
    _ userInfo: [AnyHashable: Any],
    key: String
  ) -> [String: Any] {
    if let dictionary = userInfo[key] as? [String: Any] {
      return dictionary
    }

    if let dictionary = userInfo[key] as? [AnyHashable: Any] {
      return dictionary.reduce(into: [String: Any]()) { result, entry in
        guard let nestedKey = entry.key as? String else {
          return
        }
        result[nestedKey] = entry.value
      }
    }

    return [:]
  }

  private static func stringValue(_ userInfo: [AnyHashable: Any], keys: [String]) -> String? {
    for key in keys {
      if let value = userInfo[key] as? String, !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
        return value
      }

      if let value = userInfo[key] as? NSNumber {
        return value.stringValue
      }
    }

    return nil
  }

  private static func stringValue(_ dictionary: [String: Any], keys: [String]) -> String? {
    for key in keys {
      if let value = dictionary[key] as? String, !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
        return value
      }

      if let value = dictionary[key] as? NSNumber {
        return value.stringValue
      }
    }

    return nil
  }

  private static func apsAlertValue(_ userInfo: [AnyHashable: Any], key: String) -> String? {
    guard let aps = userInfo["aps"] as? [AnyHashable: Any] else {
      return nil
    }

    if key == "body", let alert = aps["alert"] as? String {
      return alert
    }

    guard let alert = aps["alert"] as? [AnyHashable: Any] else {
      return nil
    }

    return alert[key] as? String
  }

  private static func actionsValue(_ userInfo: [AnyHashable: Any]) -> [[String: Any]] {
    if let actions = userInfo["sirrus_actions"] as? [[String: Any]] {
      return actions
    }

    if let actions = userInfo["actions"] as? [[String: Any]] {
      return actions
    }

    guard
      let serialized = stringValue(userInfo, keys: ["sirrus_actions", "sirrusActions", "actions"]),
      let data = serialized.data(using: .utf8),
      let parsed = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]]
    else {
      return []
    }

    return parsed
  }

  private static func selectedAction(
    from actions: [[String: Any]],
    responseActionIdentifier: String
  ) -> [String: Any]? {
    if let exactMatch = actions.first(where: { actionIdentifier($0) == responseActionIdentifier }) {
      return exactMatch
    }

    guard let index = sirrusActionIndex(responseActionIdentifier),
      actions.indices.contains(index)
    else {
      return nil
    }

    return actions[index]
  }

  private static func sirrusActionIndex(_ identifier: String) -> Int? {
    switch identifier {
    case "sirrus_action_1":
      return 0
    case "sirrus_action_2":
      return 1
    case "sirrus_action_3":
      return 2
    case "sirrus_action_4":
      return 3
    default:
      return sirrusDynamicActionIndex(identifier)
    }
  }

  private static func sirrusDynamicActionIndex(_ identifier: String) -> Int? {
    let prefix = "sirrus_dynamic_action_"
    guard identifier.hasPrefix(prefix) else {
      return nil
    }

    let remaining = identifier.dropFirst(prefix.count)
    guard let indexToken = remaining.split(separator: "_").first,
      let oneBasedIndex = Int(indexToken),
      oneBasedIndex > 0
    else {
      return nil
    }

    return oneBasedIndex - 1
  }

  private static func actionIdentifier(_ action: [String: Any]) -> String {
    stringValue(action, keys: ["id", "actionId", "action_id", "identifier", "postback", "actionType"]) ??
      safeIdentifierSuffix(actionTitle(action))
  }

  private static func actionTitle(_ action: [String: Any]) -> String {
    stringValue(action, keys: ["title", "label", "name", "text"]) ?? "Open"
  }

  private static func safeIdentifierSuffix(_ value: String) -> String {
    let allowed = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "_-"))
    let suffix = value.unicodeScalars.map { scalar in
      allowed.contains(scalar) ? String(scalar) : "_"
    }.joined()

    return suffix.isEmpty ? UUID().uuidString : suffix
  }
}

@objc(SirrusReactNativeSdk)
class SirrusReactNativeSdk: RCTEventEmitter, UNUserNotificationCenterDelegate {
  private typealias NotificationPresentationCompletion =
    (UNNotificationPresentationOptions) -> Void

  private struct PendingOriginalForegroundPresentation {
    let notification: UNNotification
    let messageIdentifier: String
    var latestCompletionHandler: NotificationPresentationCompletion
    var callbackIdentifiers: Set<ObjectIdentifier>
    var workItem: DispatchWorkItem?
  }

  private let pendingEventKey = "sirrus_pending_notification_event"
  private let eventName = "sirrusNotificationEvent"
  private var hasListeners = false
  private let foregroundPresentationLock = NSLock()
  private var recentlyPresentedForegroundMessageIds: [String: Date] = [:]
  private var completedForegroundCallbacks: [String: [ObjectIdentifier: Date]] = [:]
  private var pendingOriginalForegroundPresentations:
    [String: PendingOriginalForegroundPresentation] = [:]
  private var completedOriginalForegroundRequestIds: [String: Date] = [:]
  private let foregroundPresentationDedupeWindow: TimeInterval = 5 * 60
  private weak var previousDelegate: UNUserNotificationCenterDelegate?
  private var notificationDelegateObserverTokens: [NSObjectProtocol] = []

  override init() {
    super.init()

    DispatchQueue.main.async {
      self.startNotificationDelegateMonitoring()
      self.installNotificationDelegate(reason: "module_init")
    }
  }

  deinit {
    notificationDelegateObserverTokens.forEach {
      NotificationCenter.default.removeObserver($0)
    }
  }

  @objc
  override static func requiresMainQueueSetup() -> Bool {
    false
  }

  override func supportedEvents() -> [String]! {
    [eventName]
  }

  override func startObserving() {
    hasListeners = true
  }

  override func stopObserving() {
    hasListeners = false
  }

  @objc
  func requestPermission(
    _ resolve: @escaping RCTPromiseResolveBlock,
    rejecter reject: @escaping RCTPromiseRejectBlock
  ) {
    UNUserNotificationCenter.current().requestAuthorization(
      options: [.alert, .badge, .sound]
    ) { granted, error in
      if let error {
        reject("SIRRUS_PERMISSION_ERROR", error.localizedDescription, error)
        return
      }

      resolve([
        "granted": granted,
        "status": granted ? "granted" : "denied",
      ])
    }
  }

  @objc
  func registerAndroidChannels(
    _ channels: [[String: Any]],
    resolver resolve: @escaping RCTPromiseResolveBlock,
    rejecter reject: @escaping RCTPromiseRejectBlock
  ) {
    resolve(nil)
  }

  @objc
  func registerIOSCategories(
    _ categories: [[String: Any]],
    resolver resolve: @escaping RCTPromiseResolveBlock,
    rejecter reject: @escaping RCTPromiseRejectBlock
  ) {
    DispatchQueue.main.async {
      self.installNotificationDelegate(reason: "register_ios_categories")
    }
    mergeNotificationCategories(categories.map(buildCategory)) {
      resolve(nil)
    }
  }

  @objc
  func showLocalNotification(
    _ payload: [String: Any],
    resolver resolve: @escaping RCTPromiseResolveBlock,
    rejecter reject: @escaping RCTPromiseRejectBlock
  ) {
    let content = UNMutableNotificationContent()
    content.title = payload["title"] as? String ?? ""
    content.body = payload["body"] as? String ?? ""
    content.subtitle = payload["subtitle"] as? String ?? ""
    content.sound = .default
    if #available(iOS 15.0, *) {
      content.interruptionLevel = .active
    }

    var userInfo = payload["rawPayload"] as? [String: Any] ?? [:]
    if let deeplinkUrl = payload["deeplinkUrl"] as? String {
      userInfo["sirrus_deeplink_url"] = deeplinkUrl
    }
    userInfo["sirrus_message_id"] = payload["messageId"] as? String
    userInfo["sirrus_campaign_id"] = payload["campaignId"] as? String
    userInfo["sirrus_template_id"] = payload["templateId"] as? String
    userInfo["sirrus_provider"] = payload["provider"] as? String
    userInfo["sirrus_title"] = payload["title"] as? String
    userInfo["sirrus_body"] = payload["body"] as? String
    userInfo["sirrus_subtitle"] = payload["subtitle"] as? String
    if let actions = payload["actions"] as? [[String: Any]] {
      userInfo["sirrus_actions"] = actions
    }
    userInfo["source"] = userInfo["source"] as? String ?? "martech"
    userInfo["sirrus_local_notification"] = true
    content.userInfo = userInfo

    let sourceMessageIdentifier = payload["messageId"] as? String ?? UUID().uuidString
    let foregroundMessageIdentifier =
      payload["sirrusDisplaySource"] as? String == "foreground_receive"
        ? sourceMessageIdentifier
        : nil
    let requestIdentifier = "\(sourceMessageIdentifier)_\(UUID().uuidString)"
    let actions = payload["actions"] as? [[String: Any]] ?? []
    if !actions.isEmpty, let categoryId = payload["categoryId"] as? String {
      content.categoryIdentifier = categoryId
    }

    if !actions.isEmpty {
      let categories = foregroundReplacementCategories(
        actions: actions,
        content: content,
        messageIdentifier: sourceMessageIdentifier
      )
      scheduleLocalNotificationWithOptionalAttachment(
        content: content,
        payload: payload,
        requestIdentifier: requestIdentifier,
        categoriesToRegister: categories,
        foregroundMessageIdentifier: foregroundMessageIdentifier,
        resolve: resolve,
        reject: reject
      )
      return
    }

    content.categoryIdentifier = ""
    scheduleLocalNotificationWithOptionalAttachment(
      content: content,
      payload: payload,
      requestIdentifier: requestIdentifier,
      categoriesToRegister: [],
      foregroundMessageIdentifier: foregroundMessageIdentifier,
      resolve: resolve,
      reject: reject
    )
  }

  private func scheduleLocalNotificationWithOptionalAttachment(
    content: UNMutableNotificationContent,
    payload: [String: Any],
    requestIdentifier: String,
    categoriesToRegister: [UNNotificationCategory],
    foregroundMessageIdentifier: String?,
    resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    let enqueueNotification = {
      self.registerCategoriesAndEnqueueNotification(
        categories: categoriesToRegister,
        content: content,
        identifier: requestIdentifier,
        trigger: foregroundMessageIdentifier == nil
          ? nil
          : self.foregroundNotificationTrigger(),
        foregroundMessageIdentifier: foregroundMessageIdentifier,
        resolve: resolve,
        reject: reject
      )
    }

    let attachmentUrl =
      payload["imageUrl"] as? String ??
      payload["gifUrl"] as? String ??
      payload["videoUrl"] as? String

    if let attachmentUrl {
      downloadAttachment(urlString: attachmentUrl) { attachment in
        if let attachment {
          content.attachments = [attachment]
        }
        enqueueNotification()
      }
      return
    }

    enqueueNotification()
  }

  @objc
  func cancelNotification(
    _ notificationId: String,
    resolver resolve: @escaping RCTPromiseResolveBlock,
    rejecter reject: @escaping RCTPromiseRejectBlock
  ) {
    UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [notificationId])
    UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [notificationId])
    resolve(nil)
  }

  @objc
  func cancelAllNotifications(
    _ resolve: @escaping RCTPromiseResolveBlock,
    rejecter reject: @escaping RCTPromiseRejectBlock
  ) {
    UNUserNotificationCenter.current().removeAllDeliveredNotifications()
    UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
    resolve(nil)
  }

  @objc
  func setBadgeCount(
    _ count: NSNumber,
    resolver resolve: @escaping RCTPromiseResolveBlock,
    rejecter reject: @escaping RCTPromiseRejectBlock
  ) {
    DispatchQueue.main.async {
      UIApplication.shared.applicationIconBadgeNumber = count.intValue
      resolve(nil)
    }
  }

  @objc
  func consumeInitialNotificationEvent(
    _ resolve: @escaping RCTPromiseResolveBlock,
    rejecter reject: @escaping RCTPromiseRejectBlock
  ) {
    let defaults = UserDefaults.standard
    guard let stored = defaults.dictionary(forKey: pendingEventKey) else {
      resolve(nil)
      return
    }

    defaults.removeObject(forKey: pendingEventKey)
    resolve(stored)
  }

  @objc
  func consumeLatestNativePushToken(
    _ resolve: @escaping RCTPromiseResolveBlock,
    rejecter reject: @escaping RCTPromiseRejectBlock
  ) {
    resolve(nil)
  }

  @objc(addListener:)
  override func addListener(_ eventName: String!) {
    super.addListener(eventName)
  }

  @objc(removeListeners:)
  override func removeListeners(_ count: Double) {
    super.removeListeners(count)
  }

  func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    willPresent notification: UNNotification,
    withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
  ) {
    let userInfo = notification.request.content.userInfo
    if isSirrusUserInfo(userInfo) {
      if isSirrusLocalNotification(userInfo) {
        let requestIdentifier = notification.request.identifier
        guard claimForegroundWillPresent(
          requestIdentifier: requestIdentifier,
          completionHandler: completionHandler
        ) else {
#if DEBUG
          NSLog(
            "SirrusSDK: Ignoring duplicate foreground completion block. requestId=%@",
            requestIdentifier
          )
#endif
          return
        }

        let messageIdentifier = stringValue(
          userInfo,
          keys: [
            "sirrus_message_id",
            "sirrusMessageId",
            "messageId",
            "message_id",
          ]
        ) ?? ""
        let presentationOptions = foregroundPresentationOptions()
#if DEBUG
        NSLog(
          "SirrusSDK: Foreground replacement presenting callback. requestId=%@ messageId=%@ options=%lu attachmentCount=%d attachmentType=%@",
          requestIdentifier,
          messageIdentifier,
          presentationOptions.rawValue,
          notification.request.content.attachments.count,
          notification.request.content.attachments.first?.type ?? "none"
        )
#endif
        logNotificationSettings(center)
        completionHandler(presentationOptions)
        emitOrStore(buildReceivedEvent(from: notification, displayedBySystem: true))
        return
      }

      let ownsNotificationCenter = center.delegate === self
      logForegroundDelivery(
        notification: notification,
        ownsNotificationCenter: ownsNotificationCenter
      )

      if ownsNotificationCenter {
        let messageIdentifier = stringValue(
          userInfo,
          keys: [
            "sirrus_message_id",
            "sirrusMessageId",
            "messageId",
            "message_id",
            "gcm.message_id",
            "google.message_id",
          ]
        ) ?? notification.request.identifier
        let isFirstReceive = claimForegroundPresentation(
          messageIdentifier: messageIdentifier
        )
        let presentationOptions = foregroundPresentationOptions()
        guard isFirstReceive else {
#if DEBUG
          NSLog(
            "SirrusSDK: Suppressing duplicate original foreground presentation. messageId=%@ requestId=%@",
            messageIdentifier,
            notification.request.identifier
          )
#endif
          completionHandler([])
          return
        }

        queueOriginalForegroundPresentation(
          notification: notification,
          messageIdentifier: messageIdentifier,
          presentationOptions: presentationOptions,
          completionHandler: completionHandler
        )
        emitOrStore(buildReceivedEvent(from: notification, displayedBySystem: true))
        return
      }

      // A delegate such as RNFirebase may already have completed the original
      // remote request with no presentation options before forwarding it. Only
      // that non-owning path needs the local replacement fallback.
      completionHandler([])
      presentForegroundReplacement(from: notification)
      return
    }

    if let previousDelegate {
      previousDelegate.userNotificationCenter?(
        center,
        willPresent: notification,
        withCompletionHandler: completionHandler
      )
      return
    }

    completionHandler([])
  }

  func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void
  ) {
    let userInfo = response.notification.request.content.userInfo
    if isSirrusUserInfo(userInfo) {
      let event = buildInteractionEvent(from: response)
      emitOrStore(event)
      completionHandler()
      return
    }

    if let previousDelegate {
      previousDelegate.userNotificationCenter?(
        center,
        didReceive: response,
        withCompletionHandler: completionHandler
      )
      return
    }

    completionHandler()
  }

  private func emitOrStore(_ event: [String: Any]) {
    if hasListeners {
      sendEvent(withName: eventName, body: event)
    } else {
      UserDefaults.standard.set(event, forKey: pendingEventKey)
    }
  }

  private func buildReceivedEvent(
    from notification: UNNotification,
    displayedBySystem: Bool = false
  ) -> [String: Any] {
    let userInfo = notification.request.content.userInfo
    return [
      "type": "received",
      "actionId": "",
      "deeplinkUrl": stringValue(userInfo, keys: ["sirrus_deeplink_url", "sirrusDeeplinkUrl", "deeplinkUrl", "deepLinkUrl", "deepLinkUri", "deep_link_uri", "link"]) ?? "",
      "displayedBySystem": displayedBySystem,
      "payload": buildPayload(from: userInfo),
    ]
  }

  private func buildInteractionEvent(from response: UNNotificationResponse) -> [String: Any] {
    let userInfo = response.notification.request.content.userInfo
    let actions = actionsValue(userInfo)
    let selectedAction = selectedAction(
      from: actions,
      responseActionIdentifier: response.actionIdentifier
    )

    let deeplinkUrl =
      selectedAction?["deeplinkUrl"] as? String ??
      stringValue(userInfo, keys: ["sirrus_deeplink_url", "sirrusDeeplinkUrl", "deeplinkUrl", "deepLinkUrl", "deepLinkUri", "deep_link_uri", "link"])
    let actionId = selectedAction.map(actionIdentifier) ?? response.actionIdentifier

    var event: [String: Any] = [
      "type": response.actionIdentifier == UNNotificationDefaultActionIdentifier ? "opened" : "action_pressed",
      "actionId": response.actionIdentifier == UNNotificationDefaultActionIdentifier ? "" : actionId,
      "deeplinkUrl": deeplinkUrl ?? "",
      "payload": buildPayload(from: userInfo),
    ]

    if let selectedAction = selectedAction {
      event["action"] = selectedAction
    }

    return event
  }

  private func buildPayload(from userInfo: [AnyHashable: Any]) -> [String: Any] {
    let rawPayload = normalizedUserInfo(userInfo)

    return [
      "title": stringValue(userInfo, keys: ["sirrus_title", "title"]) ?? apsAlertValue(userInfo, key: "title") ?? "",
      "body": stringValue(userInfo, keys: ["sirrus_body", "body"]) ?? apsAlertValue(userInfo, key: "body") ?? "",
      "subtitle": stringValue(userInfo, keys: ["sirrus_subtitle", "subtitle"]) ?? apsAlertValue(userInfo, key: "subtitle") ?? "",
      "messageId": stringValue(userInfo, keys: ["sirrus_message_id", "sirrusMessageId", "messageId", "message_id", "gcm.message_id", "google.message_id"]) ?? "",
      "campaignId": stringValue(userInfo, keys: ["sirrus_campaign_id", "sirrusCampaignId"]) ?? "",
      "templateId": stringValue(userInfo, keys: ["sirrus_template_id", "sirrusTemplateId"]) ?? "",
      "provider": stringValue(userInfo, keys: ["sirrus_provider", "provider"]) ?? "apns",
      "deeplinkUrl": stringValue(userInfo, keys: ["sirrus_deeplink_url", "sirrusDeeplinkUrl", "deeplinkUrl", "deepLinkUrl", "deepLinkUri", "deep_link_uri", "link"]) ?? "",
      "imageUrl": mediaUrlValue(userInfo, mediaType: "image") ?? "",
      "gifUrl": stringValue(userInfo, keys: ["sirrus_gif_url", "sirrusGifUrl", "gifUrl", "gif_url"]) ?? "",
      "videoUrl": stringValue(userInfo, keys: ["sirrus_video_url", "sirrusVideoUrl", "videoUrl", "video_url"]) ?? "",
      "actions": actionsValue(userInfo),
      "data": rawPayload,
      "rawPayload": rawPayload,
    ]
  }

  private func normalizedUserInfo(_ userInfo: [AnyHashable: Any]) -> [String: Any] {
    var normalized: [String: Any] = [:]

    for (key, value) in userInfo {
      guard let key = key as? String else {
        continue
      }

      if let normalizedValue = normalizedJsonValue(value) {
        normalized[key] = normalizedValue
      }
    }

    return normalized
  }

  private func normalizedJsonValue(_ value: Any) -> Any? {
    if let value = value as? String {
      return value
    }

    if let value = value as? NSNumber {
      return value
    }

    if let value = value as? Bool {
      return value
    }

    if let value = value as? [AnyHashable: Any] {
      return normalizedUserInfo(value)
    }

    if let value = value as? [Any] {
      return value.compactMap { normalizedJsonValue($0) }
    }

    return nil
  }

  private func isSirrusUserInfo(_ userInfo: [AnyHashable: Any]) -> Bool {
    let source = stringValue(userInfo, keys: ["source", "sdk_source", "vendor", "provider"])?.lowercased()
    return source == "martech" ||
      stringValue(userInfo, keys: ["sirrus_campaign_id", "sirrusCampaignId"]) != nil ||
      stringValue(userInfo, keys: ["sirrus_template_id", "sirrusTemplateId"]) != nil
  }

  private func isSirrusLocalNotification(_ userInfo: [AnyHashable: Any]) -> Bool {
    if let value = userInfo["sirrus_local_notification"] as? Bool {
      return value
    }

    if let value = userInfo["sirrus_local_notification"] as? NSNumber {
      return value.boolValue
    }

    if let value = userInfo["sirrus_local_notification"] as? String {
      return ["true", "1", "yes"].contains(value.lowercased())
    }

    return false
  }

  private func mediaUrlValue(
    _ userInfo: [AnyHashable: Any],
    mediaType: String
  ) -> String? {
    if mediaType == "image" {
      return stringValue(
        userInfo,
        keys: [
          "sirrus_image_url",
          "sirrusImageUrl",
          "sirrus_attachment_url",
          "sirrusAttachmentUrl",
          "imageUrl",
          "image_url",
          "image",
          "mediaUrl",
          "media_url",
        ]
      ) ??
        stringValue(nestedDictionary(userInfo, key: "fcm_options"), keys: ["image", "imageUrl"]) ??
        stringValue(nestedDictionary(userInfo, key: "fcmOptions"), keys: ["image", "imageUrl"])
    }

    return nil
  }

  private func nestedDictionary(
    _ userInfo: [AnyHashable: Any],
    key: String
  ) -> [String: Any] {
    if let dictionary = userInfo[key] as? [String: Any] {
      return dictionary
    }

    if let dictionary = userInfo[key] as? [AnyHashable: Any] {
      return dictionary.reduce(into: [String: Any]()) { result, entry in
        guard let nestedKey = entry.key as? String else {
          return
        }
        result[nestedKey] = entry.value
      }
    }

    return [:]
  }

  private func stringValue(_ userInfo: [AnyHashable: Any], keys: [String]) -> String? {
    for key in keys {
      if let value = userInfo[key] as? String, !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
        return value
      }

      if let value = userInfo[key] as? NSNumber {
        return value.stringValue
      }
    }

    return nil
  }

  private func stringValue(_ dictionary: [String: Any], keys: [String]) -> String? {
    for key in keys {
      if let value = dictionary[key] as? String, !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
        return value
      }

      if let value = dictionary[key] as? NSNumber {
        return value.stringValue
      }
    }

    return nil
  }

  private func apsAlertValue(_ userInfo: [AnyHashable: Any], key: String) -> String? {
    guard let aps = userInfo["aps"] as? [AnyHashable: Any] else {
      return nil
    }

    if key == "body", let alert = aps["alert"] as? String {
      return alert
    }

    guard let alert = aps["alert"] as? [AnyHashable: Any] else {
      return nil
    }

    return alert[key] as? String
  }

  private func actionsValue(_ userInfo: [AnyHashable: Any]) -> [[String: Any]] {
    if let actions = userInfo["sirrus_actions"] as? [[String: Any]] {
      return actions
    }

    if let actions = userInfo["actions"] as? [[String: Any]] {
      return actions
    }

    guard
      let serialized = stringValue(userInfo, keys: ["sirrus_actions", "sirrusActions", "actions"]),
      let data = serialized.data(using: .utf8),
      let parsed = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]]
    else {
      return []
    }

    return parsed
  }

  private func selectedAction(
    from actions: [[String: Any]],
    responseActionIdentifier: String
  ) -> [String: Any]? {
    if let exactMatch = actions.first(where: { actionIdentifier($0) == responseActionIdentifier }) {
      return exactMatch
    }

    guard let index = sirrusActionIndex(responseActionIdentifier),
      actions.indices.contains(index)
    else {
      return nil
    }

    return actions[index]
  }

  private func sirrusActionIndex(_ identifier: String) -> Int? {
    switch identifier {
    case "sirrus_action_1":
      return 0
    case "sirrus_action_2":
      return 1
    case "sirrus_action_3":
      return 2
    case "sirrus_action_4":
      return 3
    default:
      return sirrusDynamicActionIndex(identifier)
    }
  }

  private func sirrusDynamicActionIndex(_ identifier: String) -> Int? {
    let prefix = "sirrus_dynamic_action_"
    guard identifier.hasPrefix(prefix) else {
      return nil
    }

    let remaining = identifier.dropFirst(prefix.count)
    guard let indexToken = remaining.split(separator: "_").first,
      let oneBasedIndex = Int(indexToken),
      oneBasedIndex > 0
    else {
      return nil
    }

    return oneBasedIndex - 1
  }

  private func buildCategory(_ category: [String: Any]) -> UNNotificationCategory {
    let actions = (category["actions"] as? [[String: Any]] ?? []).map(buildAction)
    return UNNotificationCategory(
      identifier: category["id"] as? String ?? UUID().uuidString,
      actions: actions,
      intentIdentifiers: [],
      options: []
    )
  }

  private func buildAction(_ action: [String: Any]) -> UNNotificationAction {
    var options: UNNotificationActionOptions = []
    if (action["foreground"] as? Bool) == true {
      options.insert(.foreground)
    }
    if (action["destructive"] as? Bool) == true {
      options.insert(.destructive)
    }
    if (action["authenticationRequired"] as? Bool) == true {
      options.insert(.authenticationRequired)
    }

    return UNNotificationAction(
      identifier: actionIdentifier(action),
      title: actionTitle(action),
      options: options
    )
  }

  private func actionIdentifier(_ action: [String: Any]) -> String {
    stringValue(action, keys: ["id", "actionId", "action_id", "identifier", "postback", "actionType"]) ??
      safeIdentifierSuffix(actionTitle(action))
  }

  private func actionTitle(_ action: [String: Any]) -> String {
    stringValue(action, keys: ["title", "label", "name", "text"]) ?? "Open"
  }

  private func mergeNotificationCategories(
    _ categories: [UNNotificationCategory],
    completion: @escaping () -> Void
  ) {
    guard !categories.isEmpty else {
      completion()
      return
    }

    let center = UNUserNotificationCenter.current()
    center.getNotificationCategories { existingCategories in
      let incomingIds = Set(categories.map { $0.identifier })
      var mergedCategories = Set(existingCategories.filter {
        !incomingIds.contains($0.identifier)
      })
      categories.forEach { mergedCategories.insert($0) }
      center.setNotificationCategories(mergedCategories)
      completion()
    }
  }

  private func registerCategoriesAndEnqueueNotification(
    categories: [UNNotificationCategory],
    content: UNMutableNotificationContent,
    identifier: String,
    trigger: UNNotificationTrigger?,
    foregroundMessageIdentifier: String?,
    resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    mergeNotificationCategories(categories) {
      self.enqueueNotification(
        content: content,
        identifier: identifier,
        trigger: trigger,
        foregroundMessageIdentifier: foregroundMessageIdentifier,
        resolve: resolve,
        reject: reject
      )
    }
  }

  private func safeIdentifierSuffix(_ value: String) -> String {
    let allowed = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "_-"))
    return value.unicodeScalars.map { scalar in
      allowed.contains(scalar) ? String(scalar) : "_"
    }.joined()
  }

  private func enqueueNotification(
    content: UNMutableNotificationContent,
    identifier: String,
    trigger: UNNotificationTrigger?,
    foregroundMessageIdentifier: String?,
    resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    DispatchQueue.main.async {
      self.installNotificationDelegate(reason: "enqueue_notification")

      if let foregroundMessageIdentifier,
        !self.claimForegroundPresentation(
          messageIdentifier: foregroundMessageIdentifier
        )
      {
#if DEBUG
        NSLog(
          "SirrusSDK: Firebase foreground route skipped duplicate native display. messageId=%@",
          foregroundMessageIdentifier
        )
#endif
        resolve([
          "displayed": false,
          "reason": "duplicate_native_foreground_display",
        ])
        return
      }

      let request = UNNotificationRequest(
        identifier: identifier,
        content: content,
        trigger: trigger
      )

      UNUserNotificationCenter.current().add(request) { error in
        if let error {
          if let foregroundMessageIdentifier {
            self.releaseForegroundPresentation(
              messageIdentifier: foregroundMessageIdentifier
            )
          }
          reject("SIRRUS_SHOW_NOTIFICATION_ERROR", error.localizedDescription, error)
          return
        }

        resolve(nil)
      }
    }
  }

  private func presentForegroundReplacement(from notification: UNNotification) {
    let remoteContent = notification.request.content
    let content = UNMutableNotificationContent()
    content.title = remoteContent.title
    content.subtitle = remoteContent.subtitle
    content.body = remoteContent.body
    content.badge = remoteContent.badge
    content.sound = remoteContent.sound ?? .default
    content.categoryIdentifier = remoteContent.categoryIdentifier
    // Attachments delivered with the original remote notification have a
    // security-scoped URL owned by that request. Reusing the same attachment
    // object in a new local foreground notification can leave an apparently
    // valid attachment that iOS silently omits while rendering. The foreground
    // replacement creates its own attachment below instead.
    content.attachments = []
    let remoteAttachment = remoteContent.attachments.first

    var userInfo = remoteContent.userInfo
    let sourceMessageIdentifier =
      stringValue(
        userInfo,
        keys: [
          "sirrus_message_id",
          "sirrusMessageId",
          "messageId",
          "message_id",
          "gcm.message_id",
          "google.message_id",
        ]
      ) ?? notification.request.identifier

    let imageUrl = mediaUrlValue(userInfo, mediaType: "image")

    // Build a clean local payload. Remote-only APNs/FCM markers can cause
    // delegate interceptors to claim the replacement and complete it before
    // Sirrus supplies its foreground presentation options.
    userInfo["sirrus_message_id"] = sourceMessageIdentifier
    userInfo["sirrus_local_notification"] = true
    userInfo["sirrus_foreground_replacement"] = true
    userInfo["sirrus_title"] = remoteContent.title
    userInfo["sirrus_subtitle"] = remoteContent.subtitle
    userInfo["sirrus_body"] = remoteContent.body
    if let imageUrl {
      userInfo["sirrus_image_url"] = imageUrl
    }
    let transportKeys = userInfo.keys.filter { key in
      guard let stringKey = key as? String else {
        return false
      }
      let normalizedKey = stringKey.lowercased()
      return normalizedKey.hasPrefix("gcm.") || normalizedKey.hasPrefix("google.")
    }
    for key in transportKeys {
      userInfo.removeValue(forKey: key)
    }
    userInfo.removeValue(forKey: "aps")
    content.userInfo = userInfo

    if content.sound == nil {
      content.sound = .default
    }
    if #available(iOS 15.0, *) {
      content.interruptionLevel = .active
    }

    let actions = actionsValue(userInfo)
    let categories = foregroundReplacementCategories(
      actions: actions,
      content: content,
      messageIdentifier: sourceMessageIdentifier
    )
    let requestIdentifier =
      "sirrus_foreground_\(safeIdentifierSuffix(sourceMessageIdentifier))_\(UUID().uuidString)"

    let enqueueReplacement = {
      guard self.claimForegroundPresentation(
        messageIdentifier: sourceMessageIdentifier
      ) else {
#if DEBUG
        NSLog(
          "SirrusSDK: Skipping duplicate foreground replacement. messageId=%@ requestId=%@",
          sourceMessageIdentifier,
          notification.request.identifier
        )
#endif
        return
      }

      self.mergeNotificationCategories(categories) {
        DispatchQueue.main.async {
          self.installNotificationDelegate(reason: "foreground_replacement")

          let request = UNNotificationRequest(
            identifier: requestIdentifier,
            content: content,
            trigger: self.foregroundNotificationTrigger()
          )

          UNUserNotificationCenter.current().add(request) { error in
            if let error {
              self.releaseForegroundPresentation(
                messageIdentifier: sourceMessageIdentifier
              )
#if DEBUG
              NSLog(
                "SirrusSDK: Foreground replacement request failed. messageId=%@ error=%@",
                sourceMessageIdentifier,
                error.localizedDescription
              )
#endif
              self.emitOrStore(
                self.buildReceivedEvent(from: notification, displayedBySystem: false)
              )
              return
            }

#if DEBUG
            NSLog(
              "SirrusSDK: Foreground replacement enqueued. messageId=%@ hasAttachment=%@ actionCount=%d",
              sourceMessageIdentifier,
              content.attachments.isEmpty ? "false" : "true",
              actions.count
            )
#endif
          }
        }
      }
    }

    let enqueueWithAttachment = { (attachment: UNNotificationAttachment?) in
      if let attachment {
        content.attachments = [attachment]
      }
      enqueueReplacement()
    }

    let cloneRemoteAttachmentOrEnqueue = {
      guard let remoteAttachment else {
        enqueueWithAttachment(nil)
        return
      }

      self.cloneAttachmentForForegroundReplacement(remoteAttachment) { attachment in
        enqueueWithAttachment(attachment)
      }
    }

    guard let attachmentUrl = imageUrl else {
      cloneRemoteAttachmentOrEnqueue()
      return
    }

    downloadAttachment(urlString: attachmentUrl) { attachment in
      guard let attachment else {
        cloneRemoteAttachmentOrEnqueue()
        return
      }

      enqueueWithAttachment(attachment)
    }
  }

  private func foregroundReplacementCategories(
    actions: [[String: Any]],
    content: UNMutableNotificationContent,
    messageIdentifier: String
  ) -> [UNNotificationCategory] {
    guard !actions.isEmpty else {
      content.categoryIdentifier = ""
      return []
    }

    let categoryIdentifier = content.categoryIdentifier.isEmpty
      ? "sirrus_dynamic_\(safeIdentifierSuffix(messageIdentifier))"
      : content.categoryIdentifier
    content.categoryIdentifier = categoryIdentifier

    let notificationActions = actions.prefix(10).enumerated().map { index, action in
      buildDynamicAction(action, index: index)
    }
    return [
      UNNotificationCategory(
        identifier: categoryIdentifier,
        actions: Array(notificationActions),
        intentIdentifiers: [],
        options: []
      ),
    ]
  }

  private func buildDynamicAction(
    _ action: [String: Any],
    index: Int
  ) -> UNNotificationAction {
    var options: UNNotificationActionOptions = []
    if (action["foreground"] as? Bool) == true {
      options.insert(.foreground)
    }
    if (action["destructive"] as? Bool) == true {
      options.insert(.destructive)
    }
    if (action["authenticationRequired"] as? Bool) == true {
      options.insert(.authenticationRequired)
    }

    let baseIdentifier = actionIdentifier(action)
    return UNNotificationAction(
      identifier:
        "sirrus_dynamic_action_\(index + 1)_\(safeIdentifierSuffix(baseIdentifier))",
      title: actionTitle(action),
      options: options
    )
  }

  private func foregroundPresentationOptions() -> UNNotificationPresentationOptions {
    if #available(iOS 14.0, *) {
      return [.banner, .list, .sound]
    }

    return [.alert, .sound]
  }

  private func foregroundNotificationTrigger() -> UNNotificationTrigger {
    UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
  }

  private func logNotificationSettings(_ center: UNUserNotificationCenter) {
#if DEBUG
    center.getNotificationSettings { settings in
      NSLog(
        "SirrusSDK: Notification settings. authorization=%ld alerts=%ld notificationCenter=%ld lockScreen=%ld sound=%ld",
        settings.authorizationStatus.rawValue,
        settings.alertSetting.rawValue,
        settings.notificationCenterSetting.rawValue,
        settings.lockScreenSetting.rawValue,
        settings.soundSetting.rawValue
      )
    }
#endif
  }

  private func claimForegroundPresentation(messageIdentifier: String) -> Bool {
    let now = Date()

    foregroundPresentationLock.lock()
    defer { foregroundPresentationLock.unlock() }

    recentlyPresentedForegroundMessageIds =
      recentlyPresentedForegroundMessageIds.filter {
        now.timeIntervalSince($0.value) <= foregroundPresentationDedupeWindow
      }

    guard recentlyPresentedForegroundMessageIds[messageIdentifier] == nil else {
      return false
    }

    recentlyPresentedForegroundMessageIds[messageIdentifier] = now
    return true
  }

  private func queueOriginalForegroundPresentation(
    notification: UNNotification,
    messageIdentifier: String,
    presentationOptions: UNNotificationPresentationOptions,
    completionHandler: @escaping NotificationPresentationCompletion
  ) {
    let requestIdentifier = notification.request.identifier
    let callbackIdentifier = ObjectIdentifier(completionHandler as AnyObject)
    let now = Date()
    var workItemToSchedule: DispatchWorkItem?

    foregroundPresentationLock.lock()

    completedOriginalForegroundRequestIds =
      completedOriginalForegroundRequestIds.filter {
        now.timeIntervalSince($0.value) <= foregroundPresentationDedupeWindow
      }

    if completedOriginalForegroundRequestIds[requestIdentifier] != nil {
      foregroundPresentationLock.unlock()
#if DEBUG
      NSLog(
        "SirrusSDK: Ignoring late duplicate original foreground callback. requestId=%@",
        requestIdentifier
      )
#endif
      return
    }

    var pending = pendingOriginalForegroundPresentations[requestIdentifier] ??
      PendingOriginalForegroundPresentation(
        notification: notification,
        messageIdentifier: messageIdentifier,
        latestCompletionHandler: completionHandler,
        callbackIdentifiers: [],
        workItem: nil
      )

    if pending.callbackIdentifiers.contains(callbackIdentifier) {
      foregroundPresentationLock.unlock()
#if DEBUG
      NSLog(
        "SirrusSDK: Ignoring duplicate original foreground completion block. requestId=%@",
        requestIdentifier
      )
#endif
      return
    }

    pending.callbackIdentifiers.insert(callbackIdentifier)
    pending.latestCompletionHandler = completionHandler
    pending.workItem?.cancel()

    let workItem = DispatchWorkItem { [weak self] in
      self?.completeOriginalForegroundPresentation(
        requestIdentifier: requestIdentifier,
        presentationOptions: presentationOptions
      )
    }
    pending.workItem = workItem
    pendingOriginalForegroundPresentations[requestIdentifier] = pending
    workItemToSchedule = workItem
    foregroundPresentationLock.unlock()

    if let workItemToSchedule {
      DispatchQueue.main.asyncAfter(
        deadline: .now() + .milliseconds(150),
        execute: workItemToSchedule
      )
    }
  }

  private func completeOriginalForegroundPresentation(
    requestIdentifier: String,
    presentationOptions: UNNotificationPresentationOptions
  ) {
    let pending: PendingOriginalForegroundPresentation

    foregroundPresentationLock.lock()
    guard let storedPending = pendingOriginalForegroundPresentations.removeValue(
      forKey: requestIdentifier
    ) else {
      foregroundPresentationLock.unlock()
      return
    }
    pending = storedPending
    completedOriginalForegroundRequestIds[requestIdentifier] = Date()
    foregroundPresentationLock.unlock()

#if DEBUG
    NSLog(
      "SirrusSDK: Presenting original foreground Sirrus push once. requestId=%@ messageId=%@ options=%lu callbackCount=%d attachmentCount=%d attachmentType=%@ actionCount=%d",
      requestIdentifier,
      pending.messageIdentifier,
      presentationOptions.rawValue,
      pending.callbackIdentifiers.count,
      pending.notification.request.content.attachments.count,
      pending.notification.request.content.attachments.first?.type ?? "none",
      actionsValue(pending.notification.request.content.userInfo).count
    )
#endif
    logNotificationSettings(UNUserNotificationCenter.current())
    pending.latestCompletionHandler(presentationOptions)
  }

  private func claimForegroundWillPresent(
    requestIdentifier: String,
    completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
  ) -> Bool {
    let now = Date()
    let callbackIdentifier = ObjectIdentifier(completionHandler as AnyObject)

    foregroundPresentationLock.lock()
    defer { foregroundPresentationLock.unlock() }

    completedForegroundCallbacks = completedForegroundCallbacks.compactMapValues { callbacks in
      let activeCallbacks = callbacks.filter {
        now.timeIntervalSince($0.value) <= foregroundPresentationDedupeWindow
      }
      return activeCallbacks.isEmpty ? nil : activeCallbacks
    }

    var requestCallbacks = completedForegroundCallbacks[requestIdentifier] ?? [:]
    guard requestCallbacks[callbackIdentifier] == nil else {
      return false
    }

    requestCallbacks[callbackIdentifier] = now
    completedForegroundCallbacks[requestIdentifier] = requestCallbacks
    return true
  }

  private func releaseForegroundPresentation(messageIdentifier: String) {
    foregroundPresentationLock.lock()
    recentlyPresentedForegroundMessageIds.removeValue(forKey: messageIdentifier)
    foregroundPresentationLock.unlock()
  }

  private func startNotificationDelegateMonitoring() {
    guard notificationDelegateObserverTokens.isEmpty else {
      return
    }

    let notificationCenter = NotificationCenter.default
    let names: [Notification.Name] = [
      UIApplication.didBecomeActiveNotification,
      UIApplication.willEnterForegroundNotification,
    ]
    notificationDelegateObserverTokens = names.map { name in
      notificationCenter.addObserver(
        forName: name,
        object: nil,
        queue: .main
      ) { [weak self] _ in
        self?.installNotificationDelegate(reason: name.rawValue)
      }
    }
  }

  private func installNotificationDelegate(reason: String) {
    let center = UNUserNotificationCenter.current()
    guard center.delegate !== self else {
#if DEBUG
      let delegateName = center.delegate.map {
        String(describing: type(of: $0))
      } ?? "nil"
      NSLog(
        "SirrusSDK: Notification delegate already installed. reason=%@ activeDelegate=%@",
        reason,
        delegateName
      )
#endif
      return
    }

    let oldDelegate = center.delegate
    if let oldDelegate {
      previousDelegate = oldDelegate
    }
    center.delegate = self

#if DEBUG
    let oldDelegateName = oldDelegate.map {
      String(describing: type(of: $0))
    } ?? "nil"
    NSLog(
      "SirrusSDK: Installed notification delegate. reason=%@ previousDelegate=%@",
      reason,
      oldDelegateName
    )
#endif
  }

  private func logForegroundDelivery(
    notification: UNNotification,
    ownsNotificationCenter: Bool
  ) {
#if DEBUG
    let delegateName = UNUserNotificationCenter.current().delegate.map {
      String(describing: type(of: $0))
    } ?? "nil"
    let messageIdentifier =
      stringValue(
        notification.request.content.userInfo,
        keys: [
          "sirrus_message_id",
          "sirrusMessageId",
          "messageId",
          "message_id",
          "gcm.message_id",
          "google.message_id",
        ]
      ) ?? notification.request.identifier
    NSLog(
      "SirrusSDK: Foreground Sirrus push received. messageId=%@ activeDelegate=%@ ownsDelegate=%@",
      messageIdentifier,
      delegateName,
      ownsNotificationCenter ? "true" : "false"
    )
#endif
  }

  private func downloadAttachment(
    urlString: String,
    completion: @escaping (UNNotificationAttachment?) -> Void
  ) {
    guard let url = normalizedAttachmentUrl(urlString) else {
#if DEBUG
      NSLog("SirrusSDK: Rich media URL is invalid. value=%@", urlString)
#endif
      completion(nil)
      return
    }

    let configuration = URLSessionConfiguration.ephemeral
    configuration.timeoutIntervalForRequest = 15
    configuration.timeoutIntervalForResource = 20
    var request = URLRequest(url: url)
    // CDN URLs using content negotiation (including Unsplash auto=format)
    // must return a format supported by UNNotificationAttachment.
    request.setValue("image/jpeg,image/png,image/gif;q=0.9,*/*;q=0.1", forHTTPHeaderField: "Accept")

    URLSession(configuration: configuration).downloadTask(with: request) { tempUrl, response, error in
      guard error == nil, let tempUrl else {
#if DEBUG
        NSLog(
          "SirrusSDK: Rich media download failed. error=%@",
          error?.localizedDescription ?? "missing temporary file"
        )
#endif
        completion(nil)
        return
      }

      if let httpResponse = response as? HTTPURLResponse,
        !(200...299).contains(httpResponse.statusCode)
      {
#if DEBUG
        NSLog(
          "SirrusSDK: Rich media download returned HTTP %d.",
          httpResponse.statusCode
        )
#endif
        completion(nil)
        return
      }

      do {
        let mimeType = self.normalizedMimeType(response?.mimeType)
        let fileExtension = self.attachmentFileExtension(
          url: url,
          mimeType: mimeType
        )
        let targetUrl = try self.makeAttachmentFileUrl(fileExtension: fileExtension)
        try FileManager.default.moveItem(at: tempUrl, to: targetUrl)
        let attachment = try UNNotificationAttachment(
          identifier: UUID().uuidString,
          url: targetUrl,
          options: nil
        )
#if DEBUG
        let byteCount = (try? targetUrl.resourceValues(forKeys: [.fileSizeKey]).fileSize) ?? 0
        NSLog(
          "SirrusSDK: Rich media attachment created. type=%@ mime=%@ bytes=%d",
          attachment.type,
          mimeType ?? "unknown",
          byteCount
        )
#endif
        completion(attachment)
      } catch {
#if DEBUG
        NSLog(
          "SirrusSDK: Rich media attachment creation failed. error=%@",
          error.localizedDescription
        )
#endif
        completion(nil)
      }
    }.resume()
  }

  private func normalizedAttachmentUrl(_ value: String) -> URL? {
    let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
    let candidates = attachmentUrlCandidates(from: trimmed)

    for candidate in candidates {
      let normalizedCandidate = normalizeAttachmentUrlCandidate(candidate)
      if let url = URL(string: normalizedCandidate),
        let scheme = url.scheme?.lowercased(),
        scheme == "http" || scheme == "https"
      {
        return url
      }
    }

    return nil
  }

  private func attachmentUrlCandidates(from value: String) -> [String] {
    var candidates: [String] = []
    let patterns = [
      #"\[[^\]]*\]\((https?://[^)\s]+)\)"#,
      #"https?://[^\s\])>'"]+"#,
    ]

    for pattern in patterns {
      guard let expression = try? NSRegularExpression(pattern: pattern) else {
        continue
      }

      let matches = expression.matches(
        in: value,
        range: NSRange(value.startIndex..<value.endIndex, in: value)
      )

      for match in matches {
        let captureIndex = match.numberOfRanges > 1 ? 1 : 0
        guard let range = Range(match.range(at: captureIndex), in: value) else {
          continue
        }
        candidates.append(String(value[range]))
      }
    }

    candidates.append(value)
    return candidates
  }

  private func normalizeAttachmentUrlCandidate(_ value: String) -> String {
    value
      .trimmingCharacters(in: .whitespacesAndNewlines)
      .replacingOccurrences(of: #"\\&"#, with: "&")
      .replacingOccurrences(of: #"\&"#, with: "&")
      .replacingOccurrences(of: #"\\?"#, with: "?")
      .replacingOccurrences(of: #"\?"#, with: "?")
      .replacingOccurrences(of: #"\\="#, with: "=")
      .replacingOccurrences(of: #"\="#, with: "=")
      .replacingOccurrences(of: "&amp;", with: "&")
  }

  private func cloneAttachmentForForegroundReplacement(
    _ sourceAttachment: UNNotificationAttachment,
    completion: @escaping (UNNotificationAttachment?) -> Void
  ) {
    DispatchQueue.global(qos: .utility).async {
      let sourceUrl = sourceAttachment.url
      let accessedSecurityScope = sourceUrl.startAccessingSecurityScopedResource()
      defer {
        if accessedSecurityScope {
          sourceUrl.stopAccessingSecurityScopedResource()
        }
      }

      do {
        let fileExtension = self.fileExtensionFromTypeIdentifier(
          sourceAttachment.type,
          fallback: sourceUrl.pathExtension
        )
        let targetUrl = try self.makeAttachmentFileUrl(fileExtension: fileExtension)
        try FileManager.default.copyItem(at: sourceUrl, to: targetUrl)
        let attachment = try UNNotificationAttachment(
          identifier: UUID().uuidString,
          url: targetUrl,
          options: nil
        )
#if DEBUG
        NSLog(
          "SirrusSDK: Rich media attachment cloned for foreground replacement. type=%@",
          attachment.type
        )
#endif
        completion(attachment)
      } catch {
#if DEBUG
        NSLog(
          "SirrusSDK: Rich media attachment clone failed. error=%@",
          error.localizedDescription
        )
#endif
        completion(nil)
      }
    }
  }

  private func makeAttachmentFileUrl(fileExtension: String) throws -> URL {
    let directoryUrl = FileManager.default.temporaryDirectory
      .appendingPathComponent("sirrus-notification-\(UUID().uuidString)", isDirectory: true)
    try FileManager.default.createDirectory(
      at: directoryUrl,
      withIntermediateDirectories: true
    )
    return directoryUrl
      .appendingPathComponent("attachment")
      .appendingPathExtension(fileExtension)
  }

  private func normalizedMimeType(_ mimeType: String?) -> String? {
    mimeType?
      .split(separator: ";", maxSplits: 1)
      .first?
      .trimmingCharacters(in: .whitespacesAndNewlines)
      .lowercased()
  }

  private func attachmentFileExtension(url: URL, mimeType: String?) -> String {
    if let mimeType {
      return fileExtensionFromMimeType(mimeType)
    }

    let pathExtension = url.pathExtension.lowercased()
    return pathExtension.isEmpty ? "jpg" : pathExtension
  }

  private func fileExtensionFromTypeIdentifier(
    _ typeIdentifier: String,
    fallback: String
  ) -> String {
    switch typeIdentifier.lowercased() {
    case "public.png":
      return "png"
    case "com.compuserve.gif":
      return "gif"
    case "public.mpeg-4":
      return "mp4"
    case "com.apple.quicktime-movie":
      return "mov"
    case "public.jpeg", "public.jpg":
      return "jpg"
    default:
      return fallback.isEmpty ? "jpg" : fallback
    }
  }

  private func fileExtensionFromMimeType(_ mimeType: String?) -> String {
    switch normalizedMimeType(mimeType) {
    case "image/jpeg", "image/jpg":
      return "jpg"
    case "image/gif":
      return "gif"
    case "image/png":
      return "png"
    case "image/webp":
      return "webp"
    case "video/mp4":
      return "mp4"
    case "video/quicktime":
      return "mov"
    default:
      return "jpg"
    }
  }
}
