import AppKit
import Observation
import SwiftUI
import os

// MARK: - The focused card window's printable card

/// **What a card window offers File ▸ Print…**: the card as the last snapshot found it, the lane it is in,
/// the board it belongs to, its folder, and the thread it is showing.
///
/// A handle of its own rather than a reuse of `cardComments` or `cardAttachments`, because neither carries
/// the card: the comments pane knows a title and a folder (it announces about them), the attachments
/// section knows a folder, and printing needs the whole `Card` — its `icon`, its reserved `labels`, its
/// body. `CardAttachments`' shape and for its reasons: one per window, `@State` in the host, published
/// through the focus system so a **menu row** reaches the frontmost card window without anyone keeping a
/// which-window-is-key register (`FocusedBoardStoreKey`).
///
/// **Re-derived from every snapshot**, like the window's title and subtitle: a card renamed, restyled or
/// moved between lanes mid-session prints as it is now, not as it was when the window opened.
@MainActor
@Observable
final class CardPrintSubject {

    /// `nil` until the window has joined its board — which is also exactly when there is nothing to print.
    var card: Card?

    /// The lane the card is in, for the print's context line. `nil` renders as the same "Untitled"
    /// placeholder a lane header shows (`PrintDocumentBuilder.untitled`).
    var laneTitle: String?

    /// The board's display name (`AppModel.displayName(of:)`), for the running head.
    var boardTitle = ""

    /// The card's folder — the anchor a relative image in its body resolves against (`BodyTarget.resolve`).
    var cardFolder: URL?

    /// The thread as the pane last read it, read again at print time.
    ///
    /// A closure rather than a stored value for `CardComments.readThread`'s reason: comments are
    /// window-scoped and outside the snapshot, so there is nothing to republish from — the pane re-reads
    /// from disk, and a print asks the same question at the moment it is asked to print.
    @ObservationIgnored
    var readThread: (() -> CommentThread)?

    init() {}
}

struct FocusedCardPrintKey: FocusedValueKey {
    typealias Value = CardPrintSubject
}

extension FocusedValues {
    var cardPrint: CardPrintSubject? {
        get { self[FocusedCardPrintKey.self] }
        set { self[FocusedCardPrintKey.self] = newValue }
    }
}

// MARK: - File ▸ Print…

