import AppKit
import Foundation
import Observation
import SwiftUI

/// Unified store + window owner for node/device pairing approvals. Both
/// prompters feed request cards here; one floating panel renders them all so
/// simultaneous requests never stack serial dialogs.
@MainActor
@Observable
final class PairingApprovalCenter {
    static let shared = PairingApprovalCenter()

    enum Kind: String, CaseIterable {
        case node
        case device
    }

    enum Decision {
        case approve
        case reject
    }

    struct Card: Identifiable, Equatable {
        let kind: Kind
        let requestId: String
        /// nodeId or deviceId.
        let subjectId: String
        let displayName: String?
        let platform: String?
        let deviceFamily: String?
        let modelIdentifier: String?
        let version: String?
        let coreVersion: String?
        let remoteIp: String?
        /// Device pairing operator role.
        let role: String?
        /// Device pairing operator scopes.
        let scopes: [String]
        /// Node capability surface.
        let caps: [String]
        /// Node command surface (drives the elevated-access warning).
        let commands: [String]
        let isRepair: Bool
        /// nil until a fresh gateway list confirms pairing history; a positive
        /// trust claim must never come from a stale snapshot.
        let previouslyPaired: Bool?
        let requestedAt: Date

        var id: String {
            self.requestId
        }
    }

    typealias DecisionHandler = @MainActor (Card, Decision) async -> Void

    private(set) var cards: [Card] = []
    private(set) var decisionsInFlight: Set<String> = []
    private var handlersByKind: [Kind: DecisionHandler] = [:]
    private var panel: PairingApprovalPanelController?
    /// Request ids visible when the user chose "Not Now"; the panel reopens
    /// automatically only when a request they have not seen yet arrives.
    private var snoozedRequestIds: Set<String> = []
    /// Request ids the visible panel has already presented. Periodic queue
    /// syncs must not re-activate the app; only genuinely new requests may.
    private var presentedRequestIds: Set<String> = []

    func register(kind: Kind, handler: @escaping DecisionHandler) {
        self.handlersByKind[kind] = handler
    }

    func unregister(kind: Kind) {
        self.handlersByKind[kind] = nil
        self.sync(kind: kind, cards: [])
    }

    /// Replace all cards of one kind (the owning prompter's queue is the
    /// source of truth). Cards stay ordered oldest-first across both kinds.
    func sync(kind: Kind, cards: [Card]) {
        var others = self.cards.filter { $0.kind != kind }
        #if DEBUG
        others += self.cards.filter { $0.kind == kind && self.demoRequestIds.contains($0.requestId) }
        #endif
        // requestId tiebreaker: Swift sort is not stable and equal timestamps
        // would let cards swap positions between syncs.
        self.cards = (others + cards).sorted {
            ($0.requestedAt, $0.requestId) < ($1.requestedAt, $1.requestId)
        }
        self.snoozedRequestIds.formIntersection(self.cards.map(\.id))
        self.updatePanel()
    }

    func decide(_ card: Card, _ decision: Decision) {
        #if DEBUG
        if self.demoRequestIds.contains(card.requestId) {
            self.demoRequestIds.remove(card.requestId)
            self.cards.removeAll { $0.requestId == card.requestId }
            self.updatePanel()
            return
        }
        #endif
        guard let handler = self.handlersByKind[card.kind] else { return }
        guard self.decisionsInFlight.insert(card.requestId).inserted else { return }
        Task { @MainActor in
            await handler(card, decision)
            self.decisionsInFlight.remove(card.requestId)
        }
    }

    /// Resolve a batch of cards with one decision. Takes the caller's
    /// rendered snapshot instead of reading the live queue: a request that
    /// arrives between render and click must never be resolved unseen.
    /// `decide`'s in-flight guard keeps repeated clicks idempotent.
    func decideAll(_ cards: [Card], _ decision: Decision) {
        for card in cards {
            self.decide(card, decision)
        }
    }

    /// "Not Now": hide the panel without resolving anything. Requests stay
    /// pending on the gateway (TTL applies) and in the menu-bar count.
    func snooze() {
        self.snoozedRequestIds = Set(self.cards.map(\.id))
        self.panel?.hide()
    }

    func showPanel() {
        guard !self.cards.isEmpty else { return }
        self.snoozedRequestIds.removeAll()
        self.presentedRequestIds.formUnion(self.cards.map(\.id))
        self.ensurePanel().show()
    }

    static func shouldAutoPresent(cardIds: [String], snoozedIds: Set<String>) -> Bool {
        !cardIds.isEmpty && !cardIds.allSatisfy { snoozedIds.contains($0) }
    }

