//! Wire message envelopes — moved from `pmacs::protocol` in session 1
//! of the `pmacs-gpu` arc. Contains the input event types
//! (`Key`/`Modifiers`/`KeyEvent`, mouse types, `FrontendEvent`), the
//! instance-side message types (`InstanceMessage`, `CursorState`,
//! `InstanceSignal`, `GoodbyeReason`, `SelectionSnapshot`), the
//! `SemanticFrame` family (`StyleSpan`/`StyleSegment`, `Decoration`,
//! `Adornment*`, `ResourceBody`), and the attach handshake
//! (`Hello`/`AttachRequest`/`InstanceCapabilities`/`FrontendCapabilities`/
//! `NegotiatedCapabilities`/`InstanceIdentity` + `PROTOCOL_VERSION` /
//! `SUPPORTED_PROTOCOL_VERSIONS`).
//!
//! The original `pmacs::protocol` module keeps internal CLI / binding
//! types (`AttachTarget`, `AttachError`, `AttachmentHandle`, the
//! `crossterm_translate` submodule) plus the existing wire-format
//! roundtrip tests; it re-exports everything below so existing
//! `crate::protocol::*` imports stay stable.

use crate::cell::{Cell, CellCoord, CellSize, DiffSpan};
use crate::ids::{ByteRange, FrontendId};

// ---------------------------------------------------------------------------
// Key encoding
// ---------------------------------------------------------------------------

/// Key code, normalized away from any specific terminal protocol.
///
/// `Char` covers printable input. The named variants cover the keys
/// terminals report distinctly (arrows, function keys, etc.). `Unknown`
/// is the escape hatch: a key the protocol layer cannot encode in
/// any of the named variants is preserved as a u32 sentinel so it
/// can round-trip through serialization without becoming an error.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
pub enum Key {
    /// A printable character. The character is the user-visible
    /// codepoint after layout / IME processing.
    Char(char),
    /// A function key. `n` is 1-based: `F(1)` is F1.
    F(u8),
    /// Backspace / `^H`.
    Backspace,
    /// Enter / Return / `^M`.
    Enter,
    /// Left arrow.
    Left,
    /// Right arrow.
    Right,
    /// Up arrow.
    Up,
    /// Down arrow.
    Down,
    /// Home key.
    Home,
    /// End key.
    End,
    /// Page Up.
    PageUp,
    /// Page Down.
    PageDown,
    /// Tab.
    Tab,
    /// Shift-Tab.
    BackTab,
    /// Forward delete.
    Delete,
    /// Insert.
    Insert,
    /// Escape.
    Escape,
    /// Caps Lock.
    CapsLock,
    /// Scroll Lock.
    ScrollLock,
    /// Num Lock.
    NumLock,
    /// Print Screen.
    PrintScreen,
    /// Pause / Break.
    Pause,
    /// Menu / context-menu key.
    Menu,
    /// Numeric-keypad center key.
    KeypadBegin,
    /// The "null" keycode (terminal-protocol artifact).
    Null,
    /// A key the protocol layer does not recognize. The `u32`
    /// preserves whatever sentinel value the upstream layer attached
    /// (e.g. a media-key code from kitty's keyboard protocol). Round-trips
    /// through serialization but is not actionable by commands.
    Unknown(u32),
}

/// Modifier-key set. Bit-flag encoding for compact wire shape.
///
/// `META` corresponds to the "logo" / "super" key on most keyboards.
/// `HYPER` is reserved for the rare keyboards that distinguish it
/// from `META` (kitty's keyboard protocol surfaces both).
#[derive(
    Copy, Clone, Eq, PartialEq, Hash, Debug, Default, serde::Serialize, serde::Deserialize,
)]
pub struct Modifiers(u8);

impl Modifiers {
    /// Empty set: no modifiers held.
    pub const NONE: Modifiers = Modifiers(0);
    /// Shift.
    pub const SHIFT: Modifiers = Modifiers(1 << 0);
    /// Control.
    pub const CTRL: Modifiers = Modifiers(1 << 1);
    /// Alt / Option.
    pub const ALT: Modifiers = Modifiers(1 << 2);
    /// Meta / Super / Logo / Command.
    pub const META: Modifiers = Modifiers(1 << 3);
    /// Hyper. Distinguished from `META` only on keyboards that
    /// surface both (kitty's keyboard protocol).
    pub const HYPER: Modifiers = Modifiers(1 << 4);

    /// Construct from a raw bit set. Bits outside the defined range
    /// are silently masked off so a future-extended wire cannot smuggle
    /// undefined bits past current decoders.
    #[must_use]
    pub const fn from_bits_truncate(bits: u8) -> Self {
        Self(bits & 0b0001_1111)
    }

    /// Raw bit set.
    #[must_use]
    pub const fn bits(self) -> u8 {
        self.0
    }

    /// Whether `self` includes every bit set in `other`.
    #[must_use]
    pub const fn contains(self, other: Modifiers) -> bool {
        (self.0 & other.0) == other.0
    }

    /// Whether no modifiers are held.
    #[must_use]
    pub const fn is_empty(self) -> bool {
        self.0 == 0
    }
}

impl std::ops::BitOr for Modifiers {
    type Output = Modifiers;
    fn bitor(self, rhs: Modifiers) -> Modifiers {
        Modifiers(self.0 | rhs.0)
    }
}

impl std::ops::BitOrAssign for Modifiers {
    fn bitor_assign(&mut self, rhs: Modifiers) {
        self.0 |= rhs.0;
    }
}

/// A keyboard event.
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct KeyEvent {
    /// Frontend that produced the event.
    pub frontend_id: FrontendId,
    /// The key code.
    pub key: Key,
    /// Modifier set held when the key was pressed.
    pub mods: Modifiers,
    /// Monotonic timestamp at which the frontend captured the event.
    /// Zero means "no timestamp available" (e.g. test-synthesized
    /// events).
    pub timestamp_ns: u64,
}

// ---------------------------------------------------------------------------
// Mouse encoding
// ---------------------------------------------------------------------------

/// Mouse button.
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum MouseButton {
    /// Left button.
    Left,
    /// Right button.
    Right,
    /// Middle button.
    Middle,
}

/// Kind of mouse interaction.
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum MouseKind {
    /// Button pressed.
    Down(MouseButton),
    /// Button released.
    Up(MouseButton),
    /// Drag with the named button held.
    Drag(MouseButton),
    /// Pointer moved with no button held.
    Move,
    /// Wheel scrolled up.
    ScrollUp,
    /// Wheel scrolled down.
    ScrollDown,
    /// Wheel scrolled left.
    ScrollLeft,
    /// Wheel scrolled right.
    ScrollRight,
}

/// A mouse event.
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct MouseEvent {
    /// Frontend that produced the event.
    pub frontend_id: FrontendId,
    /// Kind of mouse interaction.
    pub kind: MouseKind,
    /// Cell-grid coordinate of the pointer at the moment of the event.
    pub coord: CellCoord,
    /// Modifiers held during the event.
    pub mods: Modifiers,
}

// ---------------------------------------------------------------------------
// Frontend → Instance events
// ---------------------------------------------------------------------------