/// **File ▸ Print… (⌘P)** — 11-command-nexus.md's Print row, and the retirement of that document's "No
/// Print story in v1 (⌘P unused)" line.
///
/// ### The reported bug's actual cause, and what this file adds beside it
///
/// The owner's repro (2026-08-09) is the board window frontmost, File ▸ Print… **enabled**, chosen from the
/// menu — and the system alert appears anyway: "This application does not support printing. Please contact
/// the application's developer." That rules out this row ever being unclaimed or disabled in the repro; the
/// row runs, `PrintCoordinator` runs, and the alert comes out of the print operation itself. The actual cause
/// is `Kanban.entitlements`: the app is sandboxed (`com.apple.security.app-sandbox`) and had no
/// `com.apple.security.print` key, and a sandboxed app's `NSPrintOperation` is denied by the sandbox with
/// this exact wording — the fix is that one entitlement key, not anything in this file.
///
/// What *is* in this file, below, is real but secondary hardening: a **separate, narrower** failure mode
/// with the coincidence of producing the identical alert text through AppKit's own nil-target print action
/// rather than the sandbox (see "Always enabled" below) — a window that published no print scope at all used
/// to leave ⌘P's key equivalent live and unclaimed, which is worth closing regardless of the entitlement.
/// Finder's `printFiles` half (`PrintCoordinator.printFiles`) is hardening of a different kind: it was
/// simply unimplemented before, which is its own route to the same stock alert, entitlement or not.
///
/// ### Two scopes, one row — and a third that isn't a scope at all
///
/// The board window prints **the board**: its lanes left to right, each lane's cards top to bottom, as a
/// linear document rather than a picture of the strip (`PrintSource.board`). The card window prints **that
/// card**. One menu row for both, which is what the platform means by Print — the frontmost window's
/// document — and which is why the row reaches its subject through the focus system rather than through the
/// app model.
///
/// The two are told apart by which focused value is present, and cannot both be: a card window publishes no
/// `boardStore` (`CardWindowHost`), so the board branch is scopeless there. The order below therefore
/// decides nothing, and it follows `RevealInFinderCommand`'s — the board in front wins, the card-window
/// branch stands when a card window is.
///
/// A third window shape publishes neither: welcome, the template chooser, Settings, the restore-bootstrap
/// window, or a card window whose board has not joined yet (`CardPrintSubject.card`'s own `nil` note). Those
/// are not a third *scope* — there is nothing behind them to print — but ⌘P has to answer for them somehow,
/// and the next section is about exactly that answer.
///
/// ### Always enabled — the alternative is worse than a refusal
///
/// **The row never disables**, which is a reversal from this feature's first cut: it used to grey out over
/// the scopeless window above, on the ordinary "nothing to act on" theory every other command follows. The
/// trouble is what "greyed out" means for a *keyboard shortcut* rather than a menu click — a disabled
/// `Button` still owns its `.keyboardShortcut`, so ⌘P over one of those windows was never actually inert.
/// SwiftUI drops the unclaimed chord back into the responder chain, which hands it to AppKit's own nil-target
/// `printDocument:` — the platform's stock print action, present whether or not this app ever registers a
/// document type for it — and *that* action's stock failure is, coincidentally, the same alert text the
/// entitlement gap produces: "This application does not support printing. Please contact the application's
/// developer." Two different mechanisms, one AppKit sentence — the entitlement is what the owner's repro
/// needed; this closes the other door to the same words, for a window with genuinely nothing to print rather
/// than a board or card the app simply couldn't get permission to send to a printer.
///
/// So the row claims ⌘P **unconditionally**, in every window, and answers the scopeless case itself rather
/// than declining to answer at all — the same instinct `BoardInfoCommand` and `RevealInFinderCommand` already
/// have for the read-only lock (a locked board is exactly the board someone wants a paper copy of), pushed
/// one layer further out. Validation used to be "scope and nothing else"; now there *is* nothing else to
/// validate — every window answers, and the only question is which of three answers it gets.
///
/// Neither the read-only lock nor the focused-editor rule ever closed this row, unlike every mutating row in
/// `BoardCommands.swift`: a print is a **read**. An inline title editor is no obstacle either — the print
/// takes the snapshot as it stands, which is what is on screen.
///
/// A board with **nothing to print** — no lanes, or every card empty of every included component — still
/// enables the row, deliberately: the honest place to discover that is the panel's own preview, and a ⌘P
/// that greys out on a board the user is looking at reads as a broken app rather than as an empty document.
/// That refusal, and the scopeless one, are both `PrintCoordinator`'s and both say so out loud
/// (`PrintCoordinator.refuse` and `.refuseNoScope`) — an app-drawn sentence in this app's own voice, never
/// AppKit's stock line about a developer to contact.
struct PrintCommand: View {

    let appModel: AppModel

    @FocusedValue(\.boardStore) private var store
    @FocusedValue(\.cardPrint) private var cardPrint

    var body: some View {
        Button("Print…") {
            print()
        }
        .keyboardShortcut("p", modifiers: .command)
    }

    /// The row's three answers, named — the shape `print()` switches over and the shape a test can hold
    /// without a menu, a window, or AppKit (`SaveAsTemplateCommand.allowsSave`'s reason for `isEnabled`
    /// existing as a pure function in the first place, one refactor later).
    enum Scope: Equatable {
        /// A board window is in front — print the board.
        case board
        /// No board window, but a card window with a card that has joined its board — print the card.
        case card
        /// Neither: welcome, the template chooser, Settings, the restore-bootstrap window, or a card
        /// window whose board has not caught up yet. Not a failure — a fact about what is on screen — and
        /// answered with a sentence rather than silence (`PrintCoordinator.refuseNoScope`).
        case refuse
    }

    /// The row's whole decision, as a pure function of the two facts it turns on — extracted from the view
    /// for the same reason `isEnabled` used to be: a rule that can only be exercised through a menu is a
    /// rule nobody tests. What changed is the shape of the answer, not the two facts it is a function of.
    ///
    /// **The board in front wins**, exactly as `RevealInFinderCommand`'s three-way branch does — a card
    /// window publishes no `boardStore`, so the two facts can never both be true at once, and the order
    /// below decides nothing real.
    static func resolveScope(hasBoard: Bool, hasPrintableCard: Bool) -> Scope {
        if hasBoard { return .board }
        if hasPrintableCard { return .card }
        return .refuse
    }

