import AppKit
import CodexBarCore

struct PendingProviderSwitcherRebuild {
    let menu: NSMenu
    let provider: UsageProvider?
}

/// Skips the event-queue peek on run-loop passes where no event of the monitored kinds
/// can possibly be pending. The menu-tracking run loop spins on every mouse move, and the
/// session-wide event counters for keys and clicks are far cheaper to read than
/// `NSApp.nextEvent` is to call, so gating on them removes the per-pass peek cost from
/// hover-heavy menu interaction (mouse moves never advance these counters).
@MainActor
final class ProviderSwitcherEventPeekGate {
    private let eventTypes: [CGEventType]
    private let counterProvider: (CGEventType) -> UInt32
    private var lastCounters: [UInt32]?
    private var heldKeyCodes: Set<UInt16> = []
    private var emptyPeekBudget = 0

    init(
        eventTypes: [CGEventType],
        counterProvider: @escaping (CGEventType) -> UInt32 = { type in
            CGEventSource.counterForEventType(.combinedSessionState, eventType: type)
        })
    {
        self.eventTypes = eventTypes
        self.counterProvider = counterProvider
    }

    /// True when an event of a monitored kind may have been posted since the last check.
    func shouldPeek() -> Bool {
        let counters = self.eventTypes.map(self.counterProvider)
        let countersChanged = self.lastCounters.map { counters != $0 } ?? true
        self.lastCounters = counters
        if countersChanged {
            // The observer runs before run-loop sources. WindowServer can advance a counter
            // one pass before AppKit queues the NSEvent, so require two empty peeks before
            // considering the queue caught up.
            self.emptyPeekBudget = max(self.emptyPeekBudget, 2)
        }
        // CoreGraphics does not count key autorepeat events. Keep peeking while a key is
        // held so repeated provider-navigation events are still handled.
        if !self.heldKeyCodes.isEmpty {
            return true
        }
        return self.emptyPeekBudget > 0
    }

    func observe(_ event: NSEvent) {
        // An unhandled event stays queued until AppKit processes it after this observer.
        // Keep peeking until a later pass proves the matching queue is empty.
        self.emptyPeekBudget = max(self.emptyPeekBudget, 1)
        switch event.type {
        case .keyDown:
            self.heldKeyCodes.insert(event.keyCode)
        case .keyUp:
            self.heldKeyCodes.remove(event.keyCode)
        default:
            break
        }
    }

    func observeQueueEmpty(afterFindingEvent: Bool) {
        if afterFindingEvent {
            // A counter snapshot can represent multiple events that AppKit delivers across
            // run-loop passes. Keep one empty proof pending after draining available events.
            self.emptyPeekBudget = max(self.emptyPeekBudget - 1, 1)
        } else if self.emptyPeekBudget > 0 {
            self.emptyPeekBudget -= 1
        }
    }
}

/// Handles provider-switcher keyboard shortcuts and overview scrolling while the merged
/// status menu is open. `NSMenu` tracking pulls events itself, so local event monitors,
/// Carbon dispatcher handlers, registered hot keys (tracking pushes a hotkey-disable mode),
/// and `menuHasKeyEquivalent` never see these events — peeking the queue from a run-loop
/// observer is the only delivery path.
///
/// The peek itself must not disturb the tracking session: `NSApp.nextEvent` re-enters the
/// event loop in the mode it is given, and re-entering `.eventTracking` dispatches the menu
/// session's own timers and sources mid-observer. When that landed during menu setup or amid
/// rapid claimed key repeats, it killed the session and left a zombie menu on screen that no
/// longer dequeued events: clicks sat in the queue for tens of seconds while the cursor
/// beach-balled. Three guards prevent that: peeks run in a private run-loop mode with no
/// sources or timers registered (the queue is mode-agnostic, so matching still works), the
/// peek only starts once the tracking loop is confirmed pumping, and mouse clicks are not
/// monitored at all (`ProviderSwitcherView` handles those via its own `mouseDown`/`mouseUp`
/// overrides), so the monitor never dequeues a click meant for AppKit.
@MainActor
final class ProviderSwitcherShortcutEventMonitor {
    private let callback: @MainActor (NSEvent) -> Bool
    private let observer: CFRunLoopObserver
    private let trackingState = ProviderSwitcherMenuTrackingState()
    private var isActive = false

    /// A run-loop mode nothing else registers sources or timers in, so running the loop in
    /// this mode while polling the event queue cannot dispatch menu-session work re-entrantly.
    private static let peekMode = RunLoop.Mode("com.steipete.codexbar.switcher-peek")