/// Input event from frontend to instance.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum FrontendEvent {
    /// A key event.
    Key(KeyEvent),
    /// A mouse event.
    Mouse(MouseEvent),
    /// Frontend's terminal resized.
    Resize {
        /// Frontend that resized.
        frontend_id: FrontendId,
        /// New size, in cells.
        size: CellSize,
    },
    /// Bracketed-paste payload from the frontend.
    Paste {
        /// Frontend that produced the paste.
        frontend_id: FrontendId,
        /// Raw bytes pasted (the instance decodes as UTF-8 if relevant).
        data: Vec<u8>,
    },
    /// Frontend gained input focus.
    FocusGained(FrontendId),
    /// Frontend lost input focus.
    FocusLost(FrontendId),
    /// Frontend is going away. Instance treats this as immediate
    /// detach; no acknowledgement required.
    Detach(FrontendId),
    /// T M10.5: CRDT operation produced by this frontend's local
    /// edit, sent to the instance for broadcast to the other
    /// attached frontends. The actual flow that produces these
    /// (frontend maintaining a local CRDT state, applying edits
    /// optimistically, sending the resulting op) is wired in M10.8
    /// + M10.10; M10.5 declares the wire shape so the protocol
    ///   version bump (1 → 2) covers it.
    ///
    /// Only sent by v1.0 frontends (`protocol_version = 2`); v0.1
    /// frontends never emit this variant. Sessions negotiated at
    /// protocol version 1 must NOT receive this on the
    /// instance-side dispatcher (the daemon filters per-session;
    /// the editor-core treats it as an unknown frontend event if
    /// it ever arrives from a v1 session, which it shouldn't).
    CrdtOp {
        /// Which attached frontend produced this op. The instance
        /// uses this to avoid echoing the op back to its sender.
        frontend_id: FrontendId,
        /// Which buffer this op affects. The instance routes the
        /// op to that buffer's CRDT state.
        buffer_id: crate::BufferId,
        /// The CRDT operation payload — `peer_id` + opaque wire bytes
        /// loro's `import_updates` decodes.
        op: crate::CrdtOp,
    },
    /// T M11.1: the buffer byte range a semantic frontend currently
    /// has on screen, in buffer coordinates. Replaces the
    /// instance-derived grid viewport for `semantic_render` sessions:
    /// the instance scopes its `StyleSpans` / `Decorations` / … to
    /// this range rather than shipping a whole file's styling.
    ///
    /// **No pixels.** This carries a byte range, never viewport pixel
    /// size, DPI, font metrics, or glyph advances — the contract
    /// boundary invariant from the semantic-frontend design note. The
    /// frontend owns all visual-motion semantics and resolves
    /// pixel→offset locally; there is deliberately no hit-test
    /// request variant and no `SemanticResize`, both of which would
    /// leak pixels across the boundary.
    ///
    /// `generation` ties the declared range to a CRDT version so the
    /// instance can ignore a viewport that races a not-yet-applied
    /// edit (symmetric with `StyleSpans::generation`).
    ///
    /// Only emitted by sessions that negotiated `semantic_render`;
    /// a non-semantic session never sends it. M11.1 declares the
    /// wire shape; the instance-side consumer is wired with the
    /// projection seam in M11.2.
    Viewport {
        /// Which frontend's viewport this is.
        frontend_id: FrontendId,
        /// Which buffer the visible range indexes into.
        buffer_id: crate::BufferId,
        /// Half-open byte range currently on screen.
        visible: ByteRange,
        /// CRDT generation the frontend computed `visible` against.
        generation: u64,
    },
    /// Q#M1 (protocol v5): a pointer gesture a *semantic* frontend
    /// hit-tested locally to a **source byte offset**. The pixel→byte
    /// resolution happens entirely frontend-side (the frontend owns
    /// layout: fonts, inline adornments, scroll) — consistent with
    /// `Viewport`'s no-pixels contract; the instance replays its
    /// existing mouse gesture semantics in byte space, so selection
    /// behavior stays single-sourced with the grid path.
    ///
    /// Cell-grid frontends keep using [`FrontendEvent::Mouse`]; the
    /// two variants are per-session-kind, never mixed. Only sent when
    /// the instance's `Hello.protocol_version >= 5` (an older
    /// instance cannot decode the variant).
    Pointer {
        /// Which frontend produced the gesture (untrusted; the
        /// instance routes by the authenticated session, matching
        /// the `CrdtOp` / `Viewport` source-trust rule).
        frontend_id: FrontendId,
        /// Buffer the frontend was displaying when it hit-tested.
        buffer_id: crate::BufferId,
        /// Source byte offset of the hit (frontend-local hit test;
        /// adornment runs already snapped to their anchors).
        byte: u64,
        /// Which gesture step this is.
        kind: PointerKind,
        /// Modifiers held during the gesture. Carried for future
        /// Shift-click extension; the v5 instance ignores them.
        mods: Modifiers,
    },
    /// Navigate an open context menu (Q#CM1, protocol v11). A semantic
    /// frontend hit-tests the popup it drew locally and reports the row
    /// the pointer is over, so the daemon never needs the frontend's
    /// pixels — symmetric with how the GPU owns its viewport. Sent only
    /// while the daemon's menu is open.
    MenuPointer {
        /// Which frontend produced the gesture.
        frontend_id: FrontendId,
        /// Row index the pointer is over, or `None` when it left the
        /// popup (a hover off the menu, or a click outside).
        index: Option<u32>,
        /// `true` on a click/release: invoke `index`'s item, or dismiss
        /// the menu when `index` is `None`. `false` is a hover that only
        /// moves the highlight.
        invoke: bool,
    },
}

/// Gesture step for [`FrontendEvent::Pointer`]. Double-click
/// detection is frontend-side (`DoubleDown` instead of a second
/// `Down`): only the frontend knows pixel proximity and its own
/// double-click interval.
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum PointerKind {
    /// Primary button pressed at `byte`.
    Down,
    /// Pointer moved to `byte` with the primary button held.
    Drag,
    /// Primary button released at `byte`.
    Up,
    /// Second press at the same hit within the frontend's
    /// double-click window — selects the word at `byte`.
    DoubleDown,
    /// Third press at the same hit within the frontend's
    /// multi-click window — selects the whole line at `byte`,
    /// trailing newline included (Q#M4, protocol v7). The frontend
    /// sends this only to a `>= 7` instance; against an older one
    /// the third click restarts the chain as a plain `Down`.
    TripleDown,
    /// Secondary (right) button pressed at `byte` — opens the context
    /// menu there (Q#CM1, protocol v11). The frontend sends this only to
    /// a `>= 11` instance.
    Context,
}

impl FrontendEvent {
    /// The frontend that produced this event.
    #[must_use]
    pub fn frontend_id(&self) -> FrontendId {
        match self {
            Self::Key(e) => e.frontend_id,
            Self::Mouse(e) => e.frontend_id,
            Self::Resize { frontend_id, .. }
            | Self::Paste { frontend_id, .. }
            | Self::FocusGained(frontend_id)
            | Self::FocusLost(frontend_id)
            | Self::Detach(frontend_id)
            | Self::CrdtOp { frontend_id, .. }
            | Self::Viewport { frontend_id, .. }
            | Self::Pointer { frontend_id, .. }
            | Self::MenuPointer { frontend_id, .. } => *frontend_id,
        }
    }
}

// ---------------------------------------------------------------------------
// Instance → Frontend messages
// ---------------------------------------------------------------------------

/// Cursor position and visibility.
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct CursorState {
    /// Cell where the cursor should be drawn.
    pub coord: CellCoord,
    /// Whether the cursor is visible at all.
    pub visible: bool,
}

/// Instance-level signal that is not a render message.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum InstanceSignal {
    /// Terminal bell.
    Bell,
    /// Window-title change request.
    Title(String),
    /// Clipboard set request (OSC 52).
    Clipboard(Vec<u8>),
}

/// Reason an instance terminates an attachment.
///
/// Only the four variants the v0.1 daemon actually emits or rejects on.
/// `Evicted` (multi-frontend takeover) and similar will land alongside
/// the v0.3 multi-frontend work; until then `AlreadyAttached` covers
/// the single-slot equivalent.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum GoodbyeReason {
    /// Instance is shutting down (SIGTERM / SIGINT or clean exit).
    ShuttingDown,
    /// Frontend's `protocol_version` does not match the instance's.
    /// The handshake fails before any further messages.
    VersionMismatch {
        /// The instance's `PROTOCOL_VERSION`.
        server: u32,
        /// The version the frontend announced in its `AttachRequest`.
        client: u32,
    },
    /// Another frontend is currently attached. v0.1 rejects concurrent
    /// attaches; v0.3 will replace this with eviction or multiplexing.
    AlreadyAttached,
    /// Frontend sent a malformed message or otherwise violated the
    /// protocol. The connection is closed without further dialogue.
    ProtocolError,
    /// T M10.7: frontend declared one or more negotiated capability
    /// bits that the instance cannot honor. The handshake fails after
    /// the version check but before any further messages.
    ///
    /// `missing` lists the capability *field names* (e.g.,
    /// `"multi_frontend"`, `"crdt_replica"`) the frontend requested
    /// (`true`) that the instance reports as `false`. These strings
    /// are stable wire-format identifiers: they are exactly the
    /// `FrontendCapabilities` / `InstanceCapabilities` field names,
    /// not human-readable descriptions. The frontend translates them
    /// for display via [`AttachError`]'s formatting. Renaming a
    /// capability bit requires changing both the field name AND the
    /// missing-string emission in `negotiate_capabilities` in
    /// lockstep — see the M10.7 audit's wire-format-stability
    /// section.
    CapabilityMismatch {
        /// The capability bit names the frontend asked for that the
        /// instance does not support. Each entry is a verbatim
        /// `FrontendCapabilities` field name.
        missing: Vec<String>,
    },
}

