import Observation
import SwiftUI

// MARK: - The mode

/// Which of the body column's two surfaces is showing (05-card-window.md ▸ Mode grammar).
///
/// **Two cases, not three.** The raw-source outlet swaps the *entire content area* — title, body
/// and sidebar — so it is a state of the window, not of the body column, and it does not belong in
/// this enum. It lives in `CardRawSourceSession`, which is also where the grammar's two open
/// questions are settled (which mode a raw exit lands in, and what an empty body after Apply does).
/// Edit Body disabling while raw source is active (11-command-nexus.md) is that window-level state's
/// rule over this one, and it is enforced on the row: `EditBodyCommand.isEnabled(body:rawSource:)`.
public enum CardBodyMode: Equatable, Sendable {
    /// The rendered, selectable preview — **the resting state**.
    case preview
    /// The raw-Markdown editor.
    case edit

    /// The mode a window opens its body in: **Preview, unless the body is empty** (05 ▸ Mode
    /// grammar: "a card opens in Preview — unless its body is empty, which opens straight into
    /// Edit with the cursor ready (a new card has nothing to preview, so ⌘↩ during creation flows
    /// title → body without a mode stop)").
    ///
    /// Whitespace is empty (`BodyMarkup.isEmpty`): a body holding one newline previews as a blank
    /// page, and stopping the user at a blank preview of a blank body is precisely the ceremony
    /// the rule removes.
    public static func opening(body: String) -> CardBodyMode {
        BodyMarkup.isEmpty(body) ? .edit : .preview
    }

    /// ⌘E — View ▸ Edit Body's checkmark toggle. Also the whole of "Return in Preview enters Edit"
    /// and "Escape in Edit returns to Preview": three gestures, one flip, so they cannot drift.
    public var toggled: CardBodyMode {
        self == .preview ? .edit : .preview
    }
}

// MARK: - The window's body surface, as a handle

/// One card window's body column, reduced to what things *outside* it need: which mode it is in,
/// and how to put a find bar over whichever surface currently holds the keyboard.
///
/// `BoardSearchPresentation`'s shape and for its reason — one per window, `@State` in the host,
/// published through the focus system so a **menu item** (Edit ▸ Find, ⌘F) can reach the frontmost
/// card window without anyone keeping a which-window-is-key register. It is deliberately not on
/// `BoardStore`: the store is the *board's*, shared by every window on it, and two card windows
/// open on two cards of one board are in two different modes.
@MainActor
@Observable
public final class CardBodyPresentation {

    /// The surface showing right now. Starts in Preview and is settled by `openIfNeeded(body:)`
    /// the first time the window has a body to judge.
    public var mode: CardBodyMode = .preview

    /// Puts the standard find bar over the focused body surface — **Edit ▸ Find (⌘F) is
    /// find-in-text here** (05 ▸ Preview; 11-command-nexus.md scopes ⌘F "Board window: board
    /// search; card window: find-in-text"). Filled in by the surface itself, which is the only
    /// thing that holds a text view to hand the action to; `nil` until one exists, which is also
    /// exactly when ⌘F has nothing to find in.
    public var findInText: (() -> Void)?

    /// Flushes the Edit buffer — **"leaving Edit flushes the debounce"** (05 ▸ Mode grammar), and
    /// the reason the flip goes through `setMode(_:)` rather than being three separate assignments.
    ///
    /// Filled in by the window with its edit session's `endEditSession()`. It hangs here rather than
    /// on the session because *this* is the type every path that leaves Edit already holds: the menu
    /// item's toggle, Escape in the editor, and Return in Preview all flip the mode through one
    /// object, so attaching the flush to the flip is what makes "always" true by construction rather
    /// than by three call sites remembering.
    public var flushEdits: (() -> Void)?

    /// **Opens an Edit session** — the mirror of `flushEdits`, and here for its reason exactly: this
    /// is the type every path that *enters* Edit already holds, so attaching the announcement to the
    /// flip makes "always" true by construction rather than by three call sites remembering.
    ///
    /// Filled in by the window with its edit session's `beginEditSession()`. Its original second
    /// consumer — the auto-committer's stage-around — left with the git excision (2026-08-08,
    /// strategy/01-git-excision.md); the session bookkeeping it drives is its own reason now.
    public var beginEdits: (() -> Void)?

    /// Whether the opening rule has already run for this window.
    ///
    /// **Once, not per snapshot.** The rule is about *opening* a card, and the body it judges
    /// arrives with the first snapshot — but snapshots keep arriving (a watcher reload, a lane
    /// move, another window's edit). Re-running it would drag a reader back into Edit the moment
    /// someone else emptied the file, and would fight a user who had just pressed ⌘E.
    private var hasOpened = false