    init(
        events: NSEvent.EventTypeMask,
        peekGate: ProviderSwitcherEventPeekGate = ProviderSwitcherEventPeekGate(
            eventTypes: [.keyDown, .keyUp, .scrollWheel]),
        callback: @escaping @MainActor (NSEvent) -> Bool)
    {
        self.callback = callback
        let trackingState = self.trackingState

        self.observer = CFRunLoopObserverCreateWithHandler(
            nil,
            CFRunLoopActivity.beforeSources.rawValue,
            true,
            0)
        { [events, peekGate, callback, trackingState] _, _ in
            MainActor.assumeIsolated {
                guard trackingState.isTrackingActive else { return }
                guard peekGate.shouldPeek() else { return }
                var foundEvent = false
                var blockedByUnhandledEvent = false
                while let event = NSApp.nextEvent(
                    matching: events,
                    until: .distantPast,
                    inMode: Self.peekMode,
                    dequeue: false)
                {
                    foundEvent = true
                    peekGate.observe(event)
                    guard callback(event) else {
                        blockedByUnhandledEvent = true
                        break
                    }
                    _ = NSApp.nextEvent(
                        matching: events,
                        until: .distantPast,
                        inMode: Self.peekMode,
                        dequeue: true)
                }
                if !blockedByUnhandledEvent {
                    peekGate.observeQueueEmpty(afterFindingEvent: foundEvent)
                }
            }
        }
    }

    deinit {
        MainActor.assumeIsolated {
            self.stop()
        }
    }

    func start() {
        guard !self.isActive else { return }
        CFRunLoopAddObserver(
            RunLoop.main.getCFRunLoop(),
            self.observer,
            CFRunLoopMode(RunLoop.Mode.eventTracking.rawValue as CFString))
        self.isActive = true
        // The menus this monitors are shown via `popUpMenuPositioningItem`, which posts no
        // NSMenu tracking notifications. Arm the gate from a block queued in the tracking
        // run-loop mode instead: it can only execute once the menu's tracking session is alive
        // and pumping the run loop, which keeps peeks away from menu setup.
        let trackingState = self.trackingState
        RunLoop.main.perform(inModes: [.eventTracking]) {
            MainActor.assumeIsolated {
                trackingState.isTrackingActive = true
            }
        }
        CFRunLoopWakeUp(CFRunLoopGetMain())
    }

    func stop() {
        self.trackingState.isTrackingActive = false
        guard self.isActive else { return }
        CFRunLoopRemoveObserver(
            RunLoop.main.getCFRunLoop(),
            self.observer,
            CFRunLoopMode(RunLoop.Mode.eventTracking.rawValue as CFString))
        self.isActive = false
    }
}

/// Tracks whether an `NSMenu` tracking session is currently alive, so the shortcut monitor
/// only touches the event queue while AppKit is actually pumping it.
@MainActor
private final class ProviderSwitcherMenuTrackingState {
    var isTrackingActive = false
}

@MainActor
private final class ProviderSwitcherTrackingRunLoopOperation {
    private var operation: (@MainActor () -> Void)?

    init(operation: @escaping @MainActor () -> Void) {
        self.operation = operation
    }

    func run() {
        guard let operation = self.operation else { return }
        self.operation = nil
        operation()
    }
}

@MainActor
enum ProviderSwitcherTrackingRunLoopScheduler {
    static func schedule(_ operation: @escaping @MainActor () -> Void) {
        let pending = ProviderSwitcherTrackingRunLoopOperation(operation: operation)
        let runLoop = CFRunLoopGetMain()
        // Main-actor tasks can starve while AppKit owns the modal menu loop. Queue in both modes so the
        // rebuild runs during tracking, with the default mode as a fallback if tracking ends first.
        let modes = [
            RunLoop.Mode.eventTracking.rawValue,
            RunLoop.Mode.default.rawValue,
        ]
        for mode in modes {
            CFRunLoopPerformBlock(runLoop, mode as CFString) {
                MainActor.assumeIsolated {
                    pending.run()
                }
            }
        }
        CFRunLoopWakeUp(runLoop)
    }
}

extension StatusItemController {
    func installProviderSwitcherShortcutMonitorIfNeeded(for menu: NSMenu) {
        guard self.isMenuRefreshEnabled else {
            return
        }
        let hasProviderSwitcher = self.shouldMergeIcons && menu.items.first?.view is ProviderSwitcherView
        let hasPersistentRefresh = menu.items.contains { self.isPersistentRefreshItem($0) }
        guard hasProviderSwitcher || hasPersistentRefresh else {
            return
        }

        self.removeProviderSwitcherShortcutMonitor()
        self.resetOverviewScrollAccumulation()
        // Every tracked menu observes wheel events so a manual scroll made after Refresh
        // invalidates that refresh's pending viewport restore. Unhandled wheel events remain
        // queued for AppKit's native menu scroller.
        let eventMask: NSEvent.EventTypeMask = [.keyDown, .keyUp, .scrollWheel]
        let monitor = ProviderSwitcherShortcutEventMonitor(
            events: eventMask)
        { [weak self, weak menu] event in
            guard let self,
                  let menu,
                  self.openMenus[ObjectIdentifier(menu)] != nil
            else {
                return false
            }

            return self.handleMenuTrackingShortcutEvent(event, menu: menu)
        }
        monitor.start()
        self.providerSwitcherShortcutEventMonitor = monitor
        self.providerSwitcherShortcutMenuID = ObjectIdentifier(menu)
    }

