# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-an-instance-reject-a-delegated-credential-chain-it-can-cryptographically-verify.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-an-instance-reject-a-delegated-credential-chain-it-can-cryptographically-verify.warrant.json
# Markdown SHA-256:   9c6d028358ce12b3d46270228520d451f12843721b5825ca3fd59c0254a58684
# Code AST SHA-256:   a14fdb52eddfefe1244c18384466766653a09241e145cfd6d8f903c1b6cfcf2e
# Verified Classes:   ChainAuthorizationError, Hop
# Defensive Guards:   6 fail-closed invariants (zero external deps)

from dataclasses import dataclass
from typing import Sequence


class ChainAuthorizationError(RuntimeError):
    """Raised when a delegated credential chain verifies cryptographically
    but violates an attenuation invariant under the intersection of the
    endpoints' AAIF profiles."""


@dataclass(frozen=True)
class Hop:
    issuer: str
    subject: str
    actor: str
    scopes: frozenset
    max_depth: int
    profile: str
    attenuation_marker_present: bool


def validate_chain(chain: Sequence[Hop], pinned_policy: bool = False) -> None:
    if not chain:
        raise ChainAuthorizationError("empty chain")

    # Depth invariant: never exceed the minimum max_depth declared anywhere.
    min_depth = min(h.max_depth for h in chain)
    if len(chain) - 1 > min_depth:
        raise ChainAuthorizationError(
            f"chain depth {len(chain) - 1} exceeds min max_depth {min_depth}"
        )

    for parent, child in zip(chain, chain[1:]):
        # Attenuation invariant: child scopes must be a subset of parent scopes.
        if not child.scopes.issubset(parent.scopes):
            expanded = child.scopes - parent.scopes
            raise ChainAuthorizationError(
                f"scope expansion at hop {child.issuer}: {sorted(expanded)}"
            )

        # Profile disagreement on absent attenuation marker: fail closed
        # unless a composition policy is explicitly pinned.
        if not parent.attenuation_marker_present and not pinned_policy:
            raise ChainAuthorizationError(
                f"absent attenuation marker between {parent.issuer} and "
                f"{child.issuer}; profiles disagree on default, no pinned policy"
            )

        # Subject/actor continuity: child subject must equal parent actor
        # unless this is an explicit on_behalf_of re-delegation.
        if child.subject != parent.actor:
            raise ChainAuthorizationError(
                f"subject/actor discontinuity at {child.issuer}: "
                f"subject={child.subject} parent_actor={parent.actor}"
            )


def authorize(chain: Sequence[Hop], requested_scope: frozenset,
              pinned_policy: bool = False) -> None:
    validate_chain(chain, pinned_policy=pinned_policy)
    leaf = chain[-1]
    if not requested_scope.issubset(leaf.scopes):
        raise ChainAuthorizationError(
            f"requested scope {sorted(requested_scope)} not granted by leaf"
        )