/// Rendering and signals from instance to frontend.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum InstanceMessage {
    /// Cell deltas. `full_grid = true` is the initial sync sent on
    /// fresh attach (or after a resize where the previous grid is no
    /// longer applicable); `full_grid = false` is a differential
    /// frame.
    CellDelta {
        /// One run of changed cells per `DiffSpan`.
        spans: Vec<DiffSpan>,
        /// Whether `spans` represents a full-grid resync (true on
        /// fresh attach or post-resize) versus an incremental frame.
        full_grid: bool,
    },
    /// Cursor position and visibility update.
    Cursor(Option<CursorState>),
    /// Modeline cells. Reserved for v0.3 GUI use; v0.1 ships modeline
    /// inside [`InstanceMessage::CellDelta`]. The variant exists in
    /// the protocol from day one so adding the discrete channel later
    /// is not a breaking change.
    ModeLine(Vec<Cell>),
    /// Side-channel signal (bell, title, clipboard).
    Signal(InstanceSignal),
    /// Instance is terminating the attachment.
    Goodbye(GoodbyeReason),
    /// T M10.5: CRDT operation broadcast from the instance to all
    /// attached frontends. The originating frontend produced this op
    /// (via `FrontendEvent::CrdtOp` or via a local editor-core edit
    /// that synthesizes one); the instance fans it out so every
    /// attached frontend can apply the op to its local CRDT state.
    ///
    /// Only sent to v1.0 frontends — sessions negotiated at
    /// `protocol_version = 1` never receive this variant, per
    /// `§sec:m10-backward-compat`. The daemon filters at the
    /// outgoing-message path; this variant simply existing in the
    /// enum is not a wire-compat issue for v1 sessions because the
    /// daemon never emits it to them.
    ///
    /// M10.5 declares the wire shape. M10.8 wires the editor-core →
    /// daemon → frontend flow that actually emits these.
    CrdtOp {
        /// Which buffer this op affects. v1.0 frontends maintain
        /// a per-buffer local CRDT state; this routes to the right
        /// one.
        buffer_id: crate::BufferId,
        /// The CRDT operation payload — `peer_id` + opaque wire bytes
        /// loro's `import_updates` decodes.
        op: crate::CrdtOp,
    },
    /// T M10.6: cursor + selection state of one attached frontend,
    /// broadcast to the other v1.0 frontends so they can render
    /// peer-presence overlays. Coalesced at the daemon: rapid cursor
    /// movement produces one `PresenceUpdate` per tick per source
    /// frontend, carrying the *final* state, not intermediate values.
    ///
    /// Sender exclusion: the source frontend never receives its own
    /// `PresenceUpdate`. v0.1 sessions (negotiated `protocol_version =
    /// 1`) are filtered out at the daemon's outgoing-message path.
    ///
    /// M10.6 declares the wire shape AND wires the daemon-side
    /// sweep with per-session filter. In single-frontend deployments
    /// the recipient list is structurally empty (sender exclusion
    /// with no other v2 sessions); M10.8 enables the multi-frontend
    /// case where this message actually crosses the wire. The
    /// frontend's renderer for peer-cursor overlays is also M10.8.
    PresenceUpdate {
        /// Which attached frontend this presence belongs to. v1.0
        /// frontends use this to label the peer-cursor overlay
        /// ("user 4 is editing here").
        frontend_id: FrontendId,
        /// Which buffer the source frontend's cursor is in.
        buffer_id: crate::BufferId,
        /// Byte offset of the source frontend's cursor within
        /// `buffer_id`. Frontends convert to line/column at render
        /// time via the rope's coord-mapping; the wire carries the
        /// canonical byte offset to avoid encoding-vs-rendering
        /// drift across frontends.
        cursor: crate::Position,
        /// Active selection range, if any.
        selection: Option<SelectionSnapshot>,
    },
    /// T M10.10: bootstrap a frontend's local CRDT replica with the
    /// instance's current authoritative state. Sent once per active
    /// buffer at `SessionEstablished` time (and on subsequent
    /// buffer-creation events) to frontends that negotiated
    /// `crdt_replica: true`. Frontends that didn't negotiate the
    /// capability never receive this variant — the daemon's
    /// outgoing-message filter gates the send on
    /// `NegotiatedCapabilities::crdt_replica`.
    ///
    /// `crdt_snapshot` carries loro's run-encoded snapshot
    /// (`CrdtState::export_snapshot()`) — the CRDT-internal state
    /// including peer IDs, version vectors, and op-history structure.
    /// Raw byte contents are insufficient because a fresh CRDT replica
    /// initialized from bytes alone diverges on the first concurrent
    /// edit.
    ///
    /// Cursor position is intentionally absent: cursor is per-frontend
    /// window state (M10.8 `FrontendView`), not per-buffer CRDT
    /// state. The same buffer can appear in multiple windows on one
    /// frontend with different cursors; coupling cursor to
    /// `BufferSnapshot` would break this model.
    BufferSnapshot {
        /// Which buffer's CRDT state this snapshot represents.
        buffer_id: crate::BufferId,
        /// `loro::LoroDoc::export(ExportMode::Snapshot)` output. Applied
        /// to a fresh `CrdtState::new(peer_id_from_frontend(my_id))`
        /// via `import_snapshot(bytes)` on the receiving frontend.
        crdt_snapshot: Vec<u8>,
    },
    /// T M10.10: the active buffer for a replica frontend, with the
    /// cursor position within it.
    ///
    /// # Semantics (Day 3 step 3b composition-check broadened
    /// contract)
    ///
    /// `CursorByte` represents "the active buffer for this frontend
    /// is `buffer_id`; the cursor in that buffer is at `byte_pos`."
    /// Not just "the cursor moved." This contract matters: a narrow
    /// "cursor moved" emission would miss active-buffer-changed-
    /// without-cursor-motion events (Lua-driven buffer switch
    /// landing at the same byte position), and the frontend's
    /// active-buffer tracking would go stale.
    ///
    /// Daemon emits `CursorByte` on every per-tick render frame for
    /// replica frontends, derived fresh from `active_window_for(fid)`.
    /// Cursor move, active-buffer change, and active-window change
    /// all produce a new emission carrying the current `(buffer_id,
    /// byte_pos)`. The per-tick rate (16ms at 60Hz) is the same as
    /// `Cursor`'s grid-coord variant.
    ///
    /// # Why a separate variant from `Cursor`
    ///
    /// `Cursor` carries grid coordinates (row/col cells) — the
    /// frontend uses them to paint the cursor. The optimistic-apply
    /// path needs byte position (CRDT insert/delete is byte-indexed),
    /// which the grid coordinate can't recover without duplicating
    /// the daemon's view-layout logic (tab expansion, line wrap,
    /// double-width chars, viewport offset). `CursorByte` is the
    /// authoritative byte position for the active buffer.
    ///
    /// # Atomicity with `Cursor`
    ///
    /// The daemon emits `Cursor` and `CursorByte` together for
    /// replica frontends — both derived from the same render-frame
    /// iteration so they describe the cursor in the same instant in
    /// two reference frames. Non-replica frontends receive only
    /// `Cursor` (existing behavior). The replica frontend that sees
    /// `Cursor` without a paired `CursorByte` would interpret stale
    /// byte position; the daemon guarantees both emit together by
    /// derivation, not by message-protocol atomicity.
    ///
    /// # Wire-format compatibility
    ///
    /// New variant in v2; receivers without M10.10 hard-error on
    /// decode (postcard does not gracefully degrade unknown variants,
    /// per M10.10-FRAMING.md Refinement 3). Capability-gated: daemon
    /// sends only to frontends that negotiated `crdt_replica: true`.
    /// `PROTOCOL_VERSION` stays at 2.
    CursorByte {
        /// The buffer the cursor is in. A replica frontend tracks
        /// per-buffer cursors; this routes the update to the right
        /// entry.
        buffer_id: crate::BufferId,
        /// Byte offset of the cursor within `buffer_id`. Source of
        /// truth for the optimistic-apply path's insert / delete
        /// position arguments. Wire type matches
        /// `PresenceUpdate::cursor` (`u64`) for consistency; frontend
        /// converts to `usize` for the loro API.
        byte_pos: crate::Position,
    },
    /// T M11.1 — syntax + face styling over the semantic frontend's
    /// current viewport range. `generation` ties the spans to a CRDT
    /// version so the frontend can discard styling that predates an
    /// edit it has already applied optimistically. Ships **no text** —
    /// the frontend holds the rope via the `crdt_replica` machinery
    /// and interprets these spans over it.
    ///
    /// # Diff shape (T M11.4)
    ///
    /// Mirrors `CellDelta`'s `full_grid` + changed-runs structure,
    /// lifted from positional cells to byte-anchored ranges. `full =
    /// true` is a resync: the frontend discards all prior styling for
    /// `buffer_id` and the `segments` are authoritative for the whole
    /// declared viewport (first frame after a `Viewport`, a viewport
    /// jump, or a generation discontinuity). `full = false` is
    /// incremental: each [`StyleSegment`] replaces styling **only**
    /// within its `range`; bytes covered by no segment keep their
    /// previously-applied style. A frame whose styling is unchanged
    /// ships no `StyleSpans` at all.
    ///
    /// Because byte offsets cascade on edits (an insert shifts every
    /// later span), an incremental frame after an edit dirties
    /// `[edit, viewport_end)` — still bounded, and no-edit frames
    /// (cursor move, scroll within the declared viewport, selection)
    /// cost nothing. Each segment carries *all* current spans
    /// intersecting its range (clipped), not only changed ones, so an
    /// unchanged span overlapping a dirty range is faithfully
    /// reconstructed.
    ///
    /// Gated on negotiated `semantic_render`; never sent to a grid
    /// session (the daemon's per-session outgoing filter — wired with
    /// the producer in M11.2 — never emits it there, so postcard's
    /// hard-error on unknown variants is mooted exactly as it is for
    /// `CursorByte`).
    StyleSpans {
        /// Buffer these spans interpret.
        buffer_id: crate::BufferId,
        /// CRDT generation the spans were computed against.
        generation: u64,
        /// `true` → discard all prior styling for `buffer_id` first;
        /// `segments` are authoritative for the declared viewport.
        full: bool,
        /// Dirty byte regions and the styling now covering them.
        segments: Vec<StyleSegment>,
    },
    /// T M11.1 — diagnostics, search hits, current-line, and any
    /// other "this region means something" overlay, as offset ranges
    /// plus a kind. Peer selection is **not** here — it stays on the
    /// existing `PresenceUpdate` path. Gated on `semantic_render`.
    ///
    /// T M11.4 — same `full` + segment diff shape as `StyleSpans`
    /// (see its docs), and gains `generation` for parity: a frontend
    /// wants the CRDT version decorations were computed against for
    /// the same optimistic-edit race reason styling does.
    Decorations {
        /// Buffer these decorations apply to.
        buffer_id: crate::BufferId,
        /// CRDT generation the decorations were computed against.
        generation: u64,
        /// `true` → discard all prior decorations for `buffer_id`
        /// first; `segments` are authoritative for the viewport.
        full: bool,
        /// Dirty byte regions and the decorations now covering them.
        segments: Vec<DecorationSegment>,
    },
    /// T M11.1 — inlay hints, blame, lens, virtual text. Anchored at
    /// a single offset with a placement; occupies no document bytes —
    /// the frontend interleaves it at layout time. Gated on
    /// `semantic_render`.
    InlineAdornments {
        /// Buffer these adornments annotate.
        buffer_id: crate::BufferId,
        /// The adornment items for the declared viewport.
        items: Vec<InlineAdornment>,
    },
    /// Coarse whole-file styling summary for a minimap / scrollbar
    /// overview, resolving design-note Open Q#2. One [`Style`] per
    /// source line (the *dominant* style for that line by byte count
    /// across the producer's current spans); the frontend maps minimap
    /// rows to one or more of these. Unlike [`Self::StyleSpans`], this
    /// is **not** viewport-scoped — the minimap shows the whole file.
    ///
    /// Recomputed when the buffer's CRDT `generation` advances; an
    /// unchanged buffer ships no further summary. A coarser
    /// representation (fixed bands) or finer (run-length style runs)
    /// is recorded in `docs/semantic-frontend-protocol.md` as future
    /// refinements; per-line dominant style is the v1 choice because
    /// minimap rows naturally correspond to code lines.
    ///
    /// Gated on negotiated `semantic_render`; the daemon emits it only
    /// for sessions that have a [`crate::semantic_render::SemanticRenderState`]
    /// (structural gating, same as every other semantic family).
    FileStyleSummary {
        /// Buffer this summary describes.
        buffer_id: crate::BufferId,
        /// CRDT generation the summary was computed against. The
        /// frontend can discard a summary that predates an edit it
        /// has already applied optimistically.
        generation: u64,
        /// One [`crate::cell::Style`] per source line, in line order
        /// from line 0. Empty when the buffer is empty.
        lines: Vec<crate::cell::Style>,
    },
    /// Q#S1 (status band, protocol v8) — instance-authoritative
    /// status facts a semantic frontend cannot derive locally:
    /// buffer name, modified flag, whole-file diagnostic counts.
    /// Cursor position and scroll stay frontend-derived (the
    /// optimistic caret must not lag a round trip). Emitted by the
    /// semantic producer when any fact changes; kept off wires
    /// negotiated `< 8` (additive variant — an older peer would
    /// hard-error decoding it).
    StatusFacts {
        /// Buffer these facts describe.
        buffer_id: crate::BufferId,
        /// Buffer display name.
        name: String,
        /// Unsaved-changes flag.
        modified: bool,
        /// Whole-file `Error`-severity diagnostic count.
        diag_errors: u32,
        /// Whole-file `Warning`-severity diagnostic count.
        diag_warnings: u32,
    },
    /// T M11.1 — diff zones, folded-region placeholders, anything
    /// occupying its own vertical band. Anchored to the offset of the
    /// line it precedes or replaces; the frontend allocates the
    /// vertical space. Gated on `semantic_render`.
    BlockAdornments {
        /// Buffer these adornments annotate.
        buffer_id: crate::BufferId,
        /// The block items for the declared viewport.
        items: Vec<BlockAdornment>,
    },
    /// T M11.1 — the instance's authoritative fold set as document
    /// facts. Folding is an instance command-semantics concern (Lua
    /// can fold); the visual collapse is a frontend layout concern —
    /// the frontend renders the placeholder and adjusts its own
    /// layout. Gated on `semantic_render`.
    FoldState {
        /// Buffer whose fold set this is.
        buffer_id: crate::BufferId,
        /// Folded byte ranges.
        folds: Vec<ByteRange>,
    },
    /// T M11.1 — out-of-band content an adornment refers to (images,
    /// blame avatars). Sent once, referenced by `handle`, so it is
    /// not re-shipped per frame. Gated on `semantic_render`.
    ResourceOffer {
        /// Stable handle adornments reference via
        /// [`AdornmentContent::Resource`].
        handle: u64,
        /// MIME type of `body`.
        mime: String,
        /// Inline bytes or a URI the frontend resolves itself.
        body: ResourceBody,
    },
    /// Daemon-side input-dispatcher idleness signal. Tells a
    /// `crdt_replica` frontend whether the next key event would be
    /// **intercepted** by the daemon (minibuffer prompt active or
    /// dispatcher holds a pending multi-key prefix) versus would
    /// self-insert into the active buffer.
    ///
    /// Frontends running the optimistic-apply layer (`src/optimistic.rs`)
    /// gate the local apply on `idle == true`: when the daemon is not
    /// idle, plain-char keystrokes must round-trip as
    /// [`FrontendEvent::Key`] so the daemon's minibuffer / prefix
    /// dispatcher receives them. Without this signal, characters typed
    /// into a minibuffer prompt would be applied as `CrdtOp`s to the
    /// previously-active document instead — the surfacing of which
    /// motivated this wire addition.
    ///
    /// Emission contract (daemon side):
    ///
    /// - Once after `AttachRequest` is accepted, before any other
    ///   non-handshake message, so a fresh frontend starts from a
    ///   known idle state regardless of the daemon's current input
    ///   condition.
    /// - At every transition: minibuffer begin / dismiss; dispatcher
    ///   `pending` empty↔non-empty.
    /// - Coalesced where consecutive transitions land on the same
    ///   value (no spurious back-to-back identical signals).
    ///
    /// Gated on negotiated `crdt_replica` — frontends without the
    /// optimistic-apply layer have no use for this signal and the
    /// daemon's per-session filter drops it for them.
    DispatchIdle {
        /// `true` → the next key event would self-insert into the
        /// active buffer (no minibuffer, no pending prefix); optimistic
        /// apply is correct.
        /// `false` → the daemon would intercept the next key; the
        /// frontend must round-trip via [`FrontendEvent::Key`].
        idle: bool,
    },
    /// Q#SR5 (incremental search, protocol v9) — the live isearch
    /// prompt for a semantic frontend that cannot host a minibuffer.
    /// Carries the query as typed and the match readout so the
    /// frontend can render an `I-search: <query> (n/m)` band; the
    /// matches themselves arrive as [`DecorationKind::SearchMatch`] /
    /// [`DecorationKind::SearchMatchActive`] decorations. A `query` of
    /// `None` means no search is running — the frontend hides the band.
    ///
    /// Emitted by the semantic producer when the search state changes
    /// (cached-compare suppressed, like [`Self::StatusFacts`]). The
    /// `regex` / `invalid` fields (Q#RX6) changed this variant's
    /// encoding, so the daemon's per-session filter now keeps it off
    /// wires negotiated `< 10` (was `< 9` for the original four-field
    /// shape — see `PROTOCOL_VERSION`).
    SearchPrompt {
        /// Buffer the search is anchored in (the active buffer).
        buffer_id: crate::BufferId,
        /// The query as typed so far, or `None` when no search runs.
        /// `Some("")` is a freshly-started search with an empty query.
        query: Option<String>,
        /// 0-based index of the active match, or `None` when the query
        /// has no matches (a failing search).
        active: Option<u32>,
        /// Total number of matches for the current query.
        total: u32,
        /// `true` when the search is in regex mode (Q#RX3) — the
        /// frontend prefixes the prompt with `Regex `.
        regex: bool,
        /// `true` when a regex pattern failed to compile — the frontend
        /// shows `[invalid]` instead of a match count. Always `false`
        /// in literal mode.
        invalid: bool,
    },
    /// Open-menu contents for a semantic frontend (Q#CM1, protocol v11).
    /// The daemon resolves the visible items (predicates / context tags)
    /// and ships the rendered rows + highlight; the frontend draws the
    /// popup at the pixel it remembered from the right-click and reports
    /// navigation via [`FrontendEvent::MenuPointer`]. An empty `rows`
    /// closes the menu. Cached-compare suppressed like `SearchPrompt`.
    MenuPrompt {
        /// Buffer the menu is anchored in (the active buffer).
        buffer_id: crate::BufferId,
        /// Rows top-to-bottom (items + separators); empty = closed.
        rows: Vec<MenuPromptRow>,
        /// Index into `rows` of the highlighted item, or `None` when
        /// closed.
        active: Option<u32>,
    },
    /// Minibuffer prompt state for a semantic frontend (Q#MB1, protocol
    /// v12). The minibuffer is a single *global* core instance, so this
    /// is bufferless; the producer still emits it from the active-buffer
    /// viewport. `prompt: None` clears the GUI. Cached-compare
    /// suppressed like `SearchPrompt`; daemon-gated `>= 12`.
    MinibufferPrompt {
        /// The prompt string (e.g. `"M-x "`), or `None` when no
        /// minibuffer is open.
        prompt: Option<String>,
        /// The text typed so far.
        input: String,
        /// Codepoints before the cursor within `input` (the caret
        /// position).
        cursor: u32,
        /// A windowed slice of the completion candidates (best-first,
        /// already filtered/sorted by the core), `<= MB_VISIBLE`.
        candidates: Vec<String>,
        /// Highlighted row *within* `candidates`, or `None`.
        selected: Option<u32>,
        /// Total candidate count (the window is a slice of this).
        total: u32,
    },
}

