import AppKit
import IndieAbout
import SwiftUI

/// The scene graph (02-architecture.md § Windows, § Launch and window lifecycle).
///
/// ### Four scenes, and why each is the kind it is
///
/// - **Welcome** is a `Window`: there is one of it, ever, and `openWindow(id:)` focuses the existing
///   one rather than making a second.
/// - **The restore bootstrap** is a `Window` too, and a deliberate oddity — see
///   `RestoreBootstrapView` for why launch-time work has to wear a window at all.
/// - **Boards** and **cards** are `WindowGroup(for:)`s, because their identity is a *value*: opening
///   with a ref that already has a window focuses it, which is how "one board window per root" and
///   "at most one card window per card (reopen focuses)" are enforced by the scene rather than by
///   bookkeeping.
///
/// ### Restoration is the registry's, not the system's
///
/// Both groups declare `.restorationBehavior(.disabled)`. The app already knows which boards were
/// open — the registry's open-now flags, which survive a crash and reopen in `lastOpened` order —
/// and letting AppKit *also* restore windows would produce duplicates, and worse, card windows
/// restored behind boards that never opened. One mechanism, and it is the one that can explain
/// itself when a board has moved or gone.
///
/// ### Which window appears at launch
///
/// Exactly one of welcome and the bootstrap, decided once in `init` and never re-derived (see
/// `LaunchPlan`): the preference is read before any scene exists, and `restorables()` costs a
/// bookmark resolution per known board — a computed property here would pay that on every
/// scene-graph evaluation.
@main
struct KanbanApp: App {

    @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate

    @State private var appModel: AppModel

    /// What this launch does: welcome, the registry's restoration pass, or the accessibility audit
    /// suite's fixture board (`LaunchPlan`, `UITestLaunch`).
    private let launchPlan: LaunchPlan

    init() {
        // **AppKit window restoration is fully disowned** — restore-at-launch is the registry's job
        // (02-architecture.md § Launch and window lifecycle), every scene below declares
        // `.restorationBehavior(.disabled)`, and left alive the machinery is actively harmful: AppKit
        // counts its saved state (even a windowless one) as "a restored session", and SwiftUI then
        // treats every `defaultLaunchBehavior` as moot — the app launches with no windows at all and
        // no way to get one, since `windowOpener` is captured by the first scene that appears
        // (observed on macOS 26, 2026-07-29; `-ApplePersistenceIgnoreState YES` on the command line
        // proved the mechanism). Registered here because `App.init` runs before `NSApplicationMain`,
        // which is what makes a registration-domain default early enough for AppKit's read.
        UserDefaults.standard.register(defaults: ["ApplePersistenceIgnoreState": true])

        // Read first, because it decides *which app-side state the model is built over* — a fixture
        // launch keeps its recents and its clipboard snapshots in the scratch directory rather than in
        // the app's ordinary Application Support home.
        let isUITestFixtureLaunch = UITestLaunch.isFixtureLaunch
        if isUITestFixtureLaunch {
            UITestLaunch.prepareScratchDirectory()
        }
        let model = AppModel(
            registryStorageURL: isUITestFixtureLaunch
                ? UITestLaunch.registryStorageURL
                : BoardRegistry.defaultStorageURL,
            clipboardStagingRoot: isUITestFixtureLaunch
                ? UITestLaunch.clipboardStagingRoot
                : ClipboardStore.defaultStagingRoot
        )
        _appModel = State(initialValue: model)
        launchPlan = LaunchPlan.decide(
            isUITestFixtureLaunch: isUITestFixtureLaunch,
            restorePreference: AppPreferences.restoreOpenBoardsAtLaunch,
            hasRestorables: !model.boardRegistry.restorables().isEmpty
        )
        // The delegate is constructed by the adaptor before this runs, so this is the one place the
        // app's model and its AppKit half meet.
        appDelegate.appModel = model
    }

