import AppKit
import Carbon.HIToolbox

@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate {
    private var statusItem: NSStatusItem!
    private let overlay = OverlayController()
    private let calendar = CalendarWatcher()
    private let store = SettingsStore(config: Config.load())
    private let settingsWindow = SettingsWindowController()

    private var timer: Timer?
    /// Interval the live timer was built with, so settings changes can be compared.
    private var activePollSeconds = 0
    private var snapshotter: Snapshotter?
    private var testHotKey: HotKey?
    private var dismissHotKey: HotKey?
    /// Reminders already announced, so a 60-second poll doesn't repeat them.
    private var fired = Set<String>()

    private var config: Config { store.config }

    func applicationDidFinishLaunching(_ notification: Notification) {
        wireStore()
        applyLook()

        // `AlienHUD --snapshot <dir>` renders every palette off-screen and quits.
        if let index = CommandLine.arguments.firstIndex(of: "--snapshot"),
           CommandLine.arguments.count > index + 1 {
            let snapshotter = Snapshotter(directory: CommandLine.arguments[index + 1])
            self.snapshotter = snapshotter
            snapshotter.run()
            return
        }

        // `AlienHUD --preview` plays one scene and quits. Used for checking the
        // animation on a real screen without waiting for something to be due.
        if CommandLine.arguments.contains("--preview") {
            testFire()
            DispatchQueue.main.asyncAfter(deadline: .now() + Alien.sceneDuration + 2) {
                NSApp.terminate(nil)
            }
            return
        }

        buildMenuBarItem()

        // ⌃⌥⌘A test fires; ⌃⌥⌘X dismisses whatever's on screen.
        testHotKey = HotKey(keyCode: kVK_ANSI_A,
                            modifiers: UInt32(controlKey | optionKey | cmdKey)) { [weak self] in
            self?.testFire()
        }
        dismissHotKey = HotKey(keyCode: kVK_ANSI_X,
                               modifiers: UInt32(controlKey | optionKey | cmdKey)) { [weak self] in
            self?.overlay.dismissAll()
        }

        if config.calendarEnabled {
            requestCalendarAccess()
        }

        startPolling()

        // `AlienHUD --settings` opens the window straight away instead of waiting for
        // a menu click. Handy for checking the layout.
        if CommandLine.arguments.contains("--settings") {
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { [weak self] in
                self?.openSettings()
            }
        }
    }

    func applicationWillTerminate(_ notification: Notification) {
        timer?.invalidate()
    }

    // MARK: - Store wiring

    private func wireStore() {
        store.calendarAccess = calendar.accessGranted
        store.config.openAtLogin = LoginItem.isEnabled
        if calendar.accessGranted { adoptCalendars() }

        store.onChange = { [weak self] newConfig in
            guard let self else { return }
            applyLook()
            // Only rebuild the timer if the interval actually changed — otherwise every
            // nudge of the size slider would fire a fresh ClickUp request.
            if newConfig.pollSeconds != activePollSeconds {
                startPolling()
            }
            if newConfig.openAtLogin != LoginItem.isEnabled {
                store.loginItemError = LoginItem.set(newConfig.openAtLogin)
            }
        }
        store.onTestFire = { [weak self] in self?.testFire() }
        store.onRequestCalendarAccess = { [weak self] in self?.requestCalendarAccess() }
    }

    /// Pushes the appearance settings onto the overlay.
    private func applyLook() {
        overlay.scale = config.sceneScale
        overlay.fixedPaletteName = config.fixedPalette
    }

    private func requestCalendarAccess() {
        calendar.requestAccess { [weak self] granted in
            guard let self else { return }
            store.calendarAccess = granted
            if granted { adoptCalendars() }
            refreshMenu()
        }
    }

    /// Loads the calendar list for the UI, and on the very first run ticks all of them
    /// so the checklist starts from "everything" and unticking is what excludes.
    private func adoptCalendars() {
        let all = calendar.allCalendars()
        store.calendars = all
        if !config.calendarsInitialised {
            var updated = store.config
            updated.selectedCalendarIds = all.map(\.id).sorted()
            updated.calendarsInitialised = true
            store.config = updated
        }
    }

    // MARK: - Menu bar

    private func buildMenuBarItem() {
        statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
        statusItem.button?.title = "👽"
        refreshMenu()
    }

    private func refreshMenu() {
        let menu = NSMenu()

        menu.addItem(MenuHeader.item(appName: "Alien Reminder", status: statusLine()))
        menu.addItem(.separator())

        let test = NSMenuItem(title: "Test Fire", action: #selector(testFire),
                              keyEquivalent: "a")
        test.keyEquivalentModifierMask = [.control, .option, .command]
        test.target = self
        menu.addItem(test)

        let dismiss = NSMenuItem(title: "Dismiss Reminder",
                                 action: #selector(dismissReminder), keyEquivalent: "x")
        dismiss.keyEquivalentModifierMask = [.control, .option, .command]
        dismiss.target = self
        menu.addItem(dismiss)

        let check = NSMenuItem(title: "Check Now", action: #selector(checkNow),
                               keyEquivalent: "")
        check.target = self
        menu.addItem(check)

        menu.addItem(.separator())

        let settings = NSMenuItem(title: "Settings…", action: #selector(openSettings),
                                  keyEquivalent: ",")
        settings.target = self
        menu.addItem(settings)

        let quit = NSMenuItem(title: "Quit", action: #selector(quit), keyEquivalent: "q")
        quit.target = self
        menu.addItem(quit)

        statusItem.menu = menu
    }

    private func statusLine() -> String {
        guard config.calendarEnabled else { return "Reminders are switched off" }
        guard store.calendarAccess else { return "Waiting for calendar access" }
        let count = config.selectedCalendarIds.count
        if count == 0 { return "No calendars selected yet" }
        let word = count == 1 ? "calendar" : "calendars"
        return "Watching \(count) \(word) · \(config.meetingLeadMinutes) min warning"
    }

    // MARK: - Actions

    @objc private func testFire() {
        overlay.show(Reminder(id: "test-fire",
                              kind: .meeting,
                              title: "Strategy call with Bells",
                              due: Date().addingTimeInterval(5 * 60)))
    }

    @objc private func dismissReminder() {
        overlay.dismissAll()
    }

    @objc private func checkNow() {
        poll()
    }

    @objc private func openSettings() {
        // Pick up anything that changed outside the window (token added in Terminal,
        // calendar permission granted since launch).
        if calendar.accessGranted, store.calendars.isEmpty { adoptCalendars() }
        store.config.openAtLogin = LoginItem.isEnabled
        settingsWindow.show(store: store)
    }

    @objc private func quit() {
        NSApp.terminate(nil)
    }

    // MARK: - Polling

    private func startPolling() {
        timer?.invalidate()
        activePollSeconds = config.pollSeconds
        let interval = TimeInterval(max(15, config.pollSeconds))
        let timer = Timer(timeInterval: interval, repeats: true) { [weak self] _ in
            Task { @MainActor in self?.poll() }
        }
        // A little tolerance keeps the app off the CPU when nothing's due.
        timer.tolerance = 5
        RunLoop.main.add(timer, forMode: .common)
        self.timer = timer
        poll()
    }

    private func poll() {
        applyLook()

        if config.calendarEnabled, calendar.accessGranted {
            announce(calendar.upcoming(leadMinutes: config.meetingLeadMinutes,
                                       calendarIds: config.selectedCalendarIds))
        }

    }

    /// Shows anything not already shown, soonest first.
    private func announce(_ reminders: [Reminder]) {
        for reminder in reminders.sorted(by: { $0.due < $1.due }) {
            guard !fired.contains(reminder.dedupeKey) else { continue }
            fired.insert(reminder.dedupeKey)
            overlay.show(reminder)
        }
        pruneFired()
    }

    /// Keeps the fired set from growing forever — anything older than a day is gone.
    private func pruneFired() {
        guard fired.count > 500 else { return }
        let cutoff = Int(Date().addingTimeInterval(-86_400).timeIntervalSince1970 / 60)
        fired = fired.filter { key in
            guard let minute = Int(key.split(separator: "@").last ?? "") else { return true }
            return minute >= cutoff
        }
    }
}