/// One row of an open menu on the wire ([`InstanceMessage::MenuPrompt`]).
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct MenuPromptRow {
    /// Display label (empty and ignored when `separator`).
    pub label: String,
    /// `true` for a non-selectable group divider.
    pub separator: bool,
}

/// Flat selection state for the wire.
///
/// Mirrors [`crate::window::Selection`] but as a self-contained pair
/// of byte offsets — `anchor` is where the selection began,
/// `active` is the current selection cursor. Either may be the
/// numerically larger value; callers wanting `(lo, hi)` order
/// compute it locally.
///
/// Kept flat (no nested types) so [`PartialEq`] equality is exactly
/// wire-representation equality: two `SelectionSnapshot`s compare
/// equal iff they serialize to identical bytes. The presence-diff
/// sweep relies on this — see [`crate::presence::SessionRegistry`].
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct SelectionSnapshot {
    /// Where the selection began.
    pub anchor: crate::Position,
    /// The active end (typically the cursor at the moment of the
    /// snapshot).
    pub active: crate::Position,
}

// ---------------------------------------------------------------------------
// T M11.1 — Semantic-frontend projection types
//
// The payloads of the `InstanceMessage::StyleSpans` … `ResourceOffer`
// family and `FrontendEvent::Viewport`. Everything is anchored in
// **byte offsets** (consistent with `CursorByte`): line/col is a
// frontend rendering concern, CRDT position is replica-internal. The
// instance never learns a pixel — see the contract boundary in
// `docs/semantic-frontend-protocol.md`.
//
// The variant/kind sets here are provisional and co-evolve within the
// M11 arc behind the `semantic_render` capability + protocol v3,
// exactly as the CRDT op shape evolved M10.5→M10.10 behind
// `crdt_replica`. They are not a wire-compat hazard for non-semantic
// sessions: the daemon's per-session outgoing filter (wired with the
// producer in M11.2) never emits the family to a session that didn't
// negotiate `semantic_render`, so postcard's hard-error on unknown
// variants is mooted exactly as it is for `CursorByte`.
// ---------------------------------------------------------------------------

