import Foundation

// MARK: - CardWindowUndo

/// **One card window's undo session** — the second of 13-native-undo.md's two levels (re-ruled
/// 2026-07-31, the session-coarsening model).
///
/// > "the **board stack** is owned by the board session and shared by board surfaces; a **card window
/// > owns its own stack** for the session it represents — every gesture issued in that window
/// > (comment post/delete/edit, body Edit sessions, style/details changes, attachment ops where
/// > undoable) registers there at fine grain, and `window.undoManager` answers with it (standard
/// > per-window AppKit scoping)."
///
/// ### Two jobs, and the second is why this is a type rather than a stored provider
///
/// **The fine stack**: an ordinary `NativeHistoryProvider`, on *every* board. The steps a card window
/// registers are values-based inverses at the Writer boundary — the same shape whatever substrate the
/// board's own history has. (Once only the *coarse* close unit split by substrate — "one native board
/// step, or one commit" — 13-native-undo.md ▸ Undo routing; the split was written as a tier split and
/// became a board split with the 2026-08-07 pivot before the 2026-08-08 excision removed the git half
/// of it outright, `strategy/01-git-excision.md`. Nothing in this type ever read either axis.)
///
/// **The fold**: window close registers "one coarse step ... whose undo restores the card subtree to
/// its session-start state ... and whose redo reapplies the net effect". That net effect is exactly
/// the steps still on this stack, composed — see `netEffect()`.
///
/// ### Why the session-start capture is distributed rather than a subtree snapshot
///
/// A single snapshot of the card folder taken at window open, diffed against disk at close, was the
/// obvious shape and is the wrong one: it would sweep in **every** change to the card during the
/// window's life, an agent's included, and 13 ▸ Rules is explicit that "foreign writes never join the
/// stack". A disk diff cannot tell the user's gesture from somebody else's write; a stack of the
/// user's gestures never has to.
///
/// So the capture lives where it already lived — each fine step carries the value its write
/// overwrote (`CardBodyEditSession`'s session-origin bytes, `CommentEditSession.sessionStart`, the
/// prior style fields) — and the coarse step is their composition. That also makes the coarse step's
/// *after*-values the values the app itself wrote, so a foreign edit landing between a gesture and
/// the close makes the step stale (13's field-level predicate) instead of being quietly reverted.
///
/// ### The values are anchored to the card, not to its folder
///
/// "Session steps anchor by card identity, never by path" (13 ▸ Rules, ruled 2026-07-31): every write
/// folded here names the card's UUID and the thread position inside it (`HistoryAnchor`), never the
/// lane the card happened to be in when the gesture ran. The fold is therefore over identities the
/// whole way down, and one board-side lane move — which used to stale every component of the coarse
/// step at once — changes nothing about it.
@MainActor
public final class CardWindowUndo {

    // MARK: The fine stack

    /// This window's own steps. `NativeHistoryProvider` verbatim: the grammar a window needs — one
    /// register is one step, a stale step is skipped and falls through, a failed one stays — is the
    /// grammar that type already implements and this milestone had no reason to fork.
    public let stack = NativeHistoryProvider()

    /// Whether the board is refusing writes, wired by the host once the window has joined its board
    /// (`BoardUndoManager.isReadOnly`'s closure, one level down): the lock disables Undo and Redo in
    /// a card window exactly as it does on the board (13 ▸ Rules ▸ locks).
    public var isReadOnly: @MainActor () -> Bool = { false }

    /// **What this window hands back from `windowWillReturnUndoManager`** — the AppKit face over the
    /// stack above, so the Edit menu's rows light up, disable and retitle from *this window's*
    /// gestures.
    ///
    /// There is deliberately **no fall-through**: this manager never consults the board's stack, so
    /// "exhausting the window stack beeps; it never reaches board history" (06 ▸ Undo routing) is a
    /// property of what the window answers with rather than a rule someone has to remember to apply.
    public private(set) lazy var manager: BoardUndoManager = BoardUndoManager(
        history: stack,
        isReadOnly: { [weak self] in self?.isReadOnly() ?? false }
    )

    // MARK: The writes behind the steps

    /// One gesture's write, in the raw terms a fold needs: the two halves and what each of them
    /// leaves behind.
    ///
    /// It is the *unwrapped* pair — `BoardStore.registerStep`'s arguments before that method wraps
    /// them in validation and a `performWrite` bracket. The coarse step has to compose the writes
    /// themselves, because a composition of wrapped steps would validate and bracket each component
    /// separately, which is precisely the partial session revert 13 forbids ("any stale component
    /// skips the whole step — never a partial session revert").
    struct Write {
        let undoExpects: [HistoryExpectation]
        let redoExpects: [HistoryExpectation]
        let undo: @MainActor (BoardStore) throws -> Void
        let redo: @MainActor (BoardStore) throws -> Void
    }

