import Foundation

// MARK: - HistoryDirection

/// Which command the user pressed — ⌘Z or ⇧⌘Z.
///
/// **Not which half of a step is running.** A step that has already been undone goes onto the redo
/// stack *reversed* (`HistoryStep.reversed`), so the closure ⇧⌘Z crosses is the one registered as
/// `redo` and the closure a second ⌘Z crosses is the one registered as `undo` — the halves swap, and
/// a step cannot tell which it is by looking at itself. What it has to be told is the **command**,
/// because the one sentence a step ever says out loud names it: "Undo skipped — 'Fix login' changed
/// outside Lanework" (13-native-undo.md ▸ Rules), with "Redo skipped" as its mirror.
public enum HistoryDirection: Sendable, Equatable {
    case undo
    case redo

    /// The stack a step lands on once it has been crossed this way.
    public var opposite: HistoryDirection {
        self == .undo ? .redo : .undo
    }
}

// MARK: - HistoryStepOutcome

/// What happened when a step was asked to walk its write back (or forward again).
///
/// The three cases are three different fates for the *stack*, which is the only thing the provider
/// asks about:
///
/// - `.applied` — the write landed; the step's mirror image joins the opposite stack.
/// - `.skipped` — **stale** (13-native-undo.md ▸ Rules ▸ staleness validation): "target folder gone,
///   or the field no longer holding the step's after-value → the step is **skipped, not applied**:
///   popped from the stack ... and ⌘Z falls through to the next step". The step is dropped and the
///   crossing continues.
/// - `.failed` — the inverse was attempted and could not be written. The step **stays**, and the
///   crossing stops (see the case's own note).
///
/// The provider reads this answer and nothing else — the *predicate* (field-level, settled) and the
/// info-tone banner that explains a skip both belong to the step, which is the only side that knows
/// what it wrote and which board to say it on.
public enum HistoryStepOutcome: Equatable, Sendable {

    /// The step ran. Its mirror image joins the opposite stack.
    case applied

    /// The step declined: the board no longer holds the value this step's write left, so applying
    /// its inverse would clobber somebody else's newer edit. Nothing ran, the step is dropped, and
    /// the crossing continues with the next one down.
    case skipped

    /// The step tried and could not: the Writer refused the inverse (a disk error, a permission
    /// problem, a board that has gone read-only under the stack). Nothing landed.
    ///
    /// **The step stays put and the crossing stops** — the one thing that distinguishes this from
    /// `.skipped`, and the reason the case exists. 13 is silent here, so the posture is the honest
    /// reading of its two rules: a *stale* step is one the board has moved past, so dropping it
    /// loses nothing; a *failed* one is a step the user still means to cross, refused by a condition
    /// that is usually momentary (a full disk, an unplugged volume), so popping it would spend their
    /// only route back on a transient error. The failure has already banners itself as an ordinary
    /// write failure (`BoardStore.performWrite` posts before it rethrows), which is exactly the
    /// vocabulary 02-architecture.md § Write-failure surfacing gives it — and which is why a failure
    /// must *not* also raise the info-tone skip row: two rows for one event would say the step is
    /// both gone and retryable.
    ///
    /// Stopping rather than falling through follows from the same reading: fall-through exists to
    /// walk past steps the board no longer has a use for, and a disk that just refused one write is
    /// not a reason to attempt N more.
    case failed
}

// MARK: - HistoryStep

/// One undoable step: a name, and the pair of actions that walk the board backwards and forwards
/// across it.
///
/// ### An operation pair, not an `NSUndoManager` registration
///
/// 13-native-undo.md ▸ Rules puts every undoable change at the Writer boundary — "each Writer call
/// site registers the inverse operation, computed from the pre-write snapshot the store already
/// holds: move → move back ...; rename → restore title" — and both halves of that write are already
/// in the caller's hands: the before-value *is* the inverse, and the after-value is what the write
/// set (which is also what the staleness predicate compares). A step is therefore that pair, in the
/// design's own terms, and deliberately says nothing about how a stack stores it: the free tier's
/// stack is `NSUndoManager`-backed and Pro's is git (12-editions.md ▸ The provider seam), and neither
/// substrate appears here.
///
/// ### `name` is the 06 vocabulary, unprefixed
///
/// "The 06 vocabulary supplies menu titles ('Undo Move 3 Cards'), via NSUndoManager's dynamic
/// retitling — the same naming machinery both tiers use" (13). What the step carries is the bare
/// phrase — `"Move Card"`, `"Move 3 Cards"`, `"Rename Lane"` — in the vocabulary of
/// 06-history-undo.md ▸ Commit messages, plural-folded by the same rule ("one gesture, one undo
/// step — a multi-card move is one step with a plural title"). The **"Undo "/"Redo " prefix is
/// never part of it**: the platform composes and localizes that (`BoardUndoManager`), and a step
/// that spelled it would read "Undo Undo Move Card" in the Edit menu.
///
/// ### Both closures are `@MainActor`, and both are handed the direction
///
/// Everything they touch — the store, the snapshot, the banners — is `@MainActor`, and a step exists
/// to be run from a menu command. Marking them says so at the seam instead of leaving each provider
/// to rediscover it.
///
/// The `HistoryDirection` argument is **which command the user pressed**, not which half is running:
/// `reversed` swaps the two closures, so the half a ⌘Z crosses is the `redo` one as often as not,
/// and the skip sentence has to name the keystroke rather than the half (see `HistoryDirection`).
public struct HistoryStep {

