import Foundation
import UserNotifications
import AppKit
import SecretKit
import SecretAgentKit
import Brief

final class Notifier: Sendable {

    private let notificationDelegate = NotificationDelegate()

    init() {
        let updateAction = UNNotificationAction(identifier: Constants.updateActionIdentitifier, title: String(localized: .updateNotificationUpdateButton), options: [])
        let ignoreAction = UNNotificationAction(identifier: Constants.ignoreActionIdentitifier, title: String(localized: .updateNotificationIgnoreButton), options: [])
        let updateCategory = UNNotificationCategory(identifier: Constants.updateCategoryIdentitifier, actions: [updateAction, ignoreAction], intentIdentifiers: [], options: [])
        let criticalUpdateCategory = UNNotificationCategory(identifier: Constants.criticalUpdateCategoryIdentitifier, actions: [updateAction], intentIdentifiers: [], options: [])

        let rawDurations = [
            Measurement(value: 1, unit: UnitDuration.minutes),
            Measurement(value: 5, unit: UnitDuration.minutes),
            Measurement(value: 1, unit: UnitDuration.hours),
            Measurement(value: 24, unit: UnitDuration.hours)
        ]

        let doNotPersistAction = UNNotificationAction(identifier: Constants.doNotPersistActionIdentitifier, title: String(localized: .persistAuthenticationDeclineButton), options: [])
        var allPersistenceActions = [doNotPersistAction]

        let formatter = DateComponentsFormatter()
        formatter.unitsStyle = .spellOut
        formatter.allowedUnits = [.hour, .minute, .day]

        var identifiers: [String: TimeInterval] = [:]
        for duration in rawDurations {
            let seconds = duration.converted(to: .seconds).value
            guard let string = formatter.string(from: seconds)?.capitalized else { continue }
            let identifier = Constants.persistAuthenticationCategoryIdentitifier.appending("\(seconds)")
            let action = UNNotificationAction(identifier: identifier, title: string, options: [])
            identifiers[identifier] = seconds
            allPersistenceActions.append(action)
        }

        let persistAuthenticationCategory = UNNotificationCategory(identifier: Constants.persistAuthenticationCategoryIdentitifier, actions: allPersistenceActions, intentIdentifiers: [], options: [])
        if persistAuthenticationCategory.responds(to: Selector(("actionsMenuTitle"))) {
            persistAuthenticationCategory.setValue(String(localized: .persistAuthenticationAcceptButton), forKey: "_actionsMenuTitle")
        }
        UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory, persistAuthenticationCategory])
        UNUserNotificationCenter.current().delegate = notificationDelegate

        Task {
            await notificationDelegate.state.setPersistenceState(options: identifiers) { secret, store, duration in
                guard let duration = duration else { return }
                try? await store.persistAuthentication(secret: secret, forDuration: duration)
            }
        }

    }

    func prompt() {
        let notificationCenter = UNUserNotificationCenter.current()
        notificationCenter.requestAuthorization(options: .alert) { _, _ in }
    }

    func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async {
        await notificationDelegate.state.setPending(secret: secret, store: store)
        let notificationCenter = UNUserNotificationCenter.current()
        let notificationContent = UNMutableNotificationContent()
        notificationContent.title = String(localized: .signedNotificationTitle(appName: provenance.origin.displayName))
        notificationContent.subtitle = String(localized: .signedNotificationDescription(secretName: secret.name))
        notificationContent.userInfo[Constants.persistSecretIDKey] = secret.id.description
        notificationContent.userInfo[Constants.persistStoreIDKey] = store.id.description
        notificationContent.interruptionLevel = .timeSensitive
        if await store.existingPersistedAuthenticationContext(secret: secret) == nil && secret.authenticationRequirement.required {
            notificationContent.categoryIdentifier = Constants.persistAuthenticationCategoryIdentitifier
        }
        if let iconURL = provenance.origin.iconURL, let attachment = try? UNNotificationAttachment(identifier: "icon", url: iconURL, options: nil) {
            notificationContent.attachments = [attachment]
        }
        let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: nil)
        try? await notificationCenter.add(request)
    }

    func notify(update: Release, ignore: (@Sendable (Release) async -> Void)?) async {
        await notificationDelegate.state.prepareForNotification(release: update, ignoreAction: ignore)
        let notificationCenter = UNUserNotificationCenter.current()
        let notificationContent = UNMutableNotificationContent()
        if update.critical {
            notificationContent.interruptionLevel = .critical
            notificationContent.title = String(localized: .updateNotificationUpdateCriticalTitle(updateName: update.name))
        } else {
            notificationContent.title = String(localized: .updateNotificationUpdateNormalTitle(updateName: update.name))
        }
        notificationContent.subtitle = String(localized: .updateNotificationUpdateDescription)
        notificationContent.body = update.body
        notificationContent.categoryIdentifier = update.critical ? Constants.criticalUpdateCategoryIdentitifier : Constants.updateCategoryIdentitifier
        let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: nil)
        try? await notificationCenter.add(request)
    }

}

