import AppKit
import Carbon.HIToolbox
import Combine

/// Manages global and local hotkeys.
///
/// The toggle hotkey uses Carbon's `RegisterEventHotKey` which works
/// system-wide without Accessibility permission. Notch-scoped hotkeys
/// use a local `NSEvent` monitor (only fires when our app is active).
@MainActor
class HotkeyManager {

    static let shared = HotkeyManager()

    // MARK: - Callbacks

    var onToggle: (() -> Void)?
    var onNewTab: (() -> Void)?
    var onCloseTab: (() -> Void)?
    var onNextTab: (() -> Void)?
    var onPreviousTab: (() -> Void)?
    var onNextWorkspace: (() -> Void)?
    var onPreviousWorkspace: (() -> Void)?
    var onDetachTab: (() -> Void)?
    var onApplySizePreset: ((TerminalSizePreset) -> Void)?
    var onSwitchToTab: ((Int) -> Void)?
    var onSwitchToWorkspace: ((WorkspaceID) -> Void)?

    /// Notch-scoped hotkeys only fire when the notch is open.
    var isNotchOpen: Bool = false

    private var hotKeyRef: EventHotKeyRef?
    private var eventHandlerRef: EventHandlerRef?
    private var localMonitor: Any?
    private let settingsProvider: TerminalSessionConfigurationProviding
    private let workspaceRegistry: WorkspaceRegistry
    private var settingsCancellable: AnyCancellable?

    init(
        settingsProvider: TerminalSessionConfigurationProviding? = nil,
        workspaceRegistry: WorkspaceRegistry? = nil
    ) {
        self.settingsProvider = settingsProvider ?? AppSettingsController.shared
        self.workspaceRegistry = workspaceRegistry ?? WorkspaceRegistry.shared
    }

    // MARK: - Resolved bindings from typed runtime settings

    private var toggleBinding: HotkeyBinding {
        settingsProvider.hotkeySettings.toggle
    }
    private var newTabBinding: HotkeyBinding {
        settingsProvider.hotkeySettings.newTab
    }
    private var closeTabBinding: HotkeyBinding {
        settingsProvider.hotkeySettings.closeTab
    }
    private var nextTabBinding: HotkeyBinding {
        settingsProvider.hotkeySettings.nextTab
    }
    private var prevTabBinding: HotkeyBinding {
        settingsProvider.hotkeySettings.previousTab
    }
    private var nextWorkspaceBinding: HotkeyBinding {
        settingsProvider.hotkeySettings.nextWorkspace
    }
    private var previousWorkspaceBinding: HotkeyBinding {
        settingsProvider.hotkeySettings.previousWorkspace
    }
    private var detachBinding: HotkeyBinding {
        settingsProvider.hotkeySettings.detachTab
    }
    private var sizePresets: [TerminalSizePreset] {
        settingsProvider.terminalSizePresets
    }

    // MARK: - Start / Stop

    func start() {
        installCarbonHandler()
        registerToggleHotkey()
        installLocalMonitor()
        observeToggleHotkeyChanges()
    }

    func stop() {
        unregisterToggleHotkey()
        removeCarbonHandler()
        removeLocalMonitor()
        settingsCancellable = nil
    }

    // MARK: - Carbon global hotkey (toggle)

    /// Installs a Carbon event handler that receives `kEventHotKeyPressed`
    /// events when a registered hotkey fires — works system-wide.
    private func installCarbonHandler() {
        var eventType = EventTypeSpec(
            eventClass: OSType(kEventClassKeyboard),
            eventKind: UInt32(kEventHotKeyPressed)
        )

        // Closure must not capture self — uses the singleton accessor instead.
        let status = InstallEventHandler(
            GetApplicationEventTarget(),
            { (_: EventHandlerCallRef?, theEvent: EventRef?, _: UnsafeMutableRawPointer?) -> OSStatus in
                guard let theEvent else { return OSStatus(eventNotHandledErr) }

                var hotKeyID = EventHotKeyID()
                let err = GetEventParameter(
                    theEvent,
                    EventParamName(kEventParamDirectObject),
                    EventParamType(typeEventHotKeyID),
                    nil,
                    MemoryLayout<EventHotKeyID>.size,
                    nil,
                    &hotKeyID
                )
                guard err == noErr else { return err }

                if hotKeyID.id == 1 {
                    DispatchQueue.main.async {
                        HotkeyManager.shared.onToggle?()
                    }
                }
                return noErr
            },
            1,
            &eventType,
            nil,
            &eventHandlerRef
        )

        if status != noErr {
            print("[HotkeyManager] Failed to install Carbon event handler: \(status)")
        }
    }