// `ByteRange` moved to `pmacs-protocol` (see the re-export near the
// top of this file).

/// One run of buffer bytes carrying a resolved visual style. The
/// instance is the single syntax/face authority; the frontend lays
/// the style out locally over rope text it already holds. Reuses
/// [`crate::cell::Style`] so the grid and semantic projections share
/// one style vocabulary.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct StyleSpan {
    /// Byte range this style covers.
    pub range: ByteRange,
    /// The resolved style (syntax highlight ∘ faces ∘ theme).
    pub style: crate::cell::Style,
}

/// T M11.4 — one dirty byte region of an `InstanceMessage::StyleSpans`
/// frame and the styling now covering it. The semantic analog of a
/// `CellDelta` changed-run: the frontend clears styling within
/// `range` and applies `spans` (already clipped to `range`). `spans`
/// is every current span intersecting `range`, not only changed ones,
/// so an unchanged span overlapping the dirty region is preserved.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct StyleSegment {
    /// The byte region the frontend should clear and repaint.
    pub range: ByteRange,
    /// Spans intersecting `range`, each clipped to it.
    pub spans: Vec<StyleSpan>,
}

/// What a [`Decoration`] region *means*. Provisional variant set (see
/// the module-section note above). Peer selection is deliberately
/// absent — it stays on the `PresenceUpdate` path.
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum DecorationKind {
    /// LSP diagnostic, error severity.
    DiagnosticError,
    /// LSP diagnostic, warning severity.
    DiagnosticWarning,
    /// LSP diagnostic, information severity.
    DiagnosticInfo,
    /// LSP diagnostic, hint severity.
    DiagnosticHint,
    /// The local selection region.
    Selection,
    /// A non-active search match.
    SearchMatch,
    /// The currently-focused search match.
    SearchMatchActive,
    /// The line containing the cursor.
    CurrentLine,
}

/// A byte range tagged with what it means. The frontend decides how
/// to paint each [`DecorationKind`] (squiggle, highlight, gutter
/// mark) — the instance only states the fact.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Decoration {
    /// Byte range the decoration covers.
    pub range: ByteRange,
    /// What the region signifies.
    pub kind: DecorationKind,
}

/// T M11.4 — `StyleSegment`'s analog for the `Decorations` family:
/// one dirty byte region and the decorations now covering it (every
/// current decoration intersecting `range`, clipped to it).
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct DecorationSegment {
    /// The byte region the frontend should clear and repaint.
    pub range: ByteRange,
    /// Decorations intersecting `range`, each clipped to it.
    pub decorations: Vec<Decoration>,
}

/// Where an [`InlineAdornment`] sits relative to its anchor offset.
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum AdornmentPlacement {
    /// On its own, before the line containing `at`.
    BeforeLine,
    /// At the end of the line containing `at`.
    EndOfLine,
    /// Inline, exactly at the byte offset `at`.
    AtOffset,
}

/// Adornment payload: either inline styled text, or a handle into a
/// previously-sent [`InstanceMessage::ResourceOffer`] so out-of-band
/// content (images, blame avatars) is shipped once, not per frame.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum AdornmentContent {
    /// Styled virtual text.
    Text {
        /// The virtual text to display.
        text: String,
        /// Its style.
        style: crate::cell::Style,
    },
    /// A handle into a `ResourceOffer`.
    Resource {
        /// The offered resource's handle.
        handle: u64,
    },
}

/// Virtual text occupying no document bytes (inlay hints, blame,
/// lens). Anchored at a single offset; the frontend interleaves it
/// at layout time.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct InlineAdornment {
    /// Buffer byte offset this adornment anchors to.
    pub at: u64,
    /// Placement relative to `at`.
    pub placement: AdornmentPlacement,
    /// What to render.
    pub content: AdornmentContent,
}

/// Content occupying its own vertical band (diff zones, folded-region
/// placeholders). Anchored to the offset of the line it precedes or
/// replaces. `replaces` is `Some` when the band stands in for a
/// collapsed region (the frontend renders the placeholder instead of
/// that range), `None` for an additive band. The frontend allocates
/// the vertical space — the instance never dictates pixel height.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct BlockAdornment {
    /// Buffer byte offset of the line this band precedes/replaces.
    pub at: u64,
    /// The byte range this band stands in for, if it replaces one.
    pub replaces: Option<ByteRange>,
    /// What to render in the band.
    pub content: AdornmentContent,
}