    var body: some Scene {
        Window("Welcome to Lanework", id: WindowID.welcome) {
            WelcomeView()
                .environment(appModel)
                .captureWindowActions(into: appModel)
        }
        // **Never presented by the system** — the restore bootstrap below opens welcome through
        // `AppModel.showWelcome()` when the launch pass ends with nothing else on screen. `.automatic`
        // was tried here (conditioned on the plan) and macOS 26 answered it by presenting *no scene at
        // all*: not welcome, not even a `.presented` bootstrap — a windowless launch with no way back,
        // since `windowOpener` is captured by the first scene that appears. One presenter, one rule.
        .defaultLaunchBehavior(.suppressed)
        .restorationBehavior(.disabled)
        // "Welcome: resizable, no title bar (background drag)" (03-board-ui.md § Welcome screen &
        // templates). `.contentMinSize` rather than `.contentSize`, because the view states a
        // *minimum* and the recents list is meant to take whatever space the user gives it; the
        // hidden title bar is why `WelcomeView` carries a `WindowDragGesture`.
        .windowResizability(.contentMinSize)
        .defaultSize(width: 820, height: 500)
        .windowStyle(.hiddenTitleBar)
        // Its automatic Window-menu item is replaced by the explicit command below, so the title is
        // the one 11-command-nexus.md names rather than whatever the scene happens to be called.
        .commandsRemoved()

        // The template chooser (09-templates.md), reached from File ▸ New Board… ⌥⌘N and from
        // welcome's own button. A `Window` for welcome's reason — there is one of it, ever — and
        // suppressed at launch, since ⌥⌘N is the only thing that ever asks for it.
        Window("New Board", id: WindowID.templateChooser) {
            TemplateChooserView()
                .environment(appModel)
                .captureWindowActions(into: appModel)
        }
        .defaultLaunchBehavior(.suppressed)
        .restorationBehavior(.disabled)
        .windowResizability(.contentSize)
        .commandsRemoved()

        Window("", id: WindowID.restoreBootstrap) {
            RestoreBootstrapView(plan: launchPlan)
                .environment(appModel)
                .captureWindowActions(into: appModel)
        }
        // **Presented at every launch, whatever the plan** — the bootstrap is the app's one reliable
        // way to put a window on screen. Welcome's `.automatic` above is a request the system is free
        // to decline, and on macOS 26 it does: a launch with nothing to restore presented *no* scene
        // at all, which left `windowOpener` uncaptured and the app a windowless shell no menu action
        // could revive (observed 2026-07-29). The pass itself
        // still dispatches on the plan — a `.welcome` launch restores nothing and shows welcome —
        // and this window stays invisible and dismisses itself either way.
        .defaultLaunchBehavior(.presented)
        .restorationBehavior(.disabled)
        .windowStyle(.plain)
        .defaultSize(width: 1, height: 1)
        .commandsRemoved()

        WindowGroup(id: WindowID.board, for: BoardWindowRef.self) { $ref in
            if let ref {
                BoardWindowHost(ref: ref)
                    .environment(appModel)
                    .captureWindowActions(into: appModel)
            }
        }
        .restorationBehavior(.disabled)
        .defaultLaunchBehavior(.suppressed)
        .commands { menuCommands }
        // **The system title is not this scene's to show, ever** — a board window says its own
        // name through the titlebar widget (`BoardInfoWidget`/`boardLoadingTitlebarAccessory`,
        // `BoardWindowHost`), never through AppKit's own title rendering, so the two can no longer
        // draw beside each other (the duplicate-name bug this scene modifier fixes).
        //
        // Declared here, at the *scene*, rather than only as `HostedWindowController.hideTitle()`'s
        // out-of-band `NSWindow.titleVisibility` write, for a429a7e's own reason restated for a
        // second AppKit knob: `titleVisibility` on a SwiftUI scene window is SwiftUI's to hold, and
        // it writes the tree's resolved value back every time it re-applies a window's
        // configuration — a tree that says nothing resolves `.visible`, so a later pass (any body
        // re-evaluation this window's board causes — a snapshot reload, a banner, a search-field
        // focus change) put the system title back beside the widget some time after the window
        // opened correct. Verified as an A/B harness (not the live app; documented honestly rather
        // than reproduced on screen — see the card journal): an out-of-band `.hidden` write reverts
        // to `.visible` on the very next `@State`-driven render with nothing declared here, and
        // holds through the same pressure (resize, key-status changes, repeated renders) once this
        // line is added.
        //
        // **Unconditional**, unlike `toolbarBackgroundVisibility` in `BoardWindowHost`: this is a
        // *scene* modifier, so it cannot read one window's live phase the way a `View` modifier
        // bound to `store.snapshot` can — every board window it creates gets the same posture,
        // always. That is also the right posture: this app never wants the system to draw a board
        // window's title, not even for the moment before the widget exists, which is why
        // `configureLoadingWindow` now gives the loading window a plain-text stand-in widget instead
        // of leaning on the system title for that moment (`BoardWindowHost`).
        .windowToolbarStyle(.unified(showsTitle: false))

        WindowGroup(id: WindowID.card, for: CardWindowRef.self) { $ref in
            if let ref {
                CardWindowHost(ref: ref)
                    .environment(appModel)
                    .captureWindowActions(into: appModel)
            }
        }
        .restorationBehavior(.disabled)
        .defaultLaunchBehavior(.suppressed)
        // The **first** card window's size, and only that one: every later window opens at the
        // last-used size or at its card's remembered frame, both applied by the host as the window
        // attaches (05-card-window.md ▸ Window). Derived from font metrics like every other
        // measurement in that window rather than written down in points.
        .defaultSize(CardWindowMetrics.defaultSize(bodyPointSize: CardWindowMetrics.bodyPointSize))

        Settings {
            SettingsView()
                .environment(appModel)
                .captureWindowActions(into: appModel)
        }
    }