    private func print() {
        switch Self.resolveScope(hasBoard: store != nil, hasPrintableCard: cardPrint?.card != nil) {
        case .board:
            guard let store else { return }
            PrintCoordinator.printBoard(store: store, profiles: appModel.printProfiles)
        case .card:
            guard let cardPrint else { return }
            PrintCoordinator.printCard(cardPrint, profiles: appModel.printProfiles)
        case .refuse:
            PrintCoordinator.refuseNoScope()
        }
    }
}

// MARK: - PrintCoordinator

/// **The AppKit half of ⌘P**: build the session, hand the panel our accessory, run the operation. And, since
/// 2026-08-09, **the AppKit half of Finder's half too** — the `printFiles` Apple Event a File ▸ Print… on a
/// selected board, a drag onto a printer queue, or a print-and-open service delivers, forwarded here from
/// `AppDelegate.application(_:printFiles:withSettings:showPrintPanels:)` rather than left unimplemented.
/// Neither half of this was the reported bug's actual cause — `PrintCommand`'s own doc comment tells that
/// story, and it is `Kanban.entitlements`' `com.apple.security.print` key, not this file. Left unimplemented,
/// though, an unhandled `printFiles` *is* its own route to AppKit's stock refusal — "This application does
/// not support printing. Please contact the application's developer." — entitlement or not, so implementing
/// it is still worth doing on its own terms.
///
/// ### Why the windowed operation is sheeted rather than run modally
///
/// `runModal()` would block the main thread inside a nested run loop for as long as the panel is up, which
/// is the shape `NSSavePanel`'s uses in `DuplicateBoardCommand` — and is right there, because that panel
/// answers a question the copy is *waiting* on. A print is not: the board keeps reloading, the watcher keeps
/// running, an agent may be writing. So `run(_:)` uses the sheeted form when a window exists, whose
/// completion is where the Last Used capture lands. `printFiles` never has that luxury — Finder's caller is
/// not a window at all — so its half always takes the synchronous fallback `run(_:)` itself falls back to
/// when there is no key window (`runOperation(for:showsPrintPanel:)`, the two entry points' shared floor).
///
/// ### The two refusals
///
/// A document with no pages is refused with an alert instead of printed (`refuse(_:)`). It is reachable two
/// ways — a board with no cards, and every component toggled off — and both deserve a sentence rather than a
/// sheet of running heads over blank paper. A ⌘P with **no scope at all** — the window in front is not a
/// board and not a card — gets the second sentence (`refuseNoScope()`), and `printFiles` reuses that same
/// wording for a path that does not resolve to a board. Neither alert is 02-architecture.md's write-failure
/// banner surface, which is about writes: nothing failed in either case, there was simply nothing to print.
@MainActor
enum PrintCoordinator {

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

    // MARK: Entry points

    /// **The board**, as of this moment (`PrintSource`'s frozen-snapshot note).
    static func printBoard(store: BoardStore, profiles: PrintProfileStore) {
        let snapshot = store.snapshot
        let title = AppModel.displayName(of: store)

        let session = PrintSession(
            provider: PrintSourceProvider(
                withoutComments: PrintSource.board(snapshot, titled: title),
                // Deferred, and read at most once — the comments toggle decides whether a board print pays
                // a thread read per card (`PrintSourceProvider`).
                withComments: {
                    PrintSource.board(snapshot, titled: title) { card in
                        PrintComment.list(of: store.commentThread(inCard: card.id))
                    }
                }
            ),
            profiles: profiles,
            // No single folder is right for every card's relative images, so a board print resolves none
            // (`PrintDocumentRenderer.appendBody`).
            cardFolder: nil,
            jobTitle: title,
            boardTitle: title
        )
        run(session)
    }

    /// **One card**, with the thread read now.
    static func printCard(_ subject: CardPrintSubject, profiles: PrintProfileStore) {
        guard let card = subject.card else { return }
        let comments = subject.readThread.map { PrintComment.list(of: $0()) } ?? []
        let source = PrintSource.card(
            card,
            laneTitle: subject.laneTitle,
            boardTitle: subject.boardTitle,
            comments: comments
        )
        let session = PrintSession(
            provider: PrintSourceProvider(complete: source),
            profiles: profiles,
            cardFolder: subject.cardFolder,
            jobTitle: card.title.value ?? PrintDocumentBuilder.untitled,
            boardTitle: subject.boardTitle
        )
        run(session)
    }

    // MARK: The operation