/// The body of an [`InstanceMessage::ResourceOffer`] — inline bytes
/// for small payloads, or a URI the frontend resolves itself for
/// large or remote resources.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum ResourceBody {
    /// The resource bytes, carried inline.
    Inline(Vec<u8>),
    /// A URI the frontend fetches/resolves on its own.
    Uri(String),
}
// ---------------------------------------------------------------------------
// Handshake — version, identity, capabilities
// ---------------------------------------------------------------------------

/// Wire-protocol version. Bumped on any breaking change to the
/// `Hello` / `AttachRequest` / event-message shapes.
///
/// The handshake compares against [`SUPPORTED_PROTOCOL_VERSIONS`];
/// mismatches close the connection with
/// [`GoodbyeReason::VersionMismatch`]. v1.0 servers and clients accept
/// either the v0.1 wire (version 1) or the v1.0 wire (version 2) per
/// `§sec:m10-backward-compat` — both directions of the version
/// asymmetry need symmetric relaxation so v0.1-era binaries connect
/// to v1.0-era binaries (and vice versa) once both have shipped.
///
/// T M10.5: bumped from 1 to 2. The v0.1 wire (version 1) remains
/// accepted by v1.0 binaries; CRDT-only message variants
/// (`InstanceMessage::CrdtOp`, `FrontendEvent::CrdtOp`) are filtered
/// per-session for v1 negotiated sessions.
///
/// T M11.1: bumped from 2 to 3. The v1.0 wire (version 2) remains
/// accepted; the semantic-frontend variant family
/// (`InstanceMessage::StyleSpans` … `ResourceOffer`,
/// `FrontendEvent::Viewport`) is filtered per-session for sessions
/// that did not negotiate `semantic_render`. Mechanically identical
/// to the M10.5 bump: the slice-membership handshake check (not
/// strict equality) means v0.1/v1.0 binaries keep connecting
/// unchanged, and the new variants simply existing in the enums is
/// not a wire-compat issue for non-semantic sessions because the
/// daemon never emits them to those sessions.
///
/// T M11.6: bumped from 3 to 4. v1, v2, v3 wires remain accepted;
/// [`InstanceMessage::DispatchIdle`] is filtered per-session for
/// sessions whose negotiated wire is `<= 3`. Same gating shape as the
/// `CrdtOp` / semantic-frontend bumps: the daemon's per-tick emission
/// checks the session's negotiated version and skips the variant for
/// older peers. An old peer would hard-error on decode of an unknown
/// postcard variant; gating prevents that.
///
/// Q#M1 (mouse framing): bumped from 4 to 5 for
/// [`FrontendEvent::Pointer`]. The gate runs in the *frontend* this
/// time (the new variant travels frontend→instance): a semantic
/// frontend sends `Pointer` only when the instance's
/// `Hello.protocol_version >= 5`, because an older instance would
/// hard-error decoding the unknown variant.
///
/// T M4.6 (diagnostic surface): bumped from 5 to 6 for
/// `Style::underline_color`. Unlike every previous bump, this one
/// changes the *encoding of an existing struct* — `Style` rides
/// inside `Cell` / `CellDelta` / `Snapshot` / `StyleSpans`, messages
/// every session receives — so per-session send gating cannot
/// preserve compatibility (postcard is not self-describing; a v5
/// decoder mis-reads any v6 `Style`). v6 binaries therefore accept
/// only v6 peers: a version-mismatched pair fails the handshake with
/// [`GoodbyeReason::VersionMismatch`] instead of garbling cell
/// traffic mid-session.
///
/// Q#M4 (mouse deferred set): bumped from 6 to 7 for
/// [`PointerKind::TripleDown`]. Back to the cheap additive shape:
/// a new variant on a frontend→instance enum, gated in the frontend
/// (sent only when the instance's `Hello.protocol_version >= 7`),
/// so the compat ladder restarts on the v6 encoding floor —
/// `SUPPORTED_PROTOCOL_VERSIONS` grows to `[6, 7]`.
///
/// Q#S1 (status band): bumped from 7 to 8 for
/// [`InstanceMessage::StatusFacts`]. Additive again, gated in the
/// *daemon* this time (the variant travels instance→frontend): the
/// per-session filter keeps it off wires negotiated `< 8`, the same
/// shape as the `DispatchIdle` (v4) gate.
///
/// Q#SR5 (incremental search): bumped from 8 to 9 for
/// [`InstanceMessage::SearchPrompt`]. Additive and daemon-gated per
/// session, identical shape to the `StatusFacts` (v8) bump.
///
/// Q#RX6 (regex search): bumped from 9 to 10 — `SearchPrompt` gained
/// `regex` / `invalid` fields, changing that variant's postcard
/// encoding. Still daemon-gated per session (now at `< 10`); a v9 peer
/// negotiates v9 and simply receives no `SearchPrompt` (the decorations
/// still highlight), rather than mis-decoding the wider shape.
pub const PROTOCOL_VERSION: u32 = 12;

/// T M10.5: the set of protocol versions a v1.0 binary accepts on
/// the wire. v0.1 binaries only accepted `[1]`; v1.0 binaries accept
/// `[1, 2]` so the version asymmetry the §sec:m10-backward-compat
/// spec section describes is handled symmetrically on both sides.
///
/// T M11.1: extended to `[1, 2, 3]`. v1.1 binaries accept the v0.1
/// (1), v1.0 (2), and semantic-frontend (3) wires. The check remains
/// slice membership — "is the peer's `protocol_version` present in
/// this slice?" — not strict equality on `PROTOCOL_VERSION`. The
/// session's negotiated version (the peer's) is recorded for
/// downstream filtering: v1 sessions don't receive
/// `InstanceMessage::CrdtOp` / `PresenceUpdate` messages even from
/// a v3 daemon, and only sessions that negotiated `semantic_render`
/// receive the `SemanticFrame` variant family.
///
/// T M11.6: extended to `[1, 2, 3, 4]`. v4 sessions receive
/// `InstanceMessage::DispatchIdle`; older sessions are filtered out
/// of that emission.
///
/// Mouse framing Q#M1: extended to `[1, 2, 3, 4, 5]`. v5 peers may
/// send `FrontendEvent::Pointer`; the frontend-side gate (see
/// [`PROTOCOL_VERSION`]) keeps the variant off wires negotiated `< 5`.
///
/// T M4.6: narrowed to `[6]`. `Style::underline_color` changed the
/// postcard encoding of every cell-carrying message, so v6 binaries
/// cannot exchange cell traffic with any earlier wire. The v1–v5
/// compat ladder (additive variants, per-session filtering) assumed
/// shared-struct encodings never changed; this bump is the first
/// that breaks that assumption, and slice membership is how the
/// handshake communicates it.
///
/// Q#M4: extended to `[6, 7]`. `PointerKind::TripleDown` is additive
/// and frontend-gated (like `Pointer` itself at v5), so the ladder
/// resumes: v6 and v7 binaries interoperate, with the new variant
/// kept off wires whose instance negotiated `< 7`.
///
/// Q#S1: extended to `[6, 7, 8]`. `InstanceMessage::StatusFacts` is
/// additive and daemon-gated per session.
///
/// Q#SR5: extended to `[6, 7, 8, 9]`. `InstanceMessage::SearchPrompt`
/// is additive and daemon-gated per session.
///
/// Q#RX6: extended to `[6, 7, 8, 9, 10]`. `SearchPrompt` gained
/// `regex` / `invalid` (encoding change to that variant); v9 and v10
/// interoperate because the variant is daemon-gated per session, so a
/// v9 peer is simply never sent the wider shape.
///
/// Q#CM1: extended to `[6, 7, 8, 9, 10, 11]`. The context menu adds
/// `PointerKind::Context` (frontend-gated like `Pointer`/`TripleDown`),
/// `FrontendEvent::MenuPointer`, and `InstanceMessage::MenuPrompt`
/// (daemon-gated per session) — all additive, so the ladder resumes.
///
/// Q#MB1: extended to `[6, 7, 8, 9, 10, 11, 12]`.
/// `InstanceMessage::MinibufferPrompt` is additive and daemon-gated per
/// session, so the ladder resumes again.
pub const SUPPORTED_PROTOCOL_VERSIONS: &[u32] = &[6, 7, 8, 9, 10, 11, 12];

/// T M10.5: predicate for the handshake check. Returns `true` if
/// `peer_version` is in [`SUPPORTED_PROTOCOL_VERSIONS`].
#[must_use]
pub fn is_supported_protocol_version(peer_version: u32) -> bool {
    SUPPORTED_PROTOCOL_VERSIONS.contains(&peer_version)
}

