import AppKit

// MARK: - Identifiers

extension NSToolbarItem.Identifier {
    static let cardEditBody = Self("card.editBody")
    static let cardRawSource = Self("card.rawSource")
    static let cardAddAttachment = Self("card.addAttachment")
    static let cardShowSidebar = Self("card.showSidebar")
    static let cardDeleteCard = Self("card.deleteCard")
    static let cardRevealInFinder = Self("card.revealInFinder")
}

// MARK: - The card window's toolbar

/// The card window's toolbar (03-board-ui.md ▸ Toolbar; 05-card-window.md ▸ Window).
///
/// "**Card window default: Edit Body · Raw Source · Add Attachment** — the window's three committed
/// functions, all discoverable from its toolbar." **Delete Card joined them** (05 ▸ Actions, retired —
/// Pipeline card bcd3b323): the sidebar's Actions section is gone, and its destructive Delete moved here
/// (separated by a `.flexibleSpace` from the creation-and-view items ahead) — HIG's "destructive actions
/// read as separate from the everyday cluster", `Mail.app`'s own toolbar Delete being the nearest system
/// precedent for a one-click, no-confirm, recoverable-by-trash button. **Show Sidebar joined them too**
/// (05-card-window.md ▸ Composition, the toggle added beside the toolbar-customization work), positioned
/// rightmost as the conventional macOS place for a trailing-sidebar toggle — the `Show Trash` precedent
/// applied to the card window's one collapsible pane. **Reveal in Finder joined the catalog** the same
/// day, catalog-only — it already has a menu-bar twin with no default chord (`RevealInFinderCommand`),
/// so nothing was unreachable before this; the toolbar item is Customize's shortcut to it, not its only path.
///
/// ### The six items are six menu rows, predicates included
///
/// - **Edit Body** is "a single toggle button (on-state in Edit — mirroring the View ▸ Edit Body
///   checkmark)", and it disables while source mode is active. That clause is not restated here: the
///   predicate is `EditBodyCommand.isEnabled(body:rawSource:)`, the menu row's own, handed to the
///   item verbatim. The pathfinder's segmented Preview|Edit is retired, so this is one button.
/// - **Raw Source** is "likewise a toggle showing on-state", and its two directions are not
///   symmetric: on enters, off *applies* (`CardRawSourceSession.applyAndLeave`). A refused Apply
///   leaves source mode open, and the item's on-state simply fails to clear — the same way the menu
///   row's checkmark does, because both read `isActive`.
/// - **Add Attachment** "stays enabled in every mode — attachment operations never touch
///   `index.md`, so they're safe alongside a raw edit". Its predicate is likewise the row's own
///   (`AddAttachmentCommand.isEnabled`), which is scope plus the read-only lock and says nothing
///   about the body's mode.
/// - **Show Sidebar** is a toggle showing on-state too, mirroring the View ▸ Show Sidebar checkmark
///   (`ShowSidebarCommand`) — and it is the one item here with no window-scoped handle behind it:
///   its predicate reads `AppPreferences.showCardSidebar` straight off `UserDefaults`, the same
///   app-wide, persisted bit `CardWindowView` renders from and every card window's toolbar answers
///   to alike. Always enabled, `ShowCommentsCommand`'s own posture: showing or hiding a pane is not a
///   mutation, so the read-only lock has no say in it.
/// - **Delete Card** is a push button carrying `trash`, mirroring **File ▸ Delete Card**
///   (`DeleteCardCommand`) — a new, distinctly titled row (never "Delete": 11-command-nexus.md files
///   that title as the board-scope ⌘⌫ row's own singleton). Its predicate is the sidebar button's own
///   read-only check (`CardWindowActions.isDeletable`), and firing it calls `BoardStore.deleteCard(_:)`
///   directly — the same write, same bracket, same stamps ⌫ and the sidebar button always used. **No
///   confirmation**, matching the delete flow it replaces exactly: recovery is the board's trash lane,
///   so the alert 03 reserves is for the permanent purge, not this move.
/// - **Reveal in Finder** mirrors the existing card-window branch of `RevealInFinderCommand` — same
///   computation (`CardAttachments.revealURLs`), same `NSWorkspace` call, so the toolbar item and the
///   menu row can never disagree about what "the selected attachment, or the card's folder" means.
///
/// Labels are the menu titles minus a trailing ellipsis, so File ▸ Add Attachment… labels as **Add
/// Attachment** (03's own example).
@MainActor
enum CardToolbar {

    static let identifier = "dev.rzen.indie.Kanban.card"

    /// The trio plus Show Sidebar plus Delete Card — the whole catalog except Reveal in Finder, which
    /// is catalog-only (already reachable off its own menu row, no default chord). The trailing
    /// `.flexibleSpace` separates the creation-side defaults from Delete Card, and Show Sidebar sits
    /// rightmost (the conventional macOS position for a trailing-sidebar toggle).
    static let defaultItems: [NSToolbarItem.Identifier] = [
        .cardEditBody,
        .cardRawSource,
        .cardAddAttachment,
        .flexibleSpace,
        .cardDeleteCard,
        .cardShowSidebar,
    ]

