import CryptoKit
import Foundation
import Security
import UserNotifications

class NotificationService: UNNotificationServiceExtension {

    var contentHandler: ((UNNotificationContent) -> Void)?
    var bestAttemptContent: UNMutableNotificationContent?

    override func didReceive(
        _ request: UNNotificationRequest,
        withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
    ) {
        self.contentHandler = contentHandler
        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)

        // Store the current timestamp in Keychain for iOS FCM messaging tracking
        let nowMs = String(format: "%.0f", Date().timeIntervalSince1970 * 1000)
        writeToKeychain(key: "last_fcm_message_timestamp", value: nowMs)
        NSLog("Received APNs push notification, updated last_fcm_message_timestamp to \(nowMs)")

        if let bestAttemptContent = bestAttemptContent {

            guard bestAttemptContent.userInfo as? [String: Any] != nil,
                let push_data = bestAttemptContent.userInfo["push_data"] as? String
            else {
                return contentHandler(bestAttemptContent)
            }

            let data = getPushNotificationData(pushData: push_data)

            if data != nil {
                if data!.title == "blocked" {
                    NSLog("Block message because user is blocked!")
                    // https://developer.apple.com/documentation/bundleresources/entitlements/com.apple.developer.usernotifications.filtering
                    return contentHandler(UNNotificationContent())
                }
                bestAttemptContent.title = data!.title
                bestAttemptContent.body = data!.body
                bestAttemptContent.threadIdentifier = String(format: "%d", data!.notificationId)
            } else {
                NSLog("Could not decrypt message. Show default.")
                bestAttemptContent.title = "\(bestAttemptContent.title)"
            }

            contentHandler(bestAttemptContent)
        }
    }

    override func serviceExtensionTimeWillExpire() {
        // Called just before the extension will be terminated by the system.
        // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
        if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
            contentHandler(bestAttemptContent)
        }
    }

}

func getPushNotificationData(pushData: String) -> (
    title: String, body: String, notificationId: Int64
)? {

    guard let data = Data(base64Encoded: pushData) else {
        NSLog("Failed to decode base64 string")
        return nil
    }

    do {
        let pushData = try EncryptedPushNotification(serializedBytes: data)

        var pushNotification: PushNotification?
        var pushUser: PushUser?

        // Check the keyId
        if pushData.keyID == 0 {
            let key = "InsecureOnlyUsedForAddingContact".data(using: .utf8)!
            pushNotification = tryDecryptMessage(key: key, pushData: pushData)
        } else {
            let pushUsers = getPushUsers()
            if pushUsers != nil {
                for tryPushUser in pushUsers! {
                    for pushKey in tryPushUser.pushKeys {
                        if pushKey.id == pushData.keyID {
                            pushNotification = tryDecryptMessage(
                                key: pushKey.key, pushData: pushData)
                            if pushNotification != nil {
                                pushUser = tryPushUser
                                if isUUIDNewer(pushUser!.lastMessageID, pushNotification!.messageID)
                                {
                                    //return ("blocked", "blocked", 0)
                                }
                                break
                            }
                        }
                    }
                    if pushUser != nil { break }
                }
            } else {
                NSLog("pushKeys are empty")
            }
        }

        if pushUser?.blocked == true {
            return ("blocked", "blocked", 0)
        }

        // Handle the push notification based on the pushKind
        if let pushNotification = pushNotification {

            if pushNotification.kind == .testNotification {
                return ("Test Notification", "This is a test notification.", 0)
            } else if pushUser != nil {
                return (
                    pushUser!.displayName,
                    getPushNotificationText(pushNotification: pushNotification, userKnown: true).0, pushUser!.userID
                )
            } else {
                let content = getPushNotificationText(pushNotification: pushNotification, userKnown: false)
                return (
                    content.1, content.0, 1
                )
            }

        } else {
            NSLog("Failed to decrypt message or pushKind is nil")
        }
        return nil
    } catch {
        NSLog("Error decoding JSON: \(error)")
        return nil
    }
}

func isUUIDNewer(_ uuid1: String, _ uuid2: String) -> Bool {
    guard uuid1.count >= 8, uuid2.count >= 8 else { return true }
    let hex1 = String(uuid1.prefix(8))
    let hex2 = String(uuid2.prefix(8))
    guard let timestamp1 = UInt32(hex1, radix: 16),
        let timestamp2 = UInt32(hex2, radix: 16)
    else { return true }
    return timestamp1 > timestamp2
}

func tryDecryptMessage(key: Data, pushData: EncryptedPushNotification) -> PushNotification? {

    do {
        // Create a nonce for ChaChaPoly
        let nonce = try ChaChaPoly.Nonce(data: pushData.nonce)

        // Create a sealed box for ChaChaPoly
        let sealedBox = try ChaChaPoly.SealedBox(
            nonce: nonce,
            ciphertext: pushData.ciphertext,
            tag: pushData.mac
        )

        // Decrypt the data using the key
        let decryptedData = try ChaChaPoly.open(sealedBox, using: SymmetricKey(data: key))

        // Here you can determine the PushKind based on the decrypted message
        return try PushNotification(serializedBytes: decryptedData)
    } catch {
        NSLog("Decryption failed: \(error)")
    }

    return nil
}