    private func updatePanel() {
        if self.cards.isEmpty {
            self.snoozedRequestIds.removeAll()
            self.presentedRequestIds.removeAll()
            self.panel?.hide()
            return
        }
        let cardIds = self.cards.map(\.id)
        guard Self.shouldAutoPresent(cardIds: cardIds, snoozedIds: self.snoozedRequestIds) else {
            self.panel?.refreshLayout()
            return
        }
        // Activate (app focus + key panel) only for the first presentation or
        // a request not shown yet; periodic reconcile syncs merely refresh, so
        // the panel cannot steal focus every poll interval.
        let hasNewRequests = !cardIds.allSatisfy { self.presentedRequestIds.contains($0) }
        self.snoozedRequestIds.removeAll()
        self.presentedRequestIds.formUnion(cardIds)
        let panel = self.ensurePanel()
        if panel.isPanelVisible, !hasNewRequests {
            panel.refreshLayout()
        } else {
            panel.show()
        }
    }

    private func ensurePanel() -> PairingApprovalPanelController {
        if let panel = self.panel {
            return panel
        }
        let panel = PairingApprovalPanelController(center: self)
        self.panel = panel
        return panel
    }

    #if DEBUG
    /// Test seam: seed the live queue without presenting the panel (tests
    /// run without an NSApplication, so `updatePanel` must not fire).
    func _testSetCards(_ cards: [Card]) {
        self.cards = cards
    }

    /// Demo/screenshot hook: decisions on injected cards resolve locally
    /// instead of routing to a prompter.
    private var demoRequestIds: Set<String> = []

    func injectDemoCards(_ cards: [Card]) {
        self.demoRequestIds.formUnion(cards.map(\.id))
        let existing = Set(self.cards.map(\.id))
        self.cards += cards.filter { !existing.contains($0.id) }
        self.snoozedRequestIds.removeAll()
        self.updatePanel()
    }
    #endif
}

/// Floating panel with native window chrome hosting the SwiftUI approval UI,
/// so the prompt reads like a standard system dialog.
@MainActor
final class PairingApprovalPanelController {
    private let center: PairingApprovalCenter
    private var panel: NSPanel?
    private var hostingView: NSHostingView<PairingApprovalPanelView>?

    static let panelWidth: CGFloat = 460

    init(center: PairingApprovalCenter) {
        self.center = center
    }

    var isPanelVisible: Bool {
        self.panel?.isVisible == true
    }

    func show() {
        let panel = self.ensurePanel()
        self.applyFittingFrame()
        // Approval is a security decision: bring the app forward like the old
        // NSAlert flow so the prompt is keyboard-actionable immediately.
        NSApp.activate(ignoringOtherApps: true)
        panel.makeKeyAndOrderFront(nil)
        // No initial focus ring (matches NSAlert); Tab starts keyboard
        // navigation, Return approves a single request, Esc snoozes.
        panel.makeFirstResponder(nil)
        self.refreshLayout()
    }

    func hide() {
        self.panel?.orderOut(nil)
    }

    func refreshLayout() {
        // The root view stays installed (it observes the center directly);
        // replacing it would reset SwiftUI focus. Re-measure on the next tick
        // so pending Observation-driven updates are applied first.
        DispatchQueue.main.async { [weak self] in
            self?.applyFittingFrame()
        }
    }

    private func applyFittingFrame() {
        guard let panel = self.panel, let host = self.hostingView else { return }
        var size = host.fittingSize
        // Request metadata sizes the panel; clamp to the screen so the action
        // buttons stay reachable (the card list scrolls inside).
        if let screen = panel.screen ?? NSScreen.main {
            size.height = min(size.height, screen.visibleFrame.height * 0.85)
        }
        let target = self.centeredFrame(for: size)
        if target == panel.frame {
            return
        }
        if panel.isVisible {
            panel.setFrame(target, display: true, animate: true)
        } else {
            panel.setFrame(target, display: true)
        }
    }

    private func ensurePanel() -> NSPanel {
        if let panel = self.panel {
            return panel
        }
        // Titled so the system draws normal dialog chrome (opaque background,
        // rounded corners, shadow, key-window focus); the title bar itself is
        // invisible and buttonless so it reads as an alert, not a document.
        let panel = NSPanel(
            contentRect: NSRect(x: 0, y: 0, width: Self.panelWidth, height: 200),
            styleMask: [.titled, .fullSizeContentView],
            backing: .buffered,
            defer: false)
        panel.titleVisibility = .hidden
        panel.titlebarAppearsTransparent = true
        for buttonType in [NSWindow.ButtonType.closeButton, .miniaturizeButton, .zoomButton] {
            panel.standardWindowButton(buttonType)?.isHidden = true
        }
        panel.level = .floating
        panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
        panel.hidesOnDeactivate = false
        panel.isMovableByWindowBackground = true
        panel.isReleasedWhenClosed = false

        let host = NSHostingView(rootView: PairingApprovalPanelView(center: self.center))
        panel.contentView = host
        self.hostingView = host
        self.panel = panel
        return panel
    }

    private func centeredFrame(for size: NSSize) -> NSRect {
        guard let screen = self.panel?.screen ?? NSScreen.main else {
            return NSRect(origin: .zero, size: size)
        }
        let bounds = screen.visibleFrame
        // Slightly above center, where system alerts sit.
        let x = bounds.midX - size.width / 2
        let y = bounds.midY - size.height / 2 + bounds.height * 0.08
        return NSRect(x: x, y: y, width: size.width, height: size.height)
    }
}
