import Foundation

// MARK: - HistoryPhrase

/// The menu phrase an undo step carries — "Move 3 Cards", "Rename Lane", "Restyle Board".
///
/// ### Why the vocabulary is 06's and not a new one
///
/// 13-native-undo.md ▸ Rules hands the naming straight over: "The 06 vocabulary supplies menu titles
/// ('Undo Move 3 Cards'), via NSUndoManager's dynamic retitling — the same naming machinery both
/// tiers use." So the verbs here are exactly 06-history-undo.md ▸ Commit messages' list — *Add /
/// Delete / Move / Rename / Edit / Restyle / Resize / Reorder over cards, lanes, and the board*, plus
/// the trash pair's **Restore** — and the plural rule is that section's own plural folding ("Delete
/// 12 cards"), which is also 13's coalescing sentence read out loud: "a multi-card move is one step
/// with a plural title".
///
/// The **"Undo "/"Redo " prefix is never here**: the platform composes and localizes it
/// (`BoardUndoManager.undoMenuItemTitle`), and a phrase that spelled it would read "Undo Undo Move
/// Card" in the Edit menu — see `HistoryStep.name`.
///
/// ### Title case, unlike a commit subject
///
/// A commit subject is a sentence ("Move 3 cards to Done"); a menu item is a title, and macOS titles
/// its Edit-menu rows. The words are 06's; the casing is the menu's. Nothing else differs — and the
/// destination clause a commit subject carries has no place in a title that has to stay short enough
/// for a menu row.
///
/// **One deliberate exception, and it is the only one**: `cardSession(_:)` names its card
/// ("Changes to 'Fix login'"), because the coarse close step is the one phrase whose *scope* is what
/// distinguishes it — see that member.
///
/// Pure, and its own type rather than a `String` built at each call site, because a phrase composed
/// in eleven places is a vocabulary that drifts in eleven places.
public enum HistoryPhrase {

    // MARK: Verbs

    /// 06-history-undo.md ▸ Commit messages' verb list, restricted to the operations 13 makes
    /// undoable. `Permanently delete`, `Attach`, `Remove` and `Repair` are deliberately absent —
    /// those are exactly the operations that register no step at all (13 ▸ Rules ▸ what is not
    /// undoable, ▸ Out of scope).
    public enum Verb: String, Sendable, CaseIterable {
        /// A create — File ▸ New Lane, the new-card placeholder's commit, a Finder file drop's cards.
        case add = "Add"
        /// A delete — ⌫, drop-on-trash, the card window's Actions ▸ Delete — which is a *move* into
        /// `.trash/` for a card and a physical removal for a lane (03-board-ui.md § Trash).
        ///
        /// **There is no `restore` verb**: restoring is an ordinary move out (drag or ⌘X/⌘V), so it
        /// registers as `move` like any other, and Put Back is retired with the tombstone model
        /// (resettled 2026-07-28).
        case delete = "Delete"
        /// A drop that changes an item's parent.
        case move = "Move"
        /// A drop, a lane drag or ⌥⌘↑/⌥⌘↓ that changes rank among unchanged siblings.
        case reorder = "Reorder"
        case rename = "Rename"
        case restyle = "Restyle"

        /// A card's `labels` list rewritten — the sidebar's chips and the context menu's submenu
        /// (`FrontmatterKeys.labels`, activated 2026-08-09; `BoardStore.setLabels(_:onCard:on:)`).
        ///
        /// **Its own verb rather than `restyle`**, on `collapse`'s reasoning: the row has to read back
        /// the gesture it undoes, and "Undo Restyle Card" after clicking a label would name a control
        /// the user never touched. The word is 06-history-undo.md ▸ Commit messages' own for a change
        /// to this key — the composer has said "Relabel card 'X'" since before there was a control to
        /// press (`ChangeNarrator`) — so the menu row and the change journal now speak the same one.
        ///
        /// **One verb for both directions**, unlike `collapse`/`expand`: adding and removing a label
        /// are the same control pressed twice, where collapsing and expanding are two menu rows with
        /// two words.
        case relabel = "Relabel"
        case resize = "Resize"
        /// A lane folded to its slim strip (03-board-ui.md § Lane ▸ Collapsed lanes).
        ///
        /// **Two verbs rather than one**, unlike `resize`, which covers growing and shrinking alike:
        /// the row has to read back the gesture it undoes, and "Undo Resize Lane" after *Collapse
        /// Lane* would name a control the user never touched. The trash pair's Delete/Restore split is
        /// the precedent one level up.
        case collapse = "Collapse"
        case expand = "Expand"
        /// An Edit session's body save — one step at the Edit→Preview flip (13 ▸ Rules).
        case edit = "Edit"
    }

