import Foundation
import UserNotifications

open class SirrusNotificationServiceExtension: UNNotificationServiceExtension {
  private var contentHandler: ((UNNotificationContent) -> Void)?
  private var bestAttemptContent: UNMutableNotificationContent?

  open override func didReceive(
    _ request: UNNotificationRequest,
    withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
  ) {
    self.contentHandler = contentHandler

    guard let bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent else {
      contentHandler(request.content)
      return
    }

    self.bestAttemptContent = bestAttemptContent
    SirrusNotificationServiceProcessor.process(
      request: request,
      content: bestAttemptContent
    ) { [weak self] processedContent in
      self?.contentHandler?(processedContent)
      self?.contentHandler = nil
    }
  }

  open override func serviceExtensionTimeWillExpire() {
    guard let contentHandler, let bestAttemptContent else {
      return
    }

    contentHandler(bestAttemptContent)
    self.contentHandler = nil
  }
}

public enum SirrusNotificationServiceProcessor {
  private static let dynamicCategoryPrefix = "sirrus_dynamic_"
  private static let dynamicActionPrefix = "sirrus_dynamic_action_"

  public static func process(
    request: UNNotificationRequest,
    content: UNMutableNotificationContent,
    completion: @escaping (UNMutableNotificationContent) -> Void
  ) {
    guard isSirrusUserInfo(content.userInfo) else {
      completion(content)
      return
    }

    let actions = actionsValue(content.userInfo)
    let attachmentUrl = mediaUrlValue(content.userInfo)

#if DEBUG
    NSLog(
      "SirrusSDK: Notification service extension processing Sirrus push. actions=%d hasMedia=%@",
      actions.count,
      attachmentUrl == nil ? "false" : "true"
    )
#endif

    applyDynamicActions(actions, request: request, content: content) {
      guard let attachmentUrl else {
        completion(content)
        return
      }

      downloadAttachment(urlString: attachmentUrl) { attachment in
        if let attachment {
          content.attachments = [attachment]
#if DEBUG
          NSLog("SirrusSDK: Notification service extension attached rich media.")
#endif
        } else {
#if DEBUG
          NSLog("SirrusSDK: Notification service extension could not attach rich media.")
#endif
        }
        completion(content)
      }
    }
  }

  private static func applyDynamicActions(
    _ actions: [[String: Any]],
    request: UNNotificationRequest,
    content: UNMutableNotificationContent,
    completion: @escaping () -> Void
  ) {
    guard !actions.isEmpty else {
      content.categoryIdentifier = ""
      completion()
      return
    }

    let categoryId =
      stringValue(
        content.userInfo,
        keys: [
          "sirrus_category_id",
          "sirrusCategoryId",
          "iosCategory",
          "categoryId",
          "category_id",
        ]
      ) ??
      (content.categoryIdentifier.isEmpty ? nil : content.categoryIdentifier) ??
      "\(dynamicCategoryPrefix)\(safeIdentifierSuffix(messageIdentifier(content.userInfo, request: request)))"

    let notificationActions = actions.prefix(10).enumerated().map { index, action in
      buildAction(action, index: index)
    }

    let category = UNNotificationCategory(
      identifier: categoryId,
      actions: Array(notificationActions),
      intentIdentifiers: [],
      options: []
    )

    var userInfo = content.userInfo
    userInfo["sirrus_dynamic_category_id"] = categoryId
    if userInfo["sirrus_actions"] == nil, let serializedActions = serializeActions(actions) {
      userInfo["sirrus_actions"] = serializedActions
    }
    content.userInfo = userInfo
    content.categoryIdentifier = categoryId

    mergeNotificationCategories([category], completion: completion)
  }