    /// The raw writes, keyed by the step they belong to. Keyed rather than appended so that a gesture
    /// undone inside the window drops out of the fold for free: membership is `stack.pendingSteps`',
    /// and this is only the lookup.
    private var writes: [UUID: Write] = [:]

    public init() {}

    /// Records the raw write behind a step this window is about to register. Called by
    /// `BoardStore.registerStep`, which is the one place both halves are in hand.
    func record(_ id: UUID, _ write: Write) {
        writes[id] = write
    }

    /// **Empties this window's fine stack, registering nothing** — the branch switch's settle step
    /// (06-history-undo.md ▸ Branch switching, ruled 2026-07-31).
    ///
    /// > "The settle also clears each open card window's fine undo stack: pre-switch steps describe
    /// > the branch being left — Save All and Discard alike end with every window's stack empty, the
    /// > board-stack discard-and-reseed precedent one level down; the windows stay open, following
    /// > their cards onto the new branch with fresh stacks."
    ///
    /// ### Closing the stack is not closing the window
    ///
    /// This is the whole reason it is a method rather than a call to `stack.clear()` at the call site.
    /// A window *close* owes the board one coarse step folded from this stack
    /// (`BoardStore.registerCardSession`, `netEffect()`); a settle clear owes it nothing, because the
    /// window is not closing — it stays open on the other side of the checkout and its next gesture
    /// starts a fresh stack. So nothing here registers, nothing folds, and the fold that runs at the
    /// window's eventual close sees only what the *new* branch's session did: `netEffect()` reads
    /// `stack.pendingSteps`, which this empties, and `registerCardSession` answers `false` over an
    /// empty fold exactly as it does for a session with no net change.
    ///
    /// The raw writes go with the steps. They are keyed by step id and nothing on either stack names
    /// them any more, so keeping them would be a table that only grows — and a redone step's half of
    /// the fold, which is the one reason the table outlives the undo stack, has nothing left to be
    /// half of once both stacks are empty.
    ///
    /// **Nothing retires here that would not have retired anyway.** `NativeHistoryProvider.clear()`
    /// runs each dropped step's retirement, and a window's fine steps carry none: the deferred
    /// `comments/.trash/` purge is the *coarse* step's (13-native-undo.md ▸ Interaction with the
    /// trash), registered on the board's stack at close and never on this one.
    func discardSteps() {
        stack.clear()
        writes.removeAll()
    }

    // MARK: - The fold

    /// **The session's net effect, or `nil` when there is none** — what the window's close registers
    /// on the board stack as one coarse step, and what "a session with no net change registers
    /// nothing" means in code.
    ///
    /// ### The composition
    ///
    /// - **undo** — every live step's undo, newest first. Replaying the session backwards lands on the
    ///   state it started from, deleted comments included: their backing is still in
    ///   `comments/.trash/` because this step's own existence is what defers the purge — and, since
    ///   the sweep gate (13 ▸ Interaction with the trash, ruled 2026-07-31), what keeps the *next*
    ///   window open from sweeping it as residue. That claim needs nothing of its own here: the fold
    ///   below carries each delete's `.present(.trashedComment(…))` into the coarse step's
    ///   expectations, and `HistoryStep.backing(declaredBy:)` reads it straight off them.
    /// - **redo** — every live step's redo, oldest first. The session, replayed.
    /// - **the undo's expectations** — the state the session's writes left, folded **last-write-wins**
    ///   per field: what must still be true for the whole step to be safe to cross.
    /// - **the redo's expectations** — the state the coarse undo leaves, folded **first-write-wins**:
    ///   the mirror, for the same reason.
    ///
    /// One `HistoryStep` carrying every component's expectations is what makes validation
    /// transactional without a word of new machinery: `BoardStore.cross` already checks the whole list
    /// before writing anything, so any stale component skips the whole step with the ordinary
    /// info-tone banner.
    ///
    /// ### "No net change" is an equality, not a step count
    ///
    /// A body typed away and typed back across two Edit sessions is two steps whose folded before and
    /// after say the same thing, and 13 says that session registers nothing. Comparing the two folds
    /// is the whole test — and it is exact, because both sides are the values the app itself wrote.
    func netEffect() -> Write? {
        // Every step on this stack was recorded here as it was registered, and nothing is ever
        // removed — a step undone inside the window may still be redone, so its write has to survive
        // being off the undo stack. The table is therefore complete by construction and dies with the
        // window; pruning it against one stack would silently empty a redone step's half of the fold.
        let live = stack.pendingSteps.compactMap { writes[$0.id] }
        guard !live.isEmpty else { return nil }

        let after = Self.fold(live.map(\.undoExpects))
        let before = Self.fold(live.reversed().map(\.redoExpects))
        guard after != before else { return nil }

        return Write(
            undoExpects: after.expectations,
            redoExpects: before.expectations,
            undo: { store in
                for write in live.reversed() { try write.undo(store) }
            },
            redo: { store in
                for write in live { try write.redo(store) }
            }
        )
    }

