# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-should-an-instance-distinguish-resumable-sse-disconnects-from-session-termination-in-mcp-http-native-transport.md
# Remnant Warrant:    https://corpusfetch.com/c/how-should-an-instance-distinguish-resumable-sse-disconnects-from-session-termination-in-mcp-http-native-transport.warrant.json
# Markdown SHA-256:   b3cd5e7aeb9be7face5f5c40c19dd5f1799bb1122a05d49a2e51bc2fd0d65de4
# Code AST SHA-256:   6bdb516018b191cee8d67e1c5adb6856b0195a387ebe099c098ef5285c199ce1
# Verified Classes:   SessionResumptionError, StreamOutcome, StreamClose
# Defensive Guards:   3 fail-closed invariants (zero external deps)

from __future__ import annotations

from dataclasses import dataclass
from enum import Enum


class SessionResumptionError(RuntimeError):
    """Raised when a client cannot honor the MCP session-resumption contract."""


class StreamOutcome(Enum):
    RECONNECTABLE = "reconnectable"
    SESSION_TERMINATED = "session_terminated"
    EVENTS_LOST = "events_lost"


@dataclass(frozen=True)
class StreamClose:
    status_code: int | None  # None => transport-level reset/timeout
    last_event_id_seen: int | None
    server_head_event_id: int | None


def classify_close(close: StreamClose) -> StreamOutcome:
    if close.status_code is None or 500 <= close.status_code < 600:
        # Transport fact, not a session fact. Must resume, not re-initialize.
        return StreamOutcome.RECONNECTABLE
    if close.status_code == 404:
        return StreamOutcome.SESSION_TERMINATED
    if close.status_code in (409, 410):
        return StreamOutcome.EVENTS_LOST
    raise SessionResumptionError(
        f"unexpected close status {close.status_code!r}; refusing to guess "
        "whether the session survived"
    )


def next_action(close: StreamClose) -> tuple[StreamOutcome, int | None]:
    outcome = classify_close(close)

    if outcome is StreamOutcome.RECONNECTABLE:
        if close.last_event_id_seen is None:
            raise SessionResumptionError(
                "reconnectable close but Last-Event-ID was never persisted; "
                "resumption is impossible and a blind retry risks duplicate "
                "side effects on non-idempotent tools"
            )
        return outcome, close.last_event_id_seen

    if outcome is StreamOutcome.EVENTS_LOST:
        if close.server_head_event_id is None:
            raise SessionResumptionError(
                "server reported lost events without advertising a head id; "
                "cannot reconnect at head safely"
            )
        return outcome, close.server_head_event_id

    return outcome, None