    private static func run(_ session: PrintSession) {
        guard !session.blocks().isEmpty else {
            refuse(session)
            return
        }
        let operation = makeOperation(for: session, showsPrintPanel: true)

        // **The Last Used capture happens when the operation ends, and only if it ran** — see
        // `PrintCompletion`, which is also the reason the sheeted form needs a delegate at all.
        if let window = keyWindow() {
            let completion = PrintCompletion(session: session)
            operation.runModal(
                for: window,
                delegate: completion,
                didRun: #selector(PrintCompletion.printOperationDidRun(_:success:contextInfo:)),
                contextInfo: nil
            )
        } else {
            // No window to sheet on — which should not happen for a command scoped to a focused window, but
            // a print is still a legitimate thing to do and a modal run is the honest fallback. `run()` is
            // synchronous, so the capture is an ordinary line rather than a callback.
            runOperation(operation, session: session)
        }
    }

    /// **The one place an `NSPrintOperation` is actually built** — `run(_:)`'s sheeted path and
    /// `printFiles`'s always-headless one both start here, so the two can never quietly drift apart on the
    /// paper, the accessory, or the job title.
    ///
    /// `NSPrintInfo.shared`, deliberately: it is where the panel's paper, orientation and margins are
    /// remembered between prints, which is exactly the continuity a user expects from a print dialog — and
    /// an app with no `NSDocument` has no per-document print info for them to live in instead (which is also
    /// why `printFiles`'s own `withSettings` dictionary is read by nobody here — see its doc comment).
    ///
    /// The accessory and the panel's option set are built only when a panel is actually going to show:
    /// `showsPrintPanel: false` is Finder's silent-print request, and an accessory nobody will ever see is a
    /// `PrintSession` retained for no reason.
    private static func makeOperation(for session: PrintSession, showsPrintPanel: Bool) -> NSPrintOperation {
        let printInfo = NSPrintInfo.shared
        // Both modes are moot while the view answers `knowsPageRange` itself — AppKit takes the view's page
        // rects and does not subdivide them further — and they are set anyway to say what the document is:
        // one column exactly as wide as the page, which never spills sideways.
        printInfo.horizontalPagination = .clip
        printInfo.verticalPagination = .automatic

        let view = PrintDocumentView(session: session, printInfo: printInfo)
        let operation = NSPrintOperation(view: view, printInfo: printInfo)
        operation.jobTitle = session.jobTitle
        operation.showsPrintPanel = showsPrintPanel
        operation.showsProgressPanel = showsPrintPanel

        if showsPrintPanel {
            let panel = operation.printPanel
            // The preview is what makes the accessory worth having (`PrintOptionsAccessoryController`), and
            // the page-setup group is what lets the paper questions be answered in the same dialog rather
            // than in a second one this app does not have (there is no File ▸ Page Setup… row —
            // 11-command-nexus.md).
            panel.options.formUnion([.showsPreview, .showsPaperSize, .showsOrientation, .showsScaling, .showsCopies, .showsPageRange])
            panel.addAccessoryController(PrintOptionsAccessoryController(session: session))
        }
        return operation
    }

    /// The synchronous fallback both windowless callers use: run the operation now, capture Last Used only
    /// on success (`PrintCompletion`'s own asymmetry, restated here because there is no delegate on this
    /// path to hold it) — and hand back what happened, which `run(_:)`'s own fallback discards and
    /// `printFiles` reports to Finder.
    @discardableResult
    private static func runOperation(_ operation: NSPrintOperation, session: PrintSession) -> Bool {
        let success = operation.run()
        if success {
            session.profiles.captureLastUsed(session.options)
        }
        return success
    }

    /// The window the sheet hangs on: the app's key window, which for a command validated against the focus
    /// system *is* the window that published the subject. Asked of AppKit rather than threaded through the
    /// focus system because a `NSWindow` is not a value a `FocusedValue` should carry, and because
    /// `WindowAccessor`'s per-window controllers exist for window *lifecycle*, not for presenting over one.
    private static func keyWindow() -> NSWindow? {
        NSApp.keyWindow ?? NSApp.mainWindow
    }

    private static func refuse(_ session: PrintSession) {
        logger.notice("print refused: the document has no content")
        let alert = NSAlert()
        alert.messageText = "Nothing to Print"
        alert.informativeText = session.options.describesAnyContent
            ? "'\(session.jobTitle)' has no cards with any of the content you chose to include."
            : "Turn on at least one of Title, Icon & Labels, Body or Comments."
        alert.addButton(withTitle: "OK")
        alert.runModal()
    }