    /// The menu phrase, unprefixed — see the type's note.
    public let name: String

    /// Walks the board back across this step. Registered at the Writer boundary as the *inverse* of
    /// the write that just landed.
    public let undo: @MainActor (HistoryDirection) -> HistoryStepOutcome

    /// Walks it forward again — the original write, replayed. Reached only after `undo` applied,
    /// because that is the only way a step reaches the redo stack.
    public let redo: @MainActor (HistoryDirection) -> HistoryStepOutcome

    public init(
        name: String,
        undo: @escaping @MainActor (HistoryDirection) -> HistoryStepOutcome,
        redo: @escaping @MainActor (HistoryDirection) -> HistoryStepOutcome
    ) {
        self.name = name
        self.undo = undo
        self.redo = redo
    }

    /// The same step read backwards — what a provider puts on the opposite stack once this one has
    /// applied. The name does not change, which is the whole of "Undo Move Card" becoming "Redo Move
    /// Card": the phrase names the *gesture*, not the direction.
    public var reversed: HistoryStep {
        HistoryStep(name: name, undo: redo, redo: undo)
    }
}

// MARK: - HistoryProviding

/// The undo/redo substrate, behind one protocol boundary (12-editions.md ▸ The provider seam).
///
/// ### One per board session
///
/// "One stack per board, owned by the board session. Not per-window: every window over a board
/// (board window, its card windows) shares the store and shares the stack" (13-native-undo.md
/// ▸ Rules). `AppModel.BoardSession` is where that ownership lives, and the composition root binds
/// which implementation it gets: the free tier binds `NativeHistoryProvider` (two step stacks over
/// the inverses registered at the Writer boundary), Pro binds the git provider in pro-m1 (undo as
/// forward restore commits over HEAD's first-parent ancestry — 06-history-undo.md), Teams inherits
/// Pro's.
///
/// ### What this protocol deliberately does not say
///
/// - **No `NSUndoManager`, anywhere in the signature.** It is the native provider's implementation
///   detail, and a seam that vended one would be a seam only one provider could ever satisfy — the
///   opposite of the reason the seam exists at all ("the free tier's native undo is the first proof
///   the seam is real", 12). AppKit still needs an `UndoManager` to hand the responder chain; that
///   adapter is `BoardUndoManager`, which sits *over* this protocol rather than inside it.
/// - **No persistence promise.** The native stack dies with the session (13); Pro's survives
///   relaunch because git does (06). Both are honest implementations of these seven members.
/// - **No routing.** Which surface ⌘Z reaches is focus's answer, not the substrate's
///   (06 ▸ Undo routing, tier-independent) — `BoardUndoRouting`.
@MainActor
public protocol HistoryProviding: AnyObject {

    /// Records one undoable step, on top of the undo stack, clearing the redo stack — the classic
    /// rule, and the one every substrate shares.
    ///
    /// Called once per *gesture*, never once per write: "coalescing follows commit granularity ...
    /// a multi-card move is one step with a plural title; an Edit session is one step, registered at
    /// the Edit→Preview flip" (13 ▸ Rules).
    func register(_ step: HistoryStep)

    /// Whether there is a step to cross. What the Edit menu's Undo row and the toolbar's Undo item
    /// enable on, through the same responder-chain answer.
    var canUndo: Bool { get }

    var canRedo: Bool { get }

    /// The name of the step ⌘Z would cross, or `nil` when there is none — the phrase the menu title
    /// is composed from ("Move 3 Cards" → "Undo Move 3 Cards").
    var undoActionName: String? { get }

    var redoActionName: String? { get }

    /// Crosses one step backwards. A stale step is skipped rather than applied, and the crossing
    /// falls through to the next one (13 ▸ Rules ▸ staleness validation); a step whose write *failed*
    /// stays where it is and stops the crossing (`HistoryStepOutcome.failed`); an empty stack does
    /// nothing.
    func undo()

    func redo()

    /// Drops every step in both directions — session-only persistence (13 ▸ Rules), run at the board
    /// session's teardown. Also what a substrate that must re-seed (a branch switch, 06) calls first.
    func clear()
}