  private static func buildAction(_ action: [String: Any], index: Int) -> UNNotificationAction {
    var options: UNNotificationActionOptions = []
    if boolValue(action["foreground"]) {
      options.insert(.foreground)
    }
    if boolValue(action["destructive"]) {
      options.insert(.destructive)
    }
    if boolValue(action["authenticationRequired"]) {
      options.insert(.authenticationRequired)
    }

    let actionId =
      stringValue(action, keys: ["id", "actionId", "action_id", "identifier", "postback", "actionType"]) ??
      "action_\(index + 1)"
    let identifier = "\(dynamicActionPrefix)\(index + 1)_\(safeIdentifierSuffix(actionId))"
    let title = stringValue(action, keys: ["title", "label", "name", "text"]) ?? "Open"

    return UNNotificationAction(
      identifier: identifier,
      title: title,
      options: options
    )
  }

  private static func mergeNotificationCategories(
    _ categories: [UNNotificationCategory],
    completion: @escaping () -> Void
  ) {
    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 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 mediaUrlValue(_ userInfo: [AnyHashable: Any]) -> String? {
    return stringValue(
      userInfo,
      keys: [
        "sirrus_image_url",
        "sirrusImageUrl",
        "sirrus_attachment_url",
        "sirrusAttachmentUrl",
        "imageUrl",
        "image_url",
        "image",
        "mediaUrl",
        "media_url",
        "sirrus_gif_url",
        "sirrusGifUrl",
        "gifUrl",
        "gif_url",
        "sirrus_video_url",
        "sirrusVideoUrl",
        "videoUrl",
        "video_url",
      ]
    ) ??
      stringValue(nestedDictionary(userInfo, key: "fcm_options"), keys: ["image", "imageUrl"]) ??
      stringValue(nestedDictionary(userInfo, key: "fcmOptions"), keys: ["image", "imageUrl"])
  }

  private static func messageIdentifier(
    _ userInfo: [AnyHashable: Any],
    request: UNNotificationRequest
  ) -> String {
    return stringValue(
      userInfo,
      keys: ["sirrus_message_id", "sirrusMessageId", "messageId", "message_id"]
    ) ?? request.identifier
  }

  private static func serializeActions(_ actions: [[String: Any]]) -> String? {
    guard JSONSerialization.isValidJSONObject(actions),
      let data = try? JSONSerialization.data(withJSONObject: actions, options: []),
      let serialized = String(data: data, encoding: .utf8)
    else {
      return nil
    }

    return serialized
  }

  private static func boolValue(_ value: Any?) -> Bool {
    if let value = value as? Bool {
      return value
    }
    if let value = value as? NSNumber {
      return value.boolValue
    }
    if let value = value as? String {
      return ["true", "1", "yes"].contains(value.lowercased())
    }
    return false
  }

  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 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(_ 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 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 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
  }

  private static func downloadAttachment(
    urlString: String,
    completion: @escaping (UNNotificationAttachment?) -> Void
  ) {
    guard let url = normalizedAttachmentUrl(urlString) else {
      completion(nil)
      return
    }

    let configuration = URLSessionConfiguration.ephemeral
    configuration.timeoutIntervalForRequest = 25
    configuration.timeoutIntervalForResource = 25
    var request = URLRequest(url: url)
    request.setValue("image/jpeg,image/png,image/gif;q=0.9,*/*;q=0.1", forHTTPHeaderField: "Accept")

    URLSession(configuration: configuration).downloadTask(with: request) { temporaryUrl, response, _ in
      guard let temporaryUrl else {
        completion(nil)
        return
      }

      let fileExtension: String
      if url.pathExtension.isEmpty {
        fileExtension = fileExtensionFromMimeType(response?.mimeType)
      } else {
        fileExtension = url.pathExtension
      }
      let localUrl = URL(fileURLWithPath: NSTemporaryDirectory())
        .appendingPathComponent(UUID().uuidString)
        .appendingPathExtension(fileExtension)

      do {
        try FileManager.default.moveItem(at: temporaryUrl, to: localUrl)
        completion(try UNNotificationAttachment(identifier: UUID().uuidString, url: localUrl))
      } catch {
        completion(nil)
      }
    }.resume()
  }

  private static 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 static 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 static 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 static func fileExtensionFromMimeType(_ mimeType: String?) -> String {
    switch mimeType?.lowercased() {
    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"
    }
  }
}