    /// Every menu command 11-command-nexus.md inventories, at full row parity: built and validated
    /// where the window behind it already exists, present-but-`FutureCommand`-disabled where it
    /// doesn't (`FutureCommands.swift`).
    ///
    /// **The titles are API** (04-interactions.md ▸ Configurable bindings): macOS's App Shortcuts
    /// mechanism — `NSUserKeyEquivalents` under the hood — remaps menu items *by title*, so these
    /// strings are the keys a user's custom binding is stored under. They are spelled exactly as
    /// 11-command-nexus.md inventories them, unique across the whole menu bar, and changing one
    /// silently breaks every remap of it. Nothing below builds that mechanism — a stable title is the
    /// whole of the contract, and the system supplies the rest.
    @CommandsBuilder
    private var menuCommands: some Commands {
        // The About window (11-command-nexus.md files About under the app menu's standard
        // furniture): icon, version/build/date from the Info.plist that `update_build_info.sh`
        // stamped at build time — never a hardcoded string — with the version line opening the
        // bundled changelog. `AboutBox` names no edition (12-editions.md ▸ PIVOT 2026-08-08) —
        // there is one version of this app, so there is one box.
        IndieAboutCommand(configuration: AboutBox.configuration)

        // The File group, in 11-command-nexus.md's own row order: New Card, New Lane, New Board…,
        // Open…, Open Recent ▸, Board Info, Duplicate, Save as Template, Share…, Reveal in Finder,
        // Add Attachment…, then the trash trio and Empty Trash…. The board-scoped ones validate
        // against the frontmost board through the focus system, so each is simply absent-of-effect
        // when no board is in front; New Board… and Open Recent are available everywhere, including
        // with no window at all. Close is the system's own item — no row of ours to add.
        //
        // **Share… joined 2026-08-09** (design ruling, card 72691b11) right after Save as
        // Template: the third command in a row that copies the whole board wholesale, each to a
        // different destination — a sibling folder, the templates store, or a share sheet.
        CommandGroup(after: .newItem) {
            BoardCreationCommands()
            NewBoardCommand(appModel: appModel)

            Divider()

            Button("Open…") {
                appModel.presentOpenPanel()
            }
            .keyboardShortcut("o", modifiers: .command)

            // **Beside Open…, not beside Export** (15-import-export.md): the two rows next to each
            // other are the two ways a board arrives from disk, and like Open… this one needs no board
            // in front — it makes one. Export lives further down with the board-scoped commands that
            // act on the board already there. No default chord, like Welcome's row.
            ImportBoardCommand(appModel: appModel)

            OpenRecentMenu(appModel: appModel)

            Divider()

            BoardInfoCommand()

            Divider()

            DuplicateBoardCommand(appModel: appModel)
            SaveAsTemplateCommand(appModel: appModel)
            ShareBoardCommand(appModel: appModel)
            // **Export ▸ joined 2026-08-09** (15-import-export.md) as the fourth row in a row that
            // takes the whole board somewhere else — a sibling folder, the templates store, a share
            // sheet, and now one foreign document. Three named rows rather than one row with a format
            // popup; see `ExportBoardMenu` for why, and for why the titles matter.
            ExportBoardMenu(appModel: appModel)
            RevealInFinderCommand()
            AddAttachmentCommand()
            // The attachment row's hero pair, as the menu-bar twins 11-command-nexus.md's
            // context-menu contract requires ("no function's only home"). Two rows rather than one
            // that renames itself, for titles-are-API's reason (`SetAsHeroCommand`).
            SetAsHeroCommand()
            RemoveHeroCommand()
            AddCommentCommand()
            // **Delete Card joined 2026-08-09** (05-card-window.md ▸ Actions, retired — Pipeline card
            // bcd3b323): the card window's own delete, promoted off the sidebar's Actions section to
            // a menu row so the new toolbar item mirroring it (`CardToolbar`) has the menu twin every
            // toolbar function needs. Distinctly titled from the row below — "Delete" is that row's
            // own singleton title (11-command-nexus.md) — and deliberately chord-less: an enabled
            // delete-key equivalent here would steal delete-to-line-start from this window's text
            // surfaces, the reason `TrashCommands`' own ⌘⌫ was never extended to the card window.
            DeleteCardCommand()

            Divider()

            TrashCommands()
        }

        // Edit ▸ Undo/Redo, replacing the system's own pair — **the command surface is the app's**
        // (13-native-undo.md ▸ Rules, re-ruled 2026-08-08): the platform's nil-target rows resolve
        // through `NSWindow.undoManager`, which a SwiftUI window latches empty before any delegate of
        // ours can vend the board's, so the rows below read the focused session's stack themselves
        // (`UndoCommands.swift`).
        UndoRedoCommands()

        // The Edit menu: Paste as Board Background, then Find (⌘F) and its card-window stepping
        // twins, placed after the standard Cut/Copy/Paste/Select All group, which is where macOS puts
        // Find. The clipboard items above them stay the system's, answered by the board as a
        // responder (`ClipboardCommands.swift`) — a second item sharing one of those titles is what
        // titles-are-API forbids. Undo and Redo were the system's too until the latch (the group just
        // above).
        //
        // **Paste as Board Background joined 2026-08-09** with the image-data paste branch: it sits
        // directly under the system's Paste because it is the one paste this app has that ⌘V could
        // not carry — a backdrop has no selection to target, so it needs a name rather than a
        // modifier (`PasteBoardBackgroundCommand`). No default chord, like Welcome's row.
        CommandGroup(after: .pasteboard) {
            PasteBoardBackgroundCommand(clipboard: appModel.clipboard)

            Divider()

            FindCommand()
            FindSteppingCommands()
        }

        // The system's own toolbar rows — Show/Hide Toolbar and **Customize Toolbar…**, which
        // 11-command-nexus.md files under Standard macOS furniture ("Customize Toolbar… per system
        // convention (03)"). They are the platform's, spelled by the platform: both are nil-target
        // AppKit actions the key window's toolbar answers, so they validate per window (disabled on
        // welcome, live on the board and card windows) with nothing of ours in between. The
        // right-click ▸ Customize Toolbar… path 03 names is AppKit's too, and needs no row at all.
        ToolbarCommands()

        // The View menu. `CommandGroupPlacement.toolbar` *is* View — the menu the toolbar's own
        // items live in — which is where 11-command-nexus.md files Show Trash. Three dividers split it
        // by scope, which is the only grouping the inventory implies: the board's toggle, then the
        // board's zoom ladder, then the card window's view-state rows, then the app-wide appearance
        // override — last, because unlike everything above it, it needs no window in front at all.
        //
        // "Zoom In" / "Zoom Out" / "Actual Size" rather than a single "Zoom": the system's own Window
        // menu already carries a row titled Zoom, and titles are the remapping mechanism's key, so a
        // second one would collide (the rule this file's own header states).
        CommandGroup(after: .toolbar) {
            ShowTrashCommand()

            Divider()

            ZoomCommands(appModel: appModel)

            Divider()

            CardViewCommands()

            Divider()

            AppearanceCommands(appModel: appModel)
        }

        // The Board menu (11-command-nexus.md), complete and in its inventoried row order — Open
        // Card, Copy Link, Rename, Style…, the card moves, the lane moves, the width pair. Its items
        // act on the frontmost board window, which they reach through the focus system rather than
        // through the app model — see `BoardCommands.swift`, which also owns their validation.
        //
        // **Board Settings… came out 2026-08-07** with the sheet it opened (03 ▸ Board settings
        // sheet, marked retired; the 2026-07-31 popover/sheet split reversed): a board is configured
        // in its popover, whose keyboard door is File ▸ Board Info ⌘I, so this menu's last divider
        // went with the row.
        //
        // **The Board ▸ Pull/Push row (`RemoteCommands`) came out 2026-08-08** with app-managed git
        // itself (strategy/01-git-excision.md): the width pair is now the menu's last row.
        //
        // **Copy Link joined 2026-08-09** (design ruling, card 737a949f) right beside Open Card: the
        // two read-only rows — nothing here mutates the board — grouped ahead of the edit-shaped
        // block below (Rename, Style…), which is where the context menu puts the same two.
        CommandMenu("Board") {
            OpenCardCommand()
            CopyLinkCommand()
            BoardRenameCommand()
            BoardStyleCommand()

            Divider()

            MoveCardCommands()
            MoveLaneCommands()

            Divider()

            LaneWidthCommands()
        }

        CommandGroup(after: .windowList) {
            // No default chord — "— (no default)" in the Nexus is deliberate, not a gap; it remaps
            // like any other item.
            Button("Welcome to Lanework") {
                appModel.showWelcome()
            }
        }

        // File ▸ Print… (⌘P) — **the app's own row**, replacing the system's nil-target one
        // (11-command-nexus.md's Print row; the "No Print story in v1 (⌘P unused)" line it retired).
        //
        // `replacing: .printItem` rather than an addition, for the same reason Undo/Redo replace the
        // platform's pair: the standard rows are nil-target actions resolved through the responder
        // chain, and this app's print target is the *frontmatter-shaped document behind the focused
        // window*, which no responder vends. Two items sharing the title "Print…" is also exactly what
        // titles-are-API forbids.
        //
        // A reported "This application does not support printing" alert (2026-08-09) turned out to be
        // the sandbox denying `NSPrintOperation` for want of `com.apple.security.print`
        // (`Kanban.entitlements`) — the actual fix, not anything here. Found alongside it, and worth
        // keeping regardless: `PrintCommand` used to disable itself over a window with no board and no
        // card, and a disabled `Button` still owns its `.keyboardShortcut` — the unclaimed chord fell
        // straight through to AppKit's own nil-target `printDocument:`, whose stock failure happens to be
        // the identical alert text by a different route. The row now claims ⌘P unconditionally and
        // answers the no-board-no-card case itself (`PrintCommand`'s doc comment).
        //
        // Page Setup… stays absent with it: the paper questions are answered in the print panel's own
        // page-setup group (`PrintCoordinator`), so a second dialog would be a second place to set one
        // margin.
        CommandGroup(replacing: .printItem) {
            PrintCommand(appModel: appModel)
        }

        // Help carries "the one line teaching the System Settings remap path" (11-command-nexus.md ▸
        // Standard macOS furniture) — this app's whole Help menu, since there is no other content to
        // give it. The pane identifier is the modern System Settings extension id, not the legacy
        // `com.apple.preference.keyboard` prefpane path: verified on macOS 26 by observing
        // `KeyboardSettings.appex` (service `com.apple.Keyboard-Settings.extension`) launch in
        // response to this exact URL. `?Shortcuts` is as deep as a URL reaches; **App Shortcuts**
        // itself is one more click, the sidebar row inside that pane.
        CommandGroup(after: .help) {
            Button("Customize Keyboard Shortcuts…") {
                guard let url = URL(
                    string: "x-apple.systempreferences:com.apple.Keyboard-Settings.extension?Shortcuts"
                ) else { return }
                NSWorkspace.shared.open(url)
            }
        }
    }
}
