# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/why-does-an-instance-fail-delegated-credential-validation-when-an-aaif-trust-chain-crosses-an-issuer-boundary-mid-scope.md
# Remnant Warrant:    https://corpusfetch.com/c/why-does-an-instance-fail-delegated-credential-validation-when-an-aaif-trust-chain-crosses-an-issuer-boundary-mid-scope.warrant.json
# Markdown SHA-256:   23f94bada1b1b040988cb5a7026f5e9e950c313218c9d7710bdec7d69821a684
# Code AST SHA-256:   c183fa6f82daf86bedf5d00618e79636aa74cfb1f8ae83088e940d96307f8425
# Verified Classes:   TrustChainTruncationError, Hop
# Defensive Guards:   4 fail-closed invariants (zero external deps)

from dataclasses import dataclass
from typing import Optional


class TrustChainTruncationError(RuntimeError):
    """Raised when a delegated credential's trust chain is not contiguous
    from the verifier's anchor and truncation policy forbids silent acceptance."""
    pass


@dataclass(frozen=True)
class Hop:
    issuer: str
    granted_scope: frozenset
    authorized_by_parent: bool  # edge from parent issuer to this issuer


def effective_scope(
    chain: list[Hop],
    verifier_anchor: str,
    policy: str = "truncate",
) -> tuple[frozenset, Optional[str]]:
    """Compute effective scope under AAIF cross-issuer rules.

    Returns (scope, truncated_at). truncated_at is the issuer of the last
    trusted hop, or None if the full chain was trust-contiguous.
    """
    if not chain:
        raise TrustChainTruncationError("empty delegation chain")

    # Walk from root; the first hop must be anchored or reachable from anchor.
    scope = chain[0].granted_scope
    last_trusted = chain[0].issuer
    trust_broken = chain[0].issuer != verifier_anchor

    for hop in chain[1:]:
        if trust_broken:
            # Downstream narrowing is unverifiable; discard it.
            continue
        if not hop.authorized_by_parent:
            trust_broken = True
            if policy == "strict":
                raise TrustChainTruncationError(
                    f"issuer {hop.issuer} not authorized by parent; "
                    f"anchor={verifier_anchor}, last_trusted={last_trusted}"
                )
            continue
        # Trust-contiguous: apply narrowing (intersection).
        new_scope = scope & hop.granted_scope
        if not new_scope <= scope:
            raise TrustChainTruncationError(
                f"hop {hop.issuer} widened scope; AAIF forbids widening"
            )
        scope = new_scope
        last_trusted = hop.issuer

    return scope, (last_trusted if trust_broken else None)


def authorize_request(
    chain: list[Hop],
    verifier_anchor: str,
    requested: frozenset,
    policy: str = "truncate",
) -> bool:
    scope, truncated_at = effective_scope(chain, verifier_anchor, policy)
    if truncated_at is not None and policy == "strict":
        raise TrustChainTruncationError(
            f"chain truncated at {truncated_at}; strict policy rejects"
        )
    return requested <= scope