    /// **The row's third answer** (`PrintCommand.Scope.refuse`): no board window, no printable card. Same
    /// voice as `refuse(_:)` — "Nothing to Print" is the one title this file's alerts ever wear — and the
    /// same posture: nothing failed, there was simply nothing behind the window to print. This is the
    /// sentence a stray ⌘P now gets instead of falling through to AppKit's "This application does not
    /// support printing" (`PrintCommand`'s doc comment tells the whole story).
    static func refuseNoScope() {
        logger.notice("print refused: no board or card in focus")
        let alert = NSAlert()
        alert.messageText = "Nothing to Print"
        alert.informativeText = "Open a board or a card to print it."
        alert.addButton(withTitle: "OK")
        alert.runModal()
    }

    // MARK: - Finder's half: the `printFiles` Apple Event

    /// One `printFiles` path's answer — the Finder-print counterpart of `PrintCommand.Scope`, and a
    /// separate type from it rather than a reused one because a Finder path can never resolve to `.card`:
    /// Finder hands over folders, never the identity of a card inside one.
    enum FinderPrintTarget {
        case board(BoardModel)
        case refuse
    }

    /// **Board-or-refuse, read straight off disk** — `BoardLoader` succeeding on the path is the whole
    /// test, exactly as `BoardModel`'s own doc comment describes what opens: "`<root>` is the `.kanban`
    /// package (or an extension-less folder — both open)". No extension check, no UTI re-derivation: a path
    /// this app's own loader accepts is a board, whatever it is named, and a path it refuses (a plain file,
    /// a folder with no root `index.md`, one whose `schema` is newer than this build understands) is not.
    ///
    /// A free function of a path rather than folded into `printFiles` itself, for `PrintCommand.resolveScope`'s
    /// own reason one boundary over: this is the part a test can call without handing AppKit a real print job.
    static func resolveFinderPrint(atPath path: String) -> FinderPrintTarget {
        guard let model = try? BoardLoader.load(boardRoot: URL(fileURLWithPath: path)).model else {
            return .refuse
        }
        return .board(model)
    }

    /// **`AppDelegate.application(_:printFiles:withSettings:showPrintPanels:)`'s whole implementation.**
    /// Every path is resolved independently (`resolveFinderPrint`) and, for a path that resolves to a
    /// board, printed **headless** — `BoardLoader`'s snapshot directly, no `BoardStore`, no window, the same
    /// frozen-snapshot posture `printBoard(store:profiles:)` takes from a live one, because this is a
    /// snapshot too: read once, printed once. A path that is not a board refuses politely
    /// (`refuseNoScope()`'s wording) rather than surfacing the loader's own defect UI, which is a board-open
    /// experience this is not.
    ///
    /// `printSettings` is accepted, never read: this app has no `NSDocument` and therefore no per-document
    /// print info for a caller's dictionary to override (`makeOperation`'s own note) — `NSPrintInfo.shared`
    /// is the one paper configuration Finder's print and the in-app one both draw from, which is what keeps
    /// "Print This Board" from Finder and ⌘P on the same board agreeing about margins.
    ///
    /// The reply folds every path's outcome into AppKit's one required answer: any success at all is
    /// `.printingSuccess` (Finder's queue is the honest place to discover a *partial* batch failure, not this
    /// return value), a batch with no successes but at least one non-board path is `.printingFailure`, and a
    /// batch of boards that all cancelled or failed at the operation level is `.printingCancelled` — the same
    /// cancel-or-jam ambiguity `PrintCompletion` already lives with, one layer up.
    static func printFiles(
        _ paths: [String],
        settings printSettings: [NSPrintInfo.AttributeKey: Any],
        showPrintPanels: Bool,
        profiles: PrintProfileStore
    ) -> NSApplication.PrintReply {
        guard !paths.isEmpty else { return .printingFailure }

        var printedAny = false
        var refusedAny = false

        for path in paths {
            switch resolveFinderPrint(atPath: path) {
            case let .board(model):
                if printHeadlessBoard(model, atPath: path, showPrintPanels: showPrintPanels, profiles: profiles) {
                    printedAny = true
                }
            case .refuse:
                refuseNoScope()
                refusedAny = true
            }
        }

        if printedAny { return .printingSuccess }
        return refusedAny ? .printingFailure : .printingCancelled
    }

