import Foundation
import UserNotifications
import UIKit
import NucleicProtocol

/// The phone's notification pipeline (UX_IOS §5.1): local notifications for approvals and
/// needs-input transitions while the app is backgrounded, actionable Allow/Deny for low-risk
/// approvals, deep-link routing on tap, and the app-icon badge (= NEEDS YOU count).
///
/// Two arrival paths converge here:
/// - **Local** (LAN): the app is backgrounded but its socket is briefly alive; `RemoteStore`
///   posts a rich local notification (content composed on-device — nothing rides APNs).
/// - **Remote** (relay, M5): a content-free `approval.pending` tickle wakes the phone; the
///   alert text comes from Localizable.strings and the app pulls the real approval over the
///   encrypted channel on open.
@MainActor
final class NotificationRouter: NSObject {
    static let shared = NotificationRouter()

    /// The store notifications act on; set once at app start.
    weak var store: RemoteStore?

    /// The delivered identifier of the remote wake tickle ("A session is waiting for your
    /// approval"). A remote notification's identifier equals its `apns-collapse-id`, so this must
    /// match the relay's `APPROVAL_COLLAPSE_ID` (cloud/nucleic-edge/src/apns.ts) for the recall to
    /// target the right notification.
    static let approvalTickleIdentifier = "approval-pending"

    // Category / action identifiers (also referenced from the notification service side).
    static let approvalCategory = "NUCLEIC_APPROVAL"
    static let approvalActionableCategory = "NUCLEIC_APPROVAL_ACTIONABLE"
    static let inputCategory = "NUCLEIC_INPUT"
    static let allowAction = "NUCLEIC_ALLOW"
    static let denyAction = "NUCLEIC_DENY"

    /// Install the delegate + categories. Call once at launch (before any notification can
    /// arrive, so actions are always registered).
    func install(store: RemoteStore) {
        self.store = store
        let center = UNUserNotificationCenter.current()
        center.delegate = self

        // Low/medium-risk approvals resolve straight from the banner (UX_IOS §5.1);
        // Allow requires device auth so a pocket-tap can't grant. High-risk approvals use
        // the action-less category — they must open the app and be answered on the card.
        let allow = UNNotificationAction(
            identifier: Self.allowAction, title: "Allow",
            options: [.authenticationRequired])
        let deny = UNNotificationAction(
            identifier: Self.denyAction, title: "Deny",
            options: [.destructive])
        let actionable = UNNotificationCategory(
            identifier: Self.approvalActionableCategory,
            actions: [deny, allow], intentIdentifiers: [])
        let plain = UNNotificationCategory(
            identifier: Self.approvalCategory, actions: [], intentIdentifiers: [])
        let input = UNNotificationCategory(
            identifier: Self.inputCategory, actions: [], intentIdentifiers: [])
        center.setNotificationCategories([actionable, plain, input])
    }

    // MARK: - Posting (local path, composed on-device)

    /// Post a local notification for a pending approval. Rich because it never leaves the
    /// device; the remote tickle stays content-free.
    func postApproval(_ approval: ApprovalRequest, sessionTitle: String) {
        let content = UNMutableNotificationContent()
        content.title = sessionTitle
        content.body = "\(approval.toolName) wants: \(approval.title)"
        content.sound = .default
        // AskUserQuestion collects answers, it doesn't gate — a banner Allow would reply with no
        // selection (updatedInput: nil) and the CLI reports the question went unanswered. So, like
        // a high-risk approval, it uses the action-less category: it must open the app and be
        // answered on the picker card.
        content.categoryIdentifier = approval.risk.isHigh || approval.toolName == AskUserQuestion.toolName
            ? Self.approvalCategory : Self.approvalActionableCategory
        content.userInfo = [
            "sessionID": approval.sessionID.rawValue,
            "approvalID": approval.id.rawValue,
        ]
        // One notification per approval; a re-post for the same id replaces, and resolution
        // (any device) withdraws it.
        let request = UNNotificationRequest(
            identifier: "approval-\(approval.id.rawValue)", content: content, trigger: nil)
        UNUserNotificationCenter.current().add(request)
    }

    /// Post a "session needs you / finished" transition notification.
    func postSessionUpdate(_ summary: WireSessionSummary) {
        let content = UNMutableNotificationContent()
        content.title = summary.title
        content.body = summary.status == .awaitingInput && summary.disposition == .completed
            ? "Finished its work." : "Waiting for your next prompt."
        content.sound = .default
        content.categoryIdentifier = Self.inputCategory
        content.userInfo = ["sessionID": summary.sessionID.rawValue]
        let request = UNNotificationRequest(
            identifier: "input-\(summary.sessionID.rawValue)", content: content, trigger: nil)
        UNUserNotificationCenter.current().add(request)
    }

    /// Withdraw an approval's notification once it's resolved (first-responder-wins — the
    /// Mac may have answered).
    func withdrawApproval(_ id: ApprovalID) {
        let identifier = "approval-\(id.rawValue)"
        let center = UNUserNotificationCenter.current()
        center.removeDeliveredNotifications(withIdentifiers: [identifier])
        center.removePendingNotificationRequests(withIdentifiers: [identifier])
    }

    /// Recall the generic remote wake tickle ("A session is waiting for your approval") once no
    /// approval is pending anywhere. This targets *only* the shared tickle (delivered under
    /// `approvalTickleIdentifier`, its APNs collapse-id) — the content-free push a phone gets while
    /// away. Per-approval notifications are each recalled by `withdrawApproval(_:)` as they resolve,
    /// so a still-pending approval's own banner is never swept away by this.
    func withdrawApprovalAttention() {
        let center = UNUserNotificationCenter.current()
        center.removeDeliveredNotifications(withIdentifiers: [Self.approvalTickleIdentifier])
        center.removePendingNotificationRequests(withIdentifiers: [Self.approvalTickleIdentifier])
    }

    /// Keep the app-icon badge equal to the NEEDS YOU count (UX_IOS §8).
    func updateBadge(_ count: Int) {
        UNUserNotificationCenter.current().setBadgeCount(count)
    }
}

extension NotificationRouter: UNUserNotificationCenterDelegate {
    /// Foreground arrivals: show the banner unless the user is already looking at that
    /// session (the approval card is louder than a banner).
    nonisolated func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        willPresent notification: UNNotification
    ) async -> UNNotificationPresentationOptions {
        let sessionID = notification.request.content.userInfo["sessionID"] as? String
        let suppress = await MainActor.run {
            sessionID != nil && store?.openSessionID?.rawValue == sessionID
        }
        return suppress ? [] : [.banner, .sound, .badge]
    }

    /// Taps and actions. A tap routes to the session; Allow/Deny resolve the approval over
    /// a live channel (reconnecting first if the socket dropped).
    nonisolated func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse
    ) async {
        let info = response.notification.request.content.userInfo
        let sessionID = (info["sessionID"] as? String).map(SessionID.init(rawValue:))
        let approvalID = (info["approvalID"] as? String).map(ApprovalID.init(rawValue:))
        let action = response.actionIdentifier

        await MainActor.run { [weak self] in
            guard let store = self?.store else { return }
            switch action {
            case Self.allowAction:
                if let approvalID { store.respondFromNotification(approvalID, allow: true) }
            case Self.denyAction:
                if let approvalID { store.respondFromNotification(approvalID, allow: false) }
            default:
                // Plain tap (or a long-press open): route to the session.
                if let sessionID { store.route(to: sessionID) }
            }
        }
    }
}