/// Identifies an instance for client-side display.
///
/// Sent inside [`Hello`] from instance to frontend. Use of `uptime_secs`
/// instead of an absolute start time is deliberate: instance and
/// frontend may run on machines whose clocks disagree, so the frontend
/// computes "instance has been running N seconds" using only the
/// instance's view of time.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct InstanceIdentity {
    /// Pmacs version string (`env!("CARGO_PKG_VERSION")`).
    pub pmacs_version: String,
    /// Short git hash if the build embedded one. `None` for releases or
    /// source-tarball builds where no git checkout was available.
    pub build_hash: Option<String>,
    /// The name the instance was launched under (`--socket NAME`).
    /// `None` for the default daemon (no `--socket` argument).
    pub instance_name: Option<String>,
    /// Seconds since the instance started, from the instance's clock.
    /// Frontend displays "running 47m" by interpreting this against
    /// its own notion of "now," avoiding cross-machine clock skew.
    pub uptime_secs: u64,
    /// Working directory the instance is running in. Encoded as a
    /// UTF-8 string; non-UTF-8 paths are rejected at the boundary.
    pub working_directory: String,
}

impl InstanceIdentity {
    /// Build an identity for the running pmacs process.
    ///
    /// `instance_name` is the user-facing name (typically the
    /// `--socket NAME` value for the daemon path; `None` for the
    /// in-process Local mode and the unnamed default daemon).
    /// `started` is the wall-clock anchor used to compute
    /// [`Self::uptime_secs`]; the elapsed seconds are evaluated at the
    /// call site, so calling twice on different days surfaces different
    /// uptimes from the same anchor.
    ///
    /// The version comes from `CARGO_PKG_VERSION` and the build hash
    /// from the optional `PMACS_GIT_HASH` environment variable populated
    /// by the build script.
    #[must_use]
    pub fn for_running_process(instance_name: Option<String>, started: std::time::Instant) -> Self {
        Self {
            pmacs_version: env!("CARGO_PKG_VERSION").into(),
            build_hash: option_env!("PMACS_GIT_HASH").map(String::from),
            instance_name,
            uptime_secs: started.elapsed().as_secs(),
            working_directory: std::env::current_dir()
                .ok()
                .map(|p| p.to_string_lossy().into_owned())
                .unwrap_or_default(),
        }
    }
}

/// Capabilities the instance advertises to attaching frontends.
///
/// Empty for v0.1; the type exists so that adding capabilities in v0.2+
/// is not a breaking-change. Symmetric with [`FrontendCapabilities`].
///
/// T M10.5: added `multi_frontend` and `crdt_replica` bits with
/// `#[serde(default)]` so v1 wire bytes still deserialize. The
/// negotiation logic (which side advertises what, and what the
/// instance does with mismatches) is M10.7 scope; M10.5 just makes
/// the bit positions stable in the wire format.
///
/// T M10.5/8: bit defaults evolve with the substrate.
///
/// - M10.5 declared the bits with `#[serde(default)]` so v1 wire
///   bytes deserialize forward-compatibly. M10.5–M10.7 set both bits
///   to `false` so a frontend declaring `multi_frontend: true` got
///   `Goodbye(CapabilityMismatch)` — the multi-frontend path
///   wasn't actually wired yet.
/// - **T M10.8 Day 4 flip**: the instance's `multi_frontend` and
///   `crdt_replica` defaults flip to `true`. This is the "M10.8 enables
///   multi-frontend" moment — the underlying dispatcher (Day 3) and
///   broadcast routing (Day 4) support both capabilities, so the
///   instance advertises them.
///
/// The frontend-side defaults remain `false` (a frontend that omits
/// the field is conservatively treated as not supporting the
/// capability; matches v0.1 wire-format semantics).
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct InstanceCapabilities {
    /// T M10.5: instance can host multi-frontend sessions on the
    /// same buffer (per `§sec:m10-collab`). T M10.8 Day 4: default
    /// flipped to `true` — the dispatcher supports multiple
    /// attached frontends.
    #[serde(default = "default_true")]
    pub multi_frontend: bool,
    /// T M10.5: instance can broadcast `InstanceMessage::CrdtOp`
    /// messages. T M10.8 Day 4: default flipped to `true` — the
    /// broadcast routing for CRDT ops wires up in this milestone.
    #[serde(default = "default_true")]
    pub crdt_replica: bool,
    /// T M11.1: instance can produce the semantic-frontend variant
    /// family (`InstanceMessage::StyleSpans` … `ResourceOffer`) and
    /// consume `FrontendEvent::Viewport`.
    ///
    /// T M11.1 declared the bit position + negotiation mechanics with
    /// the default `false` (no producer yet). T M11.2 landed the
    /// instance-side projection seam (`SemanticRenderState`) and
    /// flipped the default to `cfg!(feature = "crdt")`: the instance
    /// now advertises `semantic_render` on CRDT builds. It tracks the
    /// `crdt` feature rather than being unconditional because the
    /// negotiation dependency rule makes a semantic session
    /// necessarily a text replica — a non-CRDT build can host
    /// neither. See [`Default`] impl below.
    #[serde(default)]
    pub semantic_render: bool,
}

// Clippy in non-CRDT builds notes that `cfg!(feature = "crdt")`
// evaluates to `false`, making this impl derivable. In CRDT builds
// the values are `true`, so the impl is genuinely manual. Allow.
#[allow(clippy::derivable_impls)]
impl Default for InstanceCapabilities {
    fn default() -> Self {
        // T M10.10 — the `crdt_replica` default tracks the `crdt`
        // Cargo feature. A daemon built without the `crdt` feature
        // can't honor a `crdt_replica: true` negotiation (the
        // CRDT-handling code paths are conditionally compiled out
        // — `send_buffer_snapshots`, `apply_remote_crdt_op`, the
        // dispatcher's CursorByte emit). Advertising `true`
        // unconditionally would be wire-protocol false advertising.
        //
        // `multi_frontend` is conceptually independent of CRDT but
        // in M10.10's architecture every multi-frontend participant
        // is also a CRDT replica; gating both on the same feature
        // keeps the daemon's advertised capabilities consistent
        // with what it can actually do.
        //
        // T M11.1 declared `semantic_render` defaulting to `false`
        // unconditionally — no projection-seam producer existed, so
        // advertising it would have been wire-protocol false
        // advertising (the M10.5→M10.7 "bits false until the path is
        // wired" discipline).
        //
        // T M11.2 — **the flip**: the instance-side projection seam
        // (`SemanticRenderState`, the producer) has landed and the
        // dispatcher selects it per session, so the instance now
        // advertises `semantic_render`. It tracks `cfg!(feature =
        // "crdt")` like `crdt_replica` because the negotiation
        // dependency rule makes a semantic session necessarily a
        // text replica; a non-CRDT build can host neither. This is
        // the "M11.2 enables semantic" moment, exactly analogous to
        // the M10.8 Day-4 multi_frontend/crdt_replica flip.
        Self {
            multi_frontend: cfg!(feature = "crdt"),
            crdt_replica: cfg!(feature = "crdt"),
            semantic_render: cfg!(feature = "crdt"),
        }
    }
}

#[allow(clippy::missing_const_for_fn)]
fn default_true() -> bool {
    true
}

/// Capabilities the frontend advertises to the instance.
///
/// All bools default to `false` so a frontend that omits a field via an
/// older `AttachRequest` is conservatively treated as not supporting
/// the capability. New capabilities added in v0.2+ get
/// `#[serde(default)]` so old wire bytes still deserialize.
// A capability set is exactly the case `struct_excessive_bools` warns
// against — but each flag is independent and the alternative (an enum
// or bitset) loses the per-field `#[serde(default)]` semantics that
// make schema evolution work.
#[allow(clippy::struct_excessive_bools)]
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FrontendCapabilities {
    /// Frontend understands DEC 2026 `BeginSynchronizedUpdate` /
    /// `EndSynchronizedUpdate` markers. Instance strips them when false.
    #[serde(default)]
    pub synchronized_output: bool,
    /// Frontend can render Unicode beyond the Basic Multilingual Plane.
    /// Instance can substitute a fallback glyph when false.
    #[serde(default)]
    pub unicode_smp: bool,
    /// Frontend supports 24-bit color (truecolor SGR sequences).
    /// Instance maps to the 256-color palette when false.
    #[serde(default)]
    pub true_color: bool,
    /// Frontend captures and forwards mouse events.
    #[serde(default)]
    pub mouse: bool,
    /// Frontend supports bracketed paste — distinguishes pasted bytes
    /// from typed bytes. Instance treats all input as keystrokes when false.
    #[serde(default)]
    pub bracketed_paste: bool,
    /// Optional human-readable terminal identifier for logs and
    /// debugging only. The instance does not branch on this value;
    /// branching is done on the explicit capability bits above.
    #[serde(default)]
    pub terminal_kind: Option<String>,
    /// T M10.5: frontend can participate in multi-frontend sessions
    /// (per `§sec:m10-collab`). false for v0.1 frontends — they
    /// attach as single-frontend and never receive `CrdtOp` /
    /// `PresenceUpdate` broadcasts. v1.0 frontends opt in via M10.7's
    /// negotiation handshake. M10.5 declares the bit position; M10.7
    /// wires the negotiation.
    ///
    /// Default is `false` — v1 frontends are treated as not
    /// supporting this feature, which matches reality (v1 frontends
    /// have no local CRDT state). A `true` default would have v1
    /// frontends claimed to support features they don't.
    #[serde(default)]
    pub multi_frontend: bool,
    /// T M10.5: frontend can apply incoming `CrdtOp` messages to a
    /// local CRDT state. false for v0.1; v1.0 opts in. M10.7 wires
    /// negotiation; M10.5 declares the bit position.
    #[serde(default)]
    pub crdt_replica: bool,
    /// T M11.1: frontend is a semantic (layout-local) renderer — it
    /// consumes the `InstanceMessage::StyleSpans` … `ResourceOffer`
    /// family and emits `FrontendEvent::Viewport`. false for v0.1 and
    /// v1.0 grid/TUI frontends; a future GPU/GUI frontend opts in.
    ///
    /// A semantic frontend is *required* to also be a text replica:
    /// the semantic frame ships no text, so the frontend must hold
    /// the rope locally via the `crdt_replica` machinery. This
    /// dependency is enforced in [`negotiate_capabilities`], not just
    /// documented — declaring `semantic_render: true` without
    /// `crdt_replica: true` is a capability mismatch, never a silent
    /// degrade.
    #[serde(default)]
    pub semantic_render: bool,
}

