# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-an-instance-of-a-crypto-exchange-quote-be-treated-as-stale-rather-than-merely-delayed.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-an-instance-of-a-crypto-exchange-quote-be-treated-as-stale-rather-than-merely-delayed.warrant.json
# Markdown SHA-256:   ec102abaf3604eaca31551f4ff3de2a95e0fcdd64bb8bfa093310f76162dc86a
# Code AST SHA-256:   fd8d9139d0f03b52b4e6fdf57f83a24ca8937697253ba4580e17fe72b7d292df
# Verified Classes:   StaleQuoteError, Quote, QuoteGate
# Defensive Guards:   2 fail-closed invariants (zero external deps)

class StaleQuoteError(RuntimeError):
    """Raised when a quote is superseded by the exchange sequence, not merely delayed."""
    pass


class Quote:
    def __init__(self, symbol: str, seq: int, t_exchange_ns: int, bid: float, ask: float):
        self.symbol = symbol
        self.seq = seq
        self.t_exchange_ns = t_exchange_ns
        self.bid = bid
        self.ask = ask


class QuoteGate:
    def __init__(self, delay_budget_ns: int):
        self.delay_budget_ns = delay_budget_ns
        self.seq_watermark = {}  # symbol -> highest observed seq

    def observe_sequence(self, symbol: str, seq: int) -> None:
        prev = self.seq_watermark.get(symbol)
        if prev is None or seq > prev:
            self.seq_watermark[symbol] = seq

    def classify(self, quote: Quote, now_ns: int) -> str:
        watermark = self.seq_watermark.get(quote.symbol)
        if watermark is None:
            # No sequence information: time-only, lower confidence.
            age = now_ns - quote.t_exchange_ns
            return "suspect" if age >= self.delay_budget_ns else "delayed_valid"
        if watermark > quote.seq:
            return "stale"
        age = now_ns - quote.t_exchange_ns
        if age >= self.delay_budget_ns:
            # No sequence advance but large age: possible feed partition.
            return "suspect"
        return "delayed_valid"

    def assert_usable(self, quote: Quote, now_ns: int) -> None:
        state = self.classify(quote, now_ns)
        if state == "stale":
            raise StaleQuoteError(
                f"quote {quote.symbol} seq={quote.seq} superseded by watermark "
                f"{self.seq_watermark.get(quote.symbol)}; book has moved, do not price"
            )
        if state == "suspect":
            raise StaleQuoteError(
                f"quote {quote.symbol} seq={quote.seq} unadvanced beyond delay budget "
                f"({now_ns - quote.t_exchange_ns}ns); possible feed partition, treat as stale"
            )
        # delayed_valid: caller may proceed with widened slippage.
        return None
