# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/where-must-an-instance-reconcile-the-sse-event-id-cursor-before-resuming-an-mcp-streamable-http-session.md
# Remnant Warrant:    https://corpusfetch.com/c/where-must-an-instance-reconcile-the-sse-event-id-cursor-before-resuming-an-mcp-streamable-http-session.warrant.json
# Markdown SHA-256:   0303cc82fb9e5c0a6feb2f33b9b21319d973108226548df950fea8577867f2f2
# Code AST SHA-256:   d427e819d402d450c8b9eb8b94aa1f4b3105b333439bf7a8ae55735e96655c94
# Verified Classes:   ResumptionCursorError, ResumeState
# Defensive Guards:   3 fail-closed invariants (zero external deps)

from dataclasses import dataclass
from typing import Optional


class ResumptionCursorError(RuntimeError):
    """Raised when an SSE replay cannot be proven contiguous with the client cursor."""
    pass


@dataclass
class ResumeState:
    session_id: str
    last_event_id: Optional[int]
    session_lifetime_monotonic: bool


def validate_resumption(state: ResumeState, first_replayed_id: Optional[int]) -> None:
    """
    Enforce the contiguity invariant before consuming a resumed MCP SSE stream.

    - If the server did not negotiate session-lifetime monotonic event IDs,
      we may not trust Last-Event-ID at all.
    - If we sent a cursor, the first replayed event MUST be exactly cursor+1.
      Anything else (None, a reset to 1, or a forward gap) is unrecoverable
      within this session and must force full re-initialization.
    """
    if not state.session_lifetime_monotonic:
        raise ResumptionCursorError(
            f"session {state.session_id} did not negotiate monotonic event IDs; "
            "refusing to resume on Last-Event-ID"
        )

    if state.last_event_id is None:
        # Fresh stream, no cursor sent; nothing to prove.
        return

    if first_replayed_id is None:
        raise ResumptionCursorError(
            f"session {state.session_id}: server opened stream with no replayed "
            f"event for cursor {state.last_event_id}; buffer likely expired"
        )

    expected = state.last_event_id + 1
    if first_replayed_id != expected:
        raise ResumptionCursorError(
            f"session {state.session_id}: non-contiguous replay; expected "
            f"event {expected}, got {first_replayed_id}. Event-ID namespace "
            "likely reset or gap is unrecoverable; fall back to re-initialize."
        )