func getPushUsers() -> [PushUser]? {
    // Retrieve the data from secure storage (Keychain)
    guard let pushUsersB64 = readFromKeychain(key: "push_keys_receiving") else {
        NSLog("No data found for key: push_keys_receiving")
        return nil
    }
    guard let pushUsersBytes = Data(base64Encoded: pushUsersB64) else {
        NSLog("Failed to decode base64 push users")
        return nil
    }

    do {
        let pushUsers = try PushUsers(serializedBytes: pushUsersBytes)
        return pushUsers.users
    } catch {
        NSLog("Error decoding JSON: \(error)")
        return nil
    }
}

// Helper function to read from Keychain
func readFromKeychain(key: String) -> String? {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: key,
        kSecAttrService as String: "flutter_secure_storage_service",
        kSecReturnData as String: kCFBooleanTrue!,
        kSecMatchLimit as String: kSecMatchLimitOne,
        kSecAttrAccessGroup as String: "CN332ZUGRP.eu.twonly.shared",  // Use your access group
    ]

    var dataTypeRef: AnyObject? = nil
    let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)

    if status == errSecSuccess {
        if let data = dataTypeRef as? Data {
            return String(data: data, encoding: .utf8)
        }
    }

    return nil
}

// Helper function to write to Keychain
func writeToKeychain(key: String, value: String) {
    guard let data = value.data(using: .utf8) else {
        NSLog("Failed to convert value to data for keychain key: \(key)")
        return
    }

    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: key,
        kSecAttrService as String: "flutter_secure_storage_service",
        kSecAttrAccessGroup as String: "CN332ZUGRP.eu.twonly.shared"
    ]

    // Delete existing item first to ensure a clean overwrite
    SecItemDelete(query as CFDictionary)

    // Add the new item with background-compatible accessibility
    var addQuery = query
    addQuery[kSecValueData as String] = data
    addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock

    let status = SecItemAdd(addQuery as CFDictionary, nil)
    if status != errSecSuccess {
        NSLog("Failed to write keychain item for key \(key): \(status)")
    } else {
        NSLog("Successfully wrote keychain item for key: \(key)")
    }
}

func getPushNotificationText(pushNotification: PushNotification, userKnown: Bool) -> (String, String) {
    let systemLanguage = Locale.current.language.languageCode?.identifier ?? "en"  // Get the current system language

    var pushNotificationText: [PushKind: String] = [:]
    var title = "You"
    var noTranslationFoundTitle = "You have a new message."
    var noTranslationFoundBody = "Open twonly to learn more."

    // Define the messages based on the system language
    if systemLanguage.contains("de") {  // German
        title = "Du"
        noTranslationFoundTitle = "Du hast eine neue Nachricht."
        noTranslationFoundBody = "Öffne twonly um mehr zu erfahren."
        if (userKnown) {
            pushNotificationText = [
                .text: "hat eine Nachricht{inGroup} gesendet.",
                .twonly: "hat ein twonly{inGroup} gesendet.",
                .video: "hat ein Video{inGroup} gesendet.",
                .image: "hat ein Bild{inGroup} gesendet.",
                .audio: "hat eine Sprachnachricht{inGroup} gesendet.",
                .contactRequest: "möchte sich mit dir vernetzen.",
                .acceptRequest: "ist jetzt mit dir vernetzt.",
                .storedMediaFile: "hat dein Bild gespeichert.",
                .reaction: "hat auf dein Bild reagiert.",
                .testNotification: "Das ist eine Testbenachrichtigung.",
                .reopenedMedia: "hat dein Bild erneut geöffnet.",
                .reactionToVideo: "hat mit {{content}} auf dein Video reagiert.",
                .reactionToText: "hat mit {{content}} auf deinen Text reagiert.",
                .reactionToImage: "hat mit {{content}} auf dein Bild reagiert.",
                .reactionToAudio: "hat mit {{content}} auf deine Sprachnachricht reagiert.",
                .response: "hat dir{inGroup} geantwortet.",
                .addedToGroup: "hat dich zu \"{{content}}\" hinzugefügt.",
            ]
        } else {
            pushNotificationText = [
                .contactRequest: "hast eine neue Kontaktanfrage erhalten.",
            ]
        }
    } else { 
        if (userKnown) {
            pushNotificationText = [
                .text: "sent a message{inGroup}.",
                .twonly: "sent a twonly{inGroup}.",
                .video: "sent a video{inGroup}.",
                .image: "sent an image{inGroup}.",
                .audio: "sent a voice message{inGroup}.",
                .contactRequest: "wants to connect with you.",
                .acceptRequest: "is now connected with you.",
                .storedMediaFile: "has stored your image.",
                .reaction: "has reacted to your image.",
                .testNotification: "This is a test notification.",
                .reopenedMedia: "has reopened your image.",
                .reactionToVideo: "has reacted with {{content}} to your video.",
                .reactionToText: "has reacted with {{content}} to your text.",
                .reactionToImage: "has reacted with {{content}} to your image.",
                .reactionToAudio: "has reacted with {{content}} to your voice message.",
                .response: "has responded{inGroup}.",
                .addedToGroup: "has added you to \"{{content}}\"",
            ]
        } else {
            pushNotificationText = [
                .contactRequest: "have received a new contact request.",
            ]
        }
    }

    var content = pushNotificationText[pushNotification.kind] ?? ""
    if (content == "") {
        title = noTranslationFoundTitle
        content = noTranslationFoundBody
    }

    if pushNotification.hasAdditionalContent {
        content.replace("{{content}}", with: pushNotification.additionalContent)
        content.replace("{inGroup}", with: " in {inGroup}")
        content.replace("{inGroup}", with: pushNotification.additionalContent)
    } else {
        content.replace("{inGroup}", with: "")
    }

    // Return the corresponding message or an empty string if not found
    return (content, title)
}