    // MARK: Folding

    /// The merged expectations of a sequence of writes, **later entries winning** — so folding a list
    /// in registration order yields the state the last write left, and folding it reversed yields the
    /// state the first write found.
    ///
    /// Merging is per target *and* per field: two style gestures that set different dimensions of one
    /// card fold to one target carrying both, while two that set the same dimension fold to one value.
    /// Presence is whole-target and takes the later answer, which is what makes a post-then-delete of
    /// one comment fold to "in the trash" rather than to two contradictory claims.
    ///
    /// **A target is an anchor, not a path** (13 ▸ Rules, ruled 2026-07-31): every write folded here
    /// is a card window's, so every anchor is a card identity, and the fold is over identities the
    /// whole way down — which is what carries "session steps anchor by card identity" into the coarse
    /// step without the fold knowing anything about it.
    static func fold(_ lists: [[HistoryExpectation]]) -> Fold {
        var fold = Fold()
        for list in lists {
            for expectation in list { fold.merge(expectation) }
        }
        return fold
    }

    /// A set of expectations being merged, in first-seen target order.
    struct Fold: Equatable {

        private struct Target: Equatable {
            var presence: HistoryExpectation.Presence
            var fields: [ExpectedField.Kind: ExpectedField]
        }

        private var targets: [HistoryAnchor: Target] = [:]
        /// First-seen order, so the folded list a step carries is stable rather than hash-ordered —
        /// a skip banner and a test both read better when the card comes before its comments.
        ///
        /// **Deliberately outside equality** (see `==`): the two folds a net-effect test compares are
        /// built by walking the same writes in opposite directions, so their orders differ by
        /// construction while the question being asked — did anything actually change — is about the
        /// values alone.
        private var order: [HistoryAnchor] = []

        static func == (lhs: Fold, rhs: Fold) -> Bool { lhs.targets == rhs.targets }

        /// **A later `.absent` clears what earlier writes said about the target**, which is how a move
        /// inside one session folds correctly: a comment edited and then deleted leaves nothing at its
        /// live location, and carrying the edit's body expectation there would make the session's own
        /// step stale the moment it was registered. Every move-shaped step declares both of its
        /// anchors precisely so this is expressible (`BoardStore.deleteComment`, `postComment`).
        mutating func merge(_ expectation: HistoryExpectation) {
            let key = Self.key(expectation.anchor)
            if targets[key] == nil {
                order.append(key)
                targets[key] = Target(presence: expectation.presence, fields: [:])
            }
            if expectation.presence == .absent {
                targets[key] = Target(presence: .absent, fields: [:])
                return
            }
            targets[key]?.presence = expectation.presence
            for field in expectation.fields {
                targets[key]?.fields[field.kind] = field
            }
        }

        /// The fold, back in the currency `HistoryStep` speaks. Fields are ordered by the kind's own
        /// declaration order so two equal folds always render identically.
        var expectations: [HistoryExpectation] {
            order.compactMap { key in
                guard let target = targets[key] else { return nil }
                let fields = Self.fieldOrder.compactMap { target.fields[$0] }
                return HistoryExpectation(anchor: key, presence: target.presence, fields: fields)
            }
        }

        /// Two anchors are one target when they name the same thing. Identity anchors already do that
        /// by construction — a UUID is its own normal form — so the only normalization left is the
        /// path anchor's, which a board gesture may spell with a trailing slash or a `.` component.
        private static func key(_ anchor: HistoryAnchor) -> HistoryAnchor {
            guard case let .path(url) = anchor else { return anchor }
            return .path(url.standardizedFileURL)
        }

        private static let fieldOrder: [ExpectedField.Kind] = [
            .title, .order, .width, .background, .backgroundImage, .hero, .icon, .iconColor, .body,
        ]
    }
}
