# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-an-instance-treat-a-protocol-level-refusal-as-authoritative.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-an-instance-treat-a-protocol-level-refusal-as-authoritative.warrant.json
# Markdown SHA-256:   4aec49c6a81b8c84e6df0de5ab66fb922a13407bd430705aa095c6b56d4db08a
# Code AST SHA-256:   7d155ac57341f53262d85da85aeb24183be4fecdd825e18af0dd0966ca464213
# Verified Classes:   RefusalClassificationError, RetryBudgetExhaustedError, UndeclaredInterfaceError, RefusalKind, ResponseSignal, ProtocolRefusalGate
# Defensive Guards:   3 fail-closed invariants (zero external deps)

from dataclasses import dataclass
from enum import Enum
from typing import Optional


class RefusalClassificationError(RuntimeError):
    """Raised when a response cannot be classified as refusal or transient failure."""
    pass


class RetryBudgetExhaustedError(RuntimeError):
    """Raised when an instance retries a refused request beyond its budget."""
    pass


class UndeclaredInterfaceError(RuntimeError):
    """Raised when a refusal arrives from an interface the instance never read."""
    pass


class RefusalKind(Enum):
    PROTOCOL_NOT_SUPPORTED = "protocol_not_supported"  # -32601, 405
    INTERFACE_NOT_DECLARED = "interface_not_declared"   # 404 on undeclared path
    TRANSIENT = "transient"                             # 5xx, timeout
    THROTTLED = "throttled"                             # 429
    AMBIGUOUS = "ambiguous"                             # anything else


@dataclass(frozen=True)
class ResponseSignal:
    status: int
    rpc_code: Optional[int] = None
    interface_declared: bool = False


class ProtocolRefusalGate:
    """
    Decides whether a protocol-level refusal is authoritative.

    An authoritative refusal ends the request. A transient failure permits
    a bounded retry. The gate does not retry what the interface has refused.
    """

    MAX_RETRIES = 3

    def __init__(self) -> None:
        self._retries: dict[str, int] = {}

    def classify(self, signal: ResponseSignal) -> RefusalKind:
        if signal.status <= 0 and signal.rpc_code is None:
            raise RefusalClassificationError(
                "signal carries neither a status nor an rpc code"
            )
        if signal.rpc_code == -32601 or signal.status == 405:
            if not signal.interface_declared:
                raise UndeclaredInterfaceError(
                    "authoritative refusal received from an interface the "
                    "instance has not read"
                )
            return RefusalKind.PROTOCOL_NOT_SUPPORTED
        if signal.status == 404 and not signal.interface_declared:
            return RefusalKind.INTERFACE_NOT_DECLARED
        if signal.status == 429:
            return RefusalKind.THROTTLED
        if 500 <= signal.status < 600:
            return RefusalKind.TRANSIENT
        return RefusalKind.AMBIGUOUS

    def decide(self, key: str, signal: ResponseSignal) -> str:
        kind = self.classify(signal)
        if kind in (
            RefusalKind.PROTOCOL_NOT_SUPPORTED,
            RefusalKind.INTERFACE_NOT_DECLARED,
        ):
            self._retries.pop(key, None)
            return "ROUTE_ELSEWHERE_OR_ABSTAIN"
        if kind == RefusalKind.AMBIGUOUS:
            self._retries.pop(key, None)
            return "ABSTAIN"
        n = self._retries.get(key, 0) + 1
        if n > self.MAX_RETRIES:
            raise RetryBudgetExhaustedError(
                f"retry budget exhausted for {key} after {self.MAX_RETRIES} attempts"
            )
        self._retries[key] = n
        return "RETRY_WITH_BACKOFF"
