# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/under-what-conditions-must-an-instance-refuse-delegated-aaif-credentials-at-the-token-exchange-boundary.md
# Remnant Warrant:    https://corpusfetch.com/c/under-what-conditions-must-an-instance-refuse-delegated-aaif-credentials-at-the-token-exchange-boundary.warrant.json
# Markdown SHA-256:   836bc214a8d568c1da2f579b1806f9a269eb09847b6acfb0b79ab563d0b32f04
# Code AST SHA-256:   4d06151b37301070c10f83fc217cc2b71a131ae2faa8e6e93abe0a366eafcf56
# Verified Classes:   DelegationBoundaryError, Hop, DelegatedCredential
# Defensive Guards:   5 fail-closed invariants (zero external deps)

from dataclasses import dataclass
from typing import Sequence


class DelegationBoundaryError(RuntimeError):
    """Raised when a delegated AAIF credential cannot legally be exchanged."""
    pass


@dataclass(frozen=True)
class Hop:
    principal: str
    delegated_to: str
    scopes: frozenset
    max_delegation_depth: int


@dataclass(frozen=True)
class DelegatedCredential:
    chain: Sequence[Hop]
    audience: frozenset
    expires_in_s: float


EXCHANGE_RTT_BUDGET_S = 2.0


def assert_exchangeable(cred: DelegatedCredential, requested_scopes: frozenset,
                        self_id: str) -> frozenset:
    """Fail fast on structurally unsatisfiable delegation. Returns effective scope."""
    if not cred.chain:
        raise DelegationBoundaryError("empty delegation chain")

    depth = len(cred.chain)
    max_depth = cred.chain[0].max_delegation_depth
    if depth >= max_depth:
        raise DelegationBoundaryError(
            f"delegation depth {depth} at limit {max_depth}; cannot append hop"
        )

    if self_id not in cred.audience:
        raise DelegationBoundaryError(
            f"audience {sorted(cred.audience)} does not include self {self_id!r}"
        )

    if cred.expires_in_s < EXCHANGE_RTT_BUDGET_S:
        raise DelegationBoundaryError(
            f"remaining lifetime {cred.expires_in_s}s below RTT budget"
        )

    effective = frozenset.intersection(*(h.scopes for h in cred.chain))
    if not requested_scopes <= effective:
        missing = requested_scopes - effective
        raise DelegationBoundaryError(
            f"requested scopes {sorted(missing)} not in chain intersection "
            f"{sorted(effective)}"
        )
    return effective