    /// One board, loaded and printed with no store and no window behind it — `printBoard(store:profiles:)`
    /// read straight off a `BoardModel` instead of a live store, with the comments closure rebuilt against
    /// `CommentThread.load` in place of `BoardStore.commentThread(inCard:)`, since there is no store here to
    /// ask.
    private static func printHeadlessBoard(
        _ model: BoardModel,
        atPath path: String,
        showPrintPanels: Bool,
        profiles: PrintProfileStore
    ) -> Bool {
        let root = URL(fileURLWithPath: path)
        let title = displayName(of: model, atPath: root)

        let session = PrintSession(
            provider: PrintSourceProvider(
                withoutComments: PrintSource.board(model, titled: title),
                withComments: {
                    PrintSource.board(model, titled: title) { card in
                        PrintComment.list(of: commentThread(of: card, inBoardAt: root, snapshot: model))
                    }
                }
            ),
            profiles: profiles,
            cardFolder: nil,
            jobTitle: title,
            boardTitle: title
        )
        guard !session.blocks().isEmpty else {
            refuse(session)
            return false
        }
        return runOperation(makeOperation(for: session, showsPrintPanel: showPrintPanels), session: session)
    }

    /// `AppModel.displayName(of:)`'s reading, off a `BoardModel` rather than a live `BoardStore` — there is
    /// no store on this path to ask, and the rule is the same either way: the board's own `title`, falling
    /// back to the folder name (01-storage-format.md § Board naming).
    private static func displayName(of model: BoardModel, atPath root: URL) -> String {
        if let title = model.title.value, !title.isEmpty { return title }
        return AppModel.folderDisplayName(of: root)
    }

    /// One card's thread, read fresh from disk — `BoardStore.commentThread(inCard:)`'s own reading, off a
    /// bare snapshot: find which lane the card lives in (`BoardStore.boardItem`, a `nonisolated static`
    /// function that needs no store instance), resolve its folder (`ItemPath.card(lane:id:).folder(under:)`),
    /// and read the thread there. `.empty` for a card the snapshot no longer names — unreachable in practice
    /// (the card came from this very snapshot's own walk), kept as the same vanished-target answer every
    /// other card-scoped read gives rather than a force-unwrap.
    private static func commentThread(of card: Card, inBoardAt root: URL, snapshot: BoardModel) -> CommentThread {
        guard let item = BoardStore.boardItem(card.id, in: snapshot), let cardID = item.cardID else {
            return .empty
        }
        let path = "\(item.laneID.rawValue)/\(cardID.rawValue)"
        return CommentThread.load(inCard: ItemPath.card(lane: item.laneID, id: cardID).folder(under: root), path: path)
    }
}

/// The sheeted operation's delegate — **and the only place the Last Used capture can honestly happen.**
///
/// `runModal(for:delegate:didRun:contextInfo:)` presents the panel as a sheet and **returns immediately**,
/// which is the whole reason this class exists: capturing on the line after that call would record the
/// options the panel *opened* with, before the user touched a control. A synchronous capture is only
/// available on the windowless `run()` fallback, which is where `PrintCoordinator` puts one.
///
/// **The gate is `success`, so a cancelled print rewrites nothing.** Cancel and a printer failure are
/// indistinguishable here — both arrive as `success == false` — and of the two possible mistakes the
/// asymmetry is clear: capturing on Cancel would overwrite the user's remembered settings with ones they
/// abandoned, while declining to capture on a jam merely leaves Last Used where it was. So the settings a
/// jammed print was configured with are not remembered; that is the cheaper loss, and the print system's
/// own queue window is where the user retries the job anyway.
///
/// **It retains itself until the callback.** `NSPrintOperation` holds its delegate weakly, and the sheeted
/// form outlives every local in the method that presented it — so the object hands its own reference back
/// only once the callback has landed.
@MainActor
private final class PrintCompletion: NSObject {

    private let session: PrintSession
    private var untilTheCallback: PrintCompletion?

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

    init(session: PrintSession) {
        self.session = session
        super.init()
        untilTheCallback = self
    }

    @objc func printOperationDidRun(_ operation: NSPrintOperation, success: Bool, contextInfo: UnsafeMutableRawPointer?) {
        Self.logger.info("print operation finished — success: \(success, privacy: .public)")
        if success {
            session.profiles.captureLastUsed(session.options)
        }
        untilTheCallback = nil
    }
}