    func removeProviderSwitcherShortcutMonitor() {
        self.providerSwitcherShortcutEventMonitor?.stop()
        self.providerSwitcherShortcutEventMonitor = nil
        self.providerSwitcherShortcutMenuID = nil
        self.clearProviderSwitcherPointerInteraction()
    }

    @discardableResult
    func handleMenuTrackingShortcutEvent(_ event: NSEvent, menu: NSMenu) -> Bool {
        if event.type == .scrollWheel {
            self.advanceMenuInteraction(for: menu)
        }
        if StatusItemMenu.isPersistentRefreshShortcut(for: event),
           menu.items.contains(where: self.isPersistentRefreshItem)
        {
            if let menu = menu as? StatusItemMenu {
                menu.requestPersistentRefreshAction()
            } else {
                self.performPersistentRefreshAction(in: ObjectIdentifier(menu))
            }
            return true
        }
        guard menu.items.first?.view is ProviderSwitcherView else { return false }
        return self.handleProviderSwitcherTrackingEvent(event, menu: menu)
    }

    func providerSwitcherContentStartIndex(in menu: NSMenu) -> Int {
        menu.items.first?.view is ProviderSwitcherView ? 2 : 0
    }

    @discardableResult
    func handleProviderSwitcherShortcut(_ event: NSEvent, menu: NSMenu) -> Bool {
        if let index = StatusItemMenu.providerSelectionIndex(for: event) {
            return self.selectProviderSwitcherSegment(at: index, menu: menu)
        }
        if let direction = StatusItemMenu.providerNavigationDirection(for: event) {
            self.navigateProviderSwitcher(direction, menu: menu)
            return true
        }
        return false
    }

    @discardableResult
    func handleProviderSwitcherTrackingEvent(_ event: NSEvent, menu: NSMenu) -> Bool {
        switch event.type {
        case .keyDown:
            return self.handleProviderSwitcherShortcut(event, menu: menu)
        case .leftMouseDown:
            guard let switcher = menu.items.first?.view as? ProviderSwitcherView else { return false }
            self.beginProviderSwitcherPointerInteraction(in: menu)
            let handled = switcher.handleMenuTrackingMouseDown(event)
            if !handled {
                self.clearProviderSwitcherPointerInteraction(in: menu)
            }
            return handled
        case .leftMouseUp:
            guard self.providerSwitcherPointerInteractionMenuID == ObjectIdentifier(menu) else {
                return false
            }
            guard let switcher = menu.items.first?.view as? ProviderSwitcherView else {
                self.clearProviderSwitcherPointerInteraction(in: menu)
                return true
            }
            _ = switcher.handleMenuTrackingMouseUp(event)
            self.finishProviderSwitcherPointerInteraction(in: menu)
            return true
        case .scrollWheel:
            return self.handleOverviewScrollWheel(event, menu: menu)
        default:
            return false
        }
    }

    func requestProviderSwitcherMenuRebuild(_ menu: NSMenu, provider: UsageProvider?) {
        guard self.providerSwitcherPointerInteractionMenuID == ObjectIdentifier(menu) else {
            self.deferSwitcherMenuRebuildIfStillVisible(menu, provider: provider)
            return
        }
        self.pendingProviderSwitcherPointerRebuild = PendingProviderSwitcherRebuild(
            menu: menu,
            provider: provider)
    }

    private func beginProviderSwitcherPointerInteraction(in menu: NSMenu) {
        let menuID = ObjectIdentifier(menu)
        if self.providerSwitcherPointerInteractionMenuID != menuID {
            self.pendingProviderSwitcherPointerRebuild = nil
        }
        self.providerSwitcherPointerInteractionMenuID = menuID
    }

    private func finishProviderSwitcherPointerInteraction(in menu: NSMenu) {
        let menuID = ObjectIdentifier(menu)
        guard self.providerSwitcherPointerInteractionMenuID == menuID else { return }
        self.providerSwitcherPointerInteractionMenuID = nil
        guard let pending = self.pendingProviderSwitcherPointerRebuild,
              pending.menu === menu
        else {
            self.pendingProviderSwitcherPointerRebuild = nil
            return
        }
        self.pendingProviderSwitcherPointerRebuild = nil
        self.deferSwitcherMenuRebuildIfStillVisible(menu, provider: pending.provider)
    }

    private func clearProviderSwitcherPointerInteraction(in menu: NSMenu? = nil) {
        if let menu,
           self.providerSwitcherPointerInteractionMenuID != ObjectIdentifier(menu)
        {
            return
        }
        self.providerSwitcherPointerInteractionMenuID = nil
        self.pendingProviderSwitcherPointerRebuild = nil
    }

    @discardableResult
    private func selectProviderSwitcherSegment(at index: Int, menu: NSMenu) -> Bool {
        guard let switcherView = menu.items.first?.view as? ProviderSwitcherView,
              switcherView.handleKeyboardSelection(at: index)
        else {
            return false
        }
        return true
    }
}