    /// - Parameters:
    ///   - actions: Delete Card's window-scoped handle (`CardWindowActions`) — the same object
    ///     `DeleteCardCommand` reads through the focus system, handed here directly since the toolbar
    ///     is installed once at window-open time rather than reactively (`CardWindowHost
    ///     .configureWindow`).
    ///   - isSidebarShown: Show Sidebar's read, defaulted to the real bit
    ///     (`AppPreferences.showCardSidebar`). Injectable so a test can drive the item without
    ///     touching the developer's own `UserDefaults.standard` domain — `StyleRecents`' own caution,
    ///     applied to the one item here with no window-scoped handle to hold a scratch value instead.
    ///   - setSidebarShown: Show Sidebar's write, defaulted to the real setter
    ///     (`AppPreferences.setShowCardSidebar`), for the same reason.
    static func specs(
        body: CardBodyPresentation,
        rawSource: CardRawSourceSession,
        attachments: CardAttachments,
        actions: CardWindowActions,
        isSidebarShown: @escaping () -> Bool = { AppPreferences.showCardSidebar },
        setSidebarShown: @escaping (Bool) -> Void = { AppPreferences.setShowCardSidebar($0) }
    ) -> [ToolbarItemSpec] {
        [
            .mirroring(
                menuTitle: "Edit Body",
                identifier: .cardEditBody,
                symbol: "square.and.pencil",
                behavior: .toggle(
                    isEnabled: { [weak body, weak rawSource] in
                        EditBodyCommand.isEnabled(body: body, rawSource: rawSource)
                    },
                    isOn: { [weak body] in body?.mode == .edit },
                    setOn: { [weak body] isOn in body?.setMode(isOn ? .edit : .preview) }
                )
            ),
            .mirroring(
                menuTitle: "Raw Source",
                identifier: .cardRawSource,
                symbol: "doc.plaintext",
                behavior: .toggle(
                    isEnabled: { [weak rawSource] in rawSource != nil },
                    isOn: { [weak rawSource] in rawSource?.isActive == true },
                    setOn: { [weak rawSource] isOn in
                        guard let rawSource else { return }
                        if isOn {
                            rawSource.enter()
                        } else {
                            rawSource.applyAndLeave()
                        }
                    }
                )
            ),
            .mirroring(
                menuTitle: "Add Attachment…",
                identifier: .cardAddAttachment,
                symbol: "paperclip",
                behavior: .button(
                    isEnabled: { [weak attachments] in AddAttachmentCommand.isEnabled(attachments) },
                    perform: { [weak attachments] in attachments?.add() }
                )
            ),
            // **The one item here with no window-scoped handle** — its read and write are the two
            // injected closures above, defaulted to `AppPreferences.showCardSidebar` /
            // `.setShowCardSidebar`, the plain `UserDefaults`-backed bit every card window's sidebar
            // (`CardWindowView`) and View ▸ Show Sidebar row (`ShowSidebarCommand`) already read and
            // write the same way. `sidebar.right` is the trailing variant: the sidebar this toggles
            // sits on the window's trailing edge (05-card-window.md ▸ Composition), never the leading
            // one the plain `sidebar.left` glyph would imply.
            .mirroring(
                menuTitle: "Show Sidebar",
                identifier: .cardShowSidebar,
                symbol: "sidebar.right",
                behavior: .toggle(isEnabled: { true }, isOn: isSidebarShown, setOn: setSidebarShown)
            ),
            // **The sidebar's former Actions ▸ Delete** (05 ▸ Actions, retired) — same write, same
            // no-confirmation posture: recovery is the board's trash lane, so there is nothing here
            // for an alert to guard. Placed last, after the trailing `.flexibleSpace` in
            // `defaultItems`, so it never sits adjacent to the four creation/view items ahead of it.
            .mirroring(
                menuTitle: "Delete Card",
                identifier: .cardDeleteCard,
                symbol: "trash",
                behavior: .button(
                    isEnabled: { [weak actions] in DeleteCardCommand.isEnabled(actions) },
                    perform: { [weak actions] in actions?.deleteCard?() }
                )
            ),
            // **The sidebar's former Actions ▸ Reveal in Finder** (05 ▸ Actions, retired) — catalog
            // only, since it already has a menu-bar twin with no default chord
            // (`RevealInFinderCommand`); Customize is this button's whole reason to exist, not its
            // only path. The computation is that row's card-window branch, verbatim, so the two
            // surfaces can never disagree about what "the selected attachment, or the card's folder"
            // means.
            .mirroring(
                menuTitle: "Reveal in Finder",
                identifier: .cardRevealInFinder,
                symbol: "folder",
                behavior: .button(
                    isEnabled: { [weak attachments] in
                        guard let attachments else { return false }
                        return !Self.revealURLs(attachments).isEmpty
                    },
                    perform: { [weak attachments] in
                        guard let attachments else { return }
                        NSWorkspace.shared.activateFileViewerSelecting(Self.revealURLs(attachments))
                    }
                )
            ),
        ]
    }

    /// `RevealInFinderCommand`'s own card-window branch, read here rather than re-derived — the
    /// selected attachment's file when the section has the keyboard, the card's folder otherwise
    /// (`CardAttachments.revealURLs`'s own rule).
    private static func revealURLs(_ attachments: CardAttachments) -> [URL] {
        CardAttachments.revealURLs(
            cardFolder: attachments.cardFolder,
            selectedURL: attachments.selectedURL,
            isSectionFocused: attachments.isFocused
        )
    }

    static func controller(
        body: CardBodyPresentation,
        rawSource: CardRawSourceSession,
        attachments: CardAttachments,
        actions: CardWindowActions,
        isSidebarShown: @escaping () -> Bool = { AppPreferences.showCardSidebar },
        setSidebarShown: @escaping (Bool) -> Void = { AppPreferences.setShowCardSidebar($0) }
    ) -> WindowToolbarController {
        WindowToolbarController(
            identifier: identifier,
            specs: specs(
                body: body,
                rawSource: rawSource,
                attachments: attachments,
                actions: actions,
                isSidebarShown: isSidebarShown,
                setSidebarShown: setSidebarShown
            ),
            defaults: defaultItems
        )
    }
}