/// T M10.7 — the negotiated capability bits for one attached session.
///
/// Computed by [`negotiate_capabilities`] from the frontend's
/// [`FrontendCapabilities`] and the instance's [`InstanceCapabilities`].
/// Each negotiated bit is the AND of the two declared bits. Fields
/// added here in future milestones append at the end with sensible
/// defaults so existing call sites stay valid.
///
/// This is a daemon-internal struct (not on the wire); the
/// negotiation result is communicated to the frontend via the
/// success of the handshake (no capability-mismatch `Goodbye`) and
/// the instance's behavior thereafter.
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct NegotiatedCapabilities {
    /// Session is eligible for multi-frontend operation. True iff
    /// both the frontend and the instance declared `multi_frontend =
    /// true`. v0.1 frontends always end up here as `false` (the v0.1
    /// wire format does not carry the field; `#[serde(default)]`
    /// makes the deserialized value `false`).
    pub multi_frontend: bool,
    /// Session can produce/consume `InstanceMessage::CrdtOp` /
    /// `FrontendEvent::CrdtOp`. True iff both sides declared
    /// `crdt_replica = true`. The daemon's outgoing-message filter for
    /// `CrdtOp` consults this in M10.8.
    pub crdt_replica: bool,
    /// T M11.1 — session uses the semantic projection: it
    /// produces/consumes the `InstanceMessage::StyleSpans` …
    /// `ResourceOffer` family and `FrontendEvent::Viewport`. True iff
    /// both sides declared `semantic_render = true` *and* the session
    /// also negotiated `crdt_replica = true` (a semantic session is a
    /// text replica; see [`negotiate_capabilities`]). The daemon's
    /// per-session outgoing filter gates the entire semantic family
    /// on this bit — wired with the producer in M11.2.
    pub semantic_render: bool,
}

/// T M10.7 — pure-function capability negotiation.
///
/// For each negotiated bit (`multi_frontend`, `crdt_replica`,
/// `semantic_render`):
///
/// | Frontend wants | Instance has | Result |
/// |----------------|--------------|--------|
/// | `false`        | `false`      | bit `false`, no error |
/// | `false`        | `true`       | bit `false`, no error |
/// | `true`         | `true`       | bit `true`,  no error |
/// | `true`         | `false`      | bit appears in `missing` |
///
/// If any bit ends up in `missing`, the negotiation fails as a whole
/// (returns `Err`). Otherwise the negotiated bits are returned as
/// [`NegotiatedCapabilities`]. The `Err` form gathers ALL missing
/// bits into one `CapabilityMismatch` — one round-trip carries the
/// complete picture rather than serial rejections. Missing bits are
/// ordered `multi_frontend`, `crdt_replica`, `semantic_render` for
/// deterministic wire output.
///
/// # T M11.1 — the `semantic_render ⇒ crdt_replica` dependency
///
/// A semantic-render session ships no text on the semantic frame;
/// the frontend holds the rope locally via the `crdt_replica`
/// machinery (`BufferSnapshot` to bootstrap, `CrdtOp` to stay live).
/// So `semantic_render` is only coherent on a session that also
/// negotiated `crdt_replica`. When the AND-rule would yield
/// `semantic_render = true` but the session did not also negotiate
/// `crdt_replica = true`, this function rejects with
/// `"semantic_render"` in `missing` rather than silently degrading
/// the session to a text-only replica. The rejected identifier is
/// `"semantic_render"` (the capability whose precondition is unmet),
/// not `"crdt_replica"`.
///
/// # Wire-format stability
///
/// The strings emitted into `missing` are exactly the
/// `FrontendCapabilities` field names (`"multi_frontend"`,
/// `"crdt_replica"`). These are stable wire-format identifiers, not
/// human-readable descriptions. User-facing translation is the
/// frontend's responsibility (see [`AttachError`]'s `Display` impl).
/// Renaming a capability bit requires updating both the field name
/// and the missing-string emission here in lockstep.
pub fn negotiate_capabilities(
    frontend: &FrontendCapabilities,
    instance: &InstanceCapabilities,
) -> Result<NegotiatedCapabilities, GoodbyeReason> {
    let mut missing = Vec::new();
    let multi_frontend = match (frontend.multi_frontend, instance.multi_frontend) {
        (true, false) => {
            missing.push("multi_frontend".to_string());
            false
        }
        (a, b) => a && b,
    };
    let crdt_replica = match (frontend.crdt_replica, instance.crdt_replica) {
        (true, false) => {
            missing.push("crdt_replica".to_string());
            false
        }
        (a, b) => a && b,
    };
    let semantic_render = match (frontend.semantic_render, instance.semantic_render) {
        (true, false) => {
            missing.push("semantic_render".to_string());
            false
        }
        (a, b) => a && b,
    };
    // T M11.1 — dependency rule. A semantic session is a text replica
    // (the semantic frame carries no text). If both sides declared
    // `semantic_render` but the session did not also negotiate
    // `crdt_replica`, reject rather than silently degrade. Guard
    // against a duplicate push: the only path where `semantic_render`
    // is already in `missing` is the `(true, false)` arm above, which
    // also sets the local `semantic_render` to false, so the
    // condition below cannot re-fire for that case — but the explicit
    // membership check keeps this robust against future reordering.
    if semantic_render && !crdt_replica && !missing.iter().any(|m| m == "semantic_render") {
        missing.push("semantic_render".to_string());
    }
    let semantic_render = semantic_render && crdt_replica;
    if missing.is_empty() {
        Ok(NegotiatedCapabilities {
            multi_frontend,
            crdt_replica,
            semantic_render,
        })
    } else {
        Err(GoodbyeReason::CapabilityMismatch { missing })
    }
}

/// First message sent by the instance to a freshly-attached frontend.
///
/// Sent immediately after the connection is accepted, before reading
/// the frontend's [`AttachRequest`]. The frontend uses
/// `instance_identity` for status display and `protocol_version` /
/// `instance_capabilities` for compatibility decisions.
///
/// The instance also stamps the `assigned_frontend_id` which the
/// frontend will use as the `FrontendId` on every event it sends.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Hello {
    /// The instance's `PROTOCOL_VERSION`.
    pub protocol_version: u32,
    /// `FrontendId` assigned to this attachment by the instance. The
    /// frontend stamps this onto subsequent events. v0.1 daemons start
    /// allocation at `FrontendId(2)` (1 reserved for the in-process TUI).
    pub assigned_frontend_id: FrontendId,
    /// Instance self-identification (version, name, uptime, cwd).
    pub instance_identity: InstanceIdentity,
    /// Instance capabilities. Empty for v0.1.
    pub instance_capabilities: InstanceCapabilities,
}

/// First message sent by a frontend after receiving [`Hello`].
///
/// Carries the frontend's view of the protocol version, the
/// capabilities it can support, and its initial terminal size. On
/// version mismatch the instance closes with
/// [`GoodbyeReason::VersionMismatch`] and no further messages flow.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct AttachRequest {
    /// The frontend's `PROTOCOL_VERSION`.
    pub protocol_version: u32,
    /// Frontend capabilities. Defaults to all-false if omitted.
    #[serde(default)]
    pub frontend_capabilities: FrontendCapabilities,
    /// The frontend's terminal size at attach time. Authoritative
    /// until the frontend sends a [`FrontendEvent::Resize`]. The
    /// instance uses this for the initial full-grid render.
    pub initial_size: CellSize,
}
