import AppKit
import os

/// The three window-lifecycle answers SwiftUI has no modifier for (02-architecture.md § Launch and
/// window lifecycle, § Windows).
///
/// It holds the `AppModel` rather than reaching for a singleton: `KanbanApp` creates the model and
/// hands it over in its own `init`, so there is exactly one and no global to accidentally build a
/// second registry behind.
@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate {

    /// Set by `KanbanApp.init()`. Optional only because the adaptor constructs this object before the
    /// model exists; it is non-`nil` from the first run-loop turn onward.
    var appModel: AppModel?

    private static let logger = Logger(subsystem: "dev.rzen.indie.Kanban", category: "app-delegate")

    /// Applies the stored appearance override to `NSApp` (03-board-ui.md ▸ Toolbar).
    ///
    /// **Here rather than in `AppModel.init`**, and the distinction matters: a unit-test host *is*
    /// this app, so `KanbanApp.init()` and therefore `AppModel.init` run on every test launch
    /// (`AppStateHome.isUnitTestHost`). `AppearanceStore.init` only reads a default; this is the one
    /// call that hands its answer to `NSApp` — the global side effect `KanbanApp.init` must not
    /// carry (`AppearanceStore.applyCurrent`). Launch is the honest home for it, and this is the
    /// app's.
    func applicationDidFinishLaunching(_ notification: Notification) {
        appModel?.appearance.applyCurrent()
    }

    /// **The close is respected.** "Closing the last board window leaves the app windowless (menu bar
    /// alive)" — a document-shaped app whose windows are boards has no business quitting because the
    /// user tidied one away, and welcome is one Dock click or one menu item back.
    func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
        false
    }

    /// A Dock click with nothing on screen shows welcome — the other half of the rule above.
    ///
    /// `false` means "handled, do nothing further"; `true` lets AppKit run its default (unminiaturize,
    /// open an untitled document), which is right when windows do exist and wrong when they do not —
    /// this app has no untitled document to make.
    func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows: Bool) -> Bool {
        guard !hasVisibleWindows, let appModel else { return true }
        appModel.showWelcome()
        return false
    }

    /// Double-clicking a `.kanban` folder in Finder, or `open -a` — "opening a board from Finder is a
    /// standard document open" (02-architecture.md § Launch and window lifecycle). Every URL is
    /// forwarded to `AppModel.openBoard(at:)`, the exact entry point welcome and File ▸ Open… already
    /// call (`AppModel.presentOpenPanel()`), so a Finder open gets the identical registry record,
    /// board window, and recents stamp, and a board that is already open focuses its window rather
    /// than opening a second one — `openBoard` starts its own security-scoped access on the URL, the
    /// same as the open panel's, so nothing needs stashing here first.
    ///
    /// **Not pre-validated.** Info.plist's `CFBundleDocumentTypes` declares the UTI, so macOS should
    /// never route anything but a `.kanban` folder here — but if it did, or the folder has since gone
    /// missing, `openBoard`'s fail-fast load surfaces the failure row-level on welcome, uniform with
    /// every other open failure. A second vocabulary for "not a board" here would just be a worse copy
    /// of the one that already exists.
    ///
    /// **Can fire before any scene has appeared** — a cold launch (the app was not already running)
    /// delivers this ahead of the first window's `onAppear`, which is where `windowOpener` is normally
    /// captured (`CaptureOpenWindow`). `openBoard` buffers a URL that arrives that early and replays
    /// it once the action exists, so this method does not have to reason about launch ordering itself.
    func application(_ application: NSApplication, open urls: [URL]) {
        guard let appModel else { return }
        for url in urls {
            appModel.openBoard(at: url)
        }
    }

    /// Finder's `printFiles` Apple Event — File ▸ Print… on a board selected in Finder, a drag onto a
    /// printer queue, or a print-and-open service. Left unimplemented, this delivers to AppKit's stock
    /// nil-target print handling and its stock failure: "This application does not support printing.
    /// Please contact the application's developer." — the same wording the *reported* bug turned out to
    /// have a different cause for (the sandbox's `com.apple.security.print` entitlement,
    /// `Kanban.entitlements`, `PrintCommand`'s doc comment tells that story) — but an unhandled Apple
    /// Event is its own route to it regardless, which is what this method retires
    /// (`PrintCoordinator.printFiles`'s doc comment tells the whole story; this method is only the
    /// receiving end of the Apple Event).
    ///
    /// Every path is resolved and printed independently — `PrintCoordinator.printFiles` does the actual
    /// work, headless (no store, no window, `BoardLoader` on the path directly), so this method is a
    /// straight handoff of the Apple Event's three arguments and nothing more. `appModel` should always
    /// be set by the time an Apple Event can reach here (it exists from the first run-loop turn onward —
    /// this type's own note), but a `nil` is answered with `.printingFailure` rather than force-unwrapped,
    /// the same defensive posture `application(_:open:)` takes one method up.
    func application(
        _ application: NSApplication,
        printFiles filenames: [String],
        withSettings printSettings: [NSPrintInfo.AttributeKey: Any],
        showPrintPanels: Bool
    ) -> NSApplication.PrintReply {
        guard let appModel else { return .printingFailure }
        return PrintCoordinator.printFiles(
            filenames,
            settings: printSettings,
            showPrintPanels: showPrintPanels,
            profiles: appModel.printProfiles
        )
    }

    /// Quit runs the close flush for **every** open board before the app goes away.
    ///
    /// The same `CloseFlushCoordinator` sequence as a user close, once per board, in the same fixed
    /// order — card windows and their sessions, then pending debounced work, then the registry stamp,
    /// then teardown (02 § Windows: "closing a board window (**and app quit**) first closes the
    /// board's card windows …"). The one difference is the cause: quit does not clear the open-now
    /// flags, which is what makes the next launch reopen exactly this set.
    ///
    /// `.terminateLater` plus a deferred reply is the only way to await anything here — the delegate
    /// method is synchronous and the flush is not. With no boards open there is nothing to flush and
    /// the app exits immediately rather than taking a run-loop turn to discover that.
    func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
        guard let appModel, appModel.hasOpenBoards else { return .terminateNow }

        Task { @MainActor in
            await appModel.flushAllBoardsForQuit()
            Self.logger.debug("quit flush complete")
            sender.reply(toApplicationShouldTerminate: true)
        }
        return .terminateLater
    }
}