    private func registerToggleHotkey() {
        unregisterToggleHotkey()

        let binding = toggleBinding
        let carbonMods = carbonModifiers(from: binding.modifiers)
        let hotKeyID = EventHotKeyID(
            signature: OSType(0x444E5452), // "DNTR"
            id: 1
        )

        let status = RegisterEventHotKey(
            UInt32(binding.keyCode),
            carbonMods,
            hotKeyID,
            GetApplicationEventTarget(),
            0,
            &hotKeyRef
        )

        if status != noErr {
            print("[HotkeyManager] Failed to register toggle hotkey: \(status)")
        }
    }

    private func unregisterToggleHotkey() {
        if let ref = hotKeyRef {
            UnregisterEventHotKey(ref)
            hotKeyRef = nil
        }
    }

    private func removeCarbonHandler() {
        if let ref = eventHandlerRef {
            RemoveEventHandler(ref)
            eventHandlerRef = nil
        }
    }

    /// Re-register the toggle hotkey whenever the typed settings change.
    private func observeToggleHotkeyChanges() {
        guard let settingsProvider = settingsProvider as? AppSettingsController else { return }

        settingsCancellable = settingsProvider.$settings
            .map(\.hotkeys.toggle)
            .removeDuplicates()
            .dropFirst()
            .sink { [weak self] _ in
                self?.registerToggleHotkey()
            }
    }

    // MARK: - Local monitor (notch-level hotkeys, only when our app is active)

    private func installLocalMonitor() {
        localMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
            guard let self else { return event }
            return self.handleLocalKeyEvent(event) ? nil : event
        }
    }

    private func removeLocalMonitor() {
        if let m = localMonitor {
            NSEvent.removeMonitor(m)
            localMonitor = nil
        }
    }

    /// Handles notch-scoped hotkeys. Returns true if the event was consumed.
    private func handleLocalKeyEvent(_ event: NSEvent) -> Bool {
        // Local shortcuts only fire when the notch is open and focused.
        guard isNotchOpen else { return false }

        if newTabBinding.matches(event) {
            onNewTab?()
            return true
        }
        if closeTabBinding.matches(event) {
            onCloseTab?()
            return true
        }
        if nextTabBinding.matches(event) {
            onNextTab?()
            return true
        }
        if prevTabBinding.matches(event) {
            onPreviousTab?()
            return true
        }
        if nextWorkspaceBinding.matches(event) {
            onNextWorkspace?()
            return true
        }
        if previousWorkspaceBinding.matches(event) {
            onPreviousWorkspace?()
            return true
        }
        if detachBinding.matches(event) {
            onDetachTab?()
            return true
        }
        for summary in workspaceRegistry.workspaceSummaries {
            guard let binding = summary.hotkey else { continue }
            if binding.matches(event) {
                onSwitchToWorkspace?(summary.id)
                return true
            }
        }
        for preset in sizePresets {
            guard let binding = preset.hotkey else { continue }
            if binding.matches(event) {
                onApplySizePreset?(preset)
                return true
            }
        }

        // Cmd+1 through Cmd+9
        if event.modifierFlags.contains(.command) {
            let digitKeyCodes: [UInt16: Int] = [
                18: 0, 19: 1, 20: 2, 21: 3, 23: 4,
                22: 5, 26: 6, 28: 7, 25: 8
            ]
            if let tabIndex = digitKeyCodes[event.keyCode] {
                onSwitchToTab?(tabIndex)
                return true
            }
        }

        return false
    }

    // MARK: - Carbon modifier conversion

    private func carbonModifiers(from nsModifiers: UInt) -> UInt32 {
        var carbon: UInt32 = 0
        let flags = NSEvent.ModifierFlags(rawValue: nsModifiers)
        if flags.contains(.command) { carbon |= UInt32(cmdKey) }
        if flags.contains(.shift)   { carbon |= UInt32(shiftKey) }
        if flags.contains(.option)  { carbon |= UInt32(optionKey) }
        if flags.contains(.control) { carbon |= UInt32(controlKey) }
        return carbon
    }
}