extension Notifier: SigningWitness {

    func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws {
    }

    func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws {
        await notify(accessTo: secret, from: store, by: provenance)
    }

}

extension Notifier {

    enum Constants {

        // Update notifications
        static let updateCategoryIdentitifier  = "com.maxgoedjen.Secretive.SecretAgent.update"
        static let criticalUpdateCategoryIdentitifier  = "com.maxgoedjen.Secretive.SecretAgent.update.critical"
        static let updateActionIdentitifier  = "com.maxgoedjen.Secretive.SecretAgent.update.updateaction"
        static let ignoreActionIdentitifier  = "com.maxgoedjen.Secretive.SecretAgent.update.ignoreaction"

        // Authorization persistence notificatoins
        static let persistAuthenticationCategoryIdentitifier  = "com.maxgoedjen.Secretive.SecretAgent.persistauthentication"
        static let doNotPersistActionIdentitifier  = "com.maxgoedjen.Secretive.SecretAgent.persistauthentication.donotpersist"
        static let persistForActionIdentitifierPrefix  = "com.maxgoedjen.Secretive.SecretAgent.persistauthentication.persist."

        static let persistSecretIDKey  = "com.maxgoedjen.Secretive.SecretAgent.persistauthentication.secretidkey"
        static let persistStoreIDKey  = "com.maxgoedjen.Secretive.SecretAgent.persistauthentication.storeidkey"
    }

}

final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Sendable {

    fileprivate actor State {
        typealias PersistAction = (@Sendable (AnySecret, AnySecretStore, TimeInterval?) async -> Void)
        typealias IgnoreAction = (@Sendable (Release) async -> Void)
        fileprivate var release: Release?
        fileprivate var ignoreAction: IgnoreAction?
        fileprivate var persistAction: PersistAction?
        fileprivate var persistOptions: [String: TimeInterval] = [:]
        fileprivate var pendingPersistableStores: [String: AnySecretStore] = [:]
        fileprivate var pendingPersistableSecrets: [String: AnySecret] = [:]

        func setPending(secret: AnySecret, store: AnySecretStore) {
            pendingPersistableSecrets[secret.id.description] = secret
            pendingPersistableStores[store.id.description] = store
        }

        func retrievePending(secretID: String, storeID: String, optionID: String) -> (AnySecret, AnySecretStore, TimeInterval)? {
            guard let secret = pendingPersistableSecrets[secretID],
                  let store = pendingPersistableStores[storeID],
                  let options = persistOptions[optionID] else {
                return nil
            }
            pendingPersistableSecrets.removeValue(forKey: secretID)
            return (secret, store, options)
        }

        func setPersistenceState(options: [String: TimeInterval], action: @escaping PersistAction) {
            self.persistOptions = options
            self.persistAction = action
        }

        func prepareForNotification(release: Release, ignoreAction: IgnoreAction?) {
            self.release = release
            self.ignoreAction = ignoreAction
        }

        
    }

    fileprivate let state = State()

    func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) {

    }

    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
        let category = response.notification.request.content.categoryIdentifier
        switch category {
        case Notifier.Constants.updateCategoryIdentitifier:
            await handleUpdateResponse(response: response)
        case Notifier.Constants.persistAuthenticationCategoryIdentitifier:
            await handlePersistAuthenticationResponse(response: response)
        default:
            break
        }
    }

    func handleUpdateResponse(response: UNNotificationResponse) async {
        let id = response.actionIdentifier
        guard let update = await state.release else { return }
        switch id {
        case Notifier.Constants.updateActionIdentitifier, UNNotificationDefaultActionIdentifier:
            NSWorkspace.shared.open(update.html_url)
        case Notifier.Constants.ignoreActionIdentitifier:
            await state.ignoreAction?(update)
        default:
            fatalError()
        }
    }

    func handlePersistAuthenticationResponse(response: UNNotificationResponse) async {
        guard let secretID = response.notification.request.content.userInfo[Notifier.Constants.persistSecretIDKey] as? String,
              let storeID = response.notification.request.content.userInfo[Notifier.Constants.persistStoreIDKey] as? String else {
            return
        }
        let optionID = response.actionIdentifier
        guard let (secret, store, persistOptions) = await state.retrievePending(secretID: secretID, storeID: storeID, optionID: optionID) else { return }
        await state.persistAction?(secret, store, persistOptions)
    }


    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
        [.list, .banner]
    }

}