    public init() {}

    /// Applies the opening rule the first time it is called, and does nothing on every call after.
    @discardableResult
    public func openIfNeeded(body: String) -> CardBodyMode {
        guard !hasOpened else { return mode }
        hasOpened = true
        mode = CardBodyMode.opening(body: body)
        // A card that opened straight into Edit because its body was empty is in a session exactly
        // like one the user pressed ⌘E in, and the session bookkeeping applies either way.
        if mode == .edit { beginEdits?() }
        return mode
    }

    /// ⌘E, Return in Preview, Escape in Edit — see `CardBodyMode.toggled`.
    public func toggleMode() {
        setMode(mode.toggled)
    }

    /// The one place the mode changes, and therefore the one place **leaving Edit flushes** (05 ▸
    /// Mode grammar: "Leaving Edit flushes the debounce (mode flip, raw-source entry, window close)
    /// — the preview never lags the text that produced it, and neither does disk").
    ///
    /// The flush runs *before* the flip, not after: Preview reads the same buffer the editor was
    /// writing, so a flip that rendered first and saved second would be indistinguishable on screen
    /// — but a failure in that order would leave the user reading text the app had just failed to
    /// save, with the mode already changed under them. Saving first means the banner (and, on a
    /// close, the modal) arrives while the editor is still the thing on screen.
    ///
    /// Setting the mode it already has does nothing at all, which is what keeps a redundant
    /// menu-item validation pass or a re-published focus value from flushing an untouched buffer.
    public func setMode(_ newMode: CardBodyMode) {
        guard newMode != mode else { return }
        if mode == .edit { flushEdits?() }
        mode = newMode
        if newMode == .edit { beginEdits?() }
    }
}

// MARK: - View ▸ Edit Body

/// View ▸ Edit Body (⌘E) — the body column's mode toggle, with checkmark state (11-command-nexus.md;
/// 05-card-window.md ▸ Mode grammar).
///
/// **A `Toggle`, because the row is a checkmark row**: 11 files it as "(checkmark toggle)", and
/// 04-interactions.md ▸ Configurable bindings requires that such a row keep "one stable title,
/// checkmark state only" — so the title is the same string it was while the row was disabled, and
/// what changed with this milestone is the validation and the action, exactly as `FutureCommands`
/// predicts.
///
/// Validation is scope **plus the raw-source clause**: with no card window in front there is no
/// `cardBody` focused value, and the row disables; with source mode active it disables too — "View ▸
/// Edit Body (⌘E) disables while source mode is active, matching its toolbar item" (05 ▸ Raw source
/// outlet; 11-command-nexus.md files the same clause on the row). The reason is that the two would
/// be editing the same bytes from two surfaces: while the whole `index.md` is open as text, a mode
/// flip in the body column beneath it has nothing to flip *to* — the column is not on screen — and
/// its buffer's next debounced save would write a body the raw buffer is also about to overwrite.
/// Cancel and Apply own the exits (03-board-ui.md ▸ Toolbar).
///
/// The read-only lock is deliberately **not** part of it — entering Edit is not a mutation, and
/// 02-architecture.md § the lock's scope keeps editor buffers alive under the lock (only their saves
/// suspend), so a locked board can still be read in the editor and its text copied out.
struct EditBodyCommand: View {

    @FocusedValue(\.cardBody) private var cardBody
    @FocusedValue(\.cardRawSource) private var rawSource

    /// The row's validation, as a value a test can hold: a menu item's `.disabled` is otherwise only
    /// observable by driving the menu bar, and "⌘E disables while raw source is active" is precisely
    /// the kind of clause that regresses silently.
    static func isEnabled(body: CardBodyPresentation?, rawSource: CardRawSourceSession?) -> Bool {
        body != nil && rawSource?.isActive != true
    }

    var body: some View {
        Toggle("Edit Body", isOn: Binding(
            get: { cardBody?.mode == .edit },
            set: { isOn in cardBody?.setMode(isOn ? .edit : .preview) }
        ))
        .keyboardShortcut("e", modifiers: .command)
        .disabled(!Self.isEnabled(body: cardBody, rawSource: rawSource))
    }
}

/// The focused card window's body column, beside `FocusedValues.boardSearch` — see
/// `FocusedBoardStoreKey` for why window-scoped menu items reach their window this way.
struct FocusedCardBodyKey: FocusedValueKey {
    typealias Value = CardBodyPresentation
}

extension FocusedValues {
    var cardBody: CardBodyPresentation? {
        get { self[FocusedCardBodyKey.self] }
        set { self[FocusedCardBodyKey.self] = newValue }
    }
}