    // MARK: Nouns

    /// What the gesture acted on. `board` is deliberately count-less: there is one board, and
    /// "Rename 1 Board" is not a phrase anyone writes.
    public enum Kind: Sendable, Equatable {
        case card
        case lane
        case board
        /// One comment. `.delete` and `.edit` reach it — the inline edit session joined the
        /// vocabulary with the window stack (re-ruled 2026-07-31: "every gesture issued in that
        /// window — comment post/delete/**edit**, body Edit sessions ... registers there at fine
        /// grain"), registered at its own commit point exactly as the body's session is. Posting has
        /// its own phrase (`comment`, below); the draft save and the trash purge still register no
        /// step at all (13-native-undo.md — the permanent-delete posture).
        case comment

        var singular: String {
            switch self {
            case .card: "Card"
            case .lane: "Lane"
            case .board: "Board"
            case .comment: "Comment"
            }
        }

        var plural: String {
            switch self {
            case .card: "Cards"
            case .lane: "Lanes"
            case .board: "Board"
            case .comment: "Comments"
            }
        }
    }

    // MARK: The comment family

    /// **The post's phrase** — 06-history-undo.md's path-shaped "Comment on '⟨card⟩'" as a menu
    /// title, which is the verb on its own.
    ///
    /// Not `name(_:kind:count:)` with a `Verb.comment`, because the composition would read "Comment
    /// Comment": here the verb already names its object, which is `Kind.board`'s count-less rule
    /// arriving from the other direction. The destination clause a commit subject carries ("on 'Fix
    /// login'") is dropped exactly as every other phrase drops it — a menu row has to stay short.
    public static let comment = "Comment"

    // MARK: The card-window session

    /// **The coarse close step's phrase** — one card window's whole session, as the board's stack sees
    /// it: **"Changes to '⟨card⟩'"** (13-native-undo.md ▸ Rules, ruled 2026-07-31).
    ///
    /// > "the session's net effect registers on the board stack as **one coarse step named
    /// > "Changes to '⟨card⟩'"** … the board row reads "Undo Changes to 'Fix login'": plural and
    /// > scope-flavoured, distinct from every fine verb, honest about folding many kinds; the fine
    /// > body-edit wording never leaks onto the board menu."
    ///
    /// ### Why this one phrase carries its item
    ///
    /// Every other phrase here drops the item clause a commit subject carries, because a verb plus a
    /// noun already says what the row is (the type's note). This step has no such verb: what it folds
    /// is a comment posted, a colour chosen, a paragraph rewritten, all at once, and a row that named
    /// any one of them would be lying about the other two — while enumerating them would be the
    /// "Mixed update" problem in a menu (06-history-undo.md ▸ Commit messages). So the *scope* is the
    /// phrase, and a scope is only legible when it names what it is the scope **of**.
    ///
    /// ### It is not `Edit Card`, and that is the point
    ///
    /// This member read `name(.edit, kind: .card)` until the ruling, and the fine body-edit step still
    /// does (`registerBodyEdit`) — two different steps on two different stacks, one row apart in the
    /// Edit menu, saying the same six characters. The board menu now says "Changes to 'Fix login'" and
    /// the window menu says "Edit Card": the coarse row names the session, the fine row names the
    /// gesture, and neither can be mistaken for the other.
    ///
    /// - Parameter title: the card's title **as the step is registered** — `nil` renders the same
    ///   placeholder the card's own window title bar renders ("Untitled" is a rendering, never a
    ///   value — 03-board-ui.md § Card face), so a menu row and the window it came from name the card
    ///   the same way.
    public static func cardSession(_ title: String?) -> String {
        "Changes to '\(title ?? "Untitled")'"
    }

    // MARK: Composition

    /// The phrase for one gesture: `"Move Card"`, `"Move 3 Cards"`, `"Restyle Board"`.
    ///
    /// A `count` of one or less folds to the singular — a batch that turned out to name a single item
    /// is one item, and a zero never reaches here because a gesture that wrote nothing registers
    /// nothing.
    public static func name(_ verb: Verb, kind: Kind, count: Int = 1) -> String {
        guard count > 1, kind != .board else { return "\(verb.rawValue) \(kind.singular)" }
        return "\(verb.rawValue) \(count) \(kind.plural)"
    }
}
