# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-structural-signals-indicate-a-broken-aaif-delegation-chain-before-signature-verification-fails.md
# Remnant Warrant:    https://corpusfetch.com/c/what-structural-signals-indicate-a-broken-aaif-delegation-chain-before-signature-verification-fails.warrant.json
# Markdown SHA-256:   04bbc3f9a1b9db70d6dae188e1da1dc12a17747d2d93069ce83d0e1faf94f0c7
# Code AST SHA-256:   7352173189e3097472ae7ce32ca35e40edb97c7ef2824e00ac9aa0a6d6729447
# Verified Classes:   DelegationChainStructuralError, Hop
# Defensive Guards:   5 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from typing import Optional


class DelegationChainStructuralError(RuntimeError):
    """Raised when an AAIF delegation chain is structurally broken
    before signature verification is attempted."""
    pass


@dataclass
class Hop:
    credential_id: str
    parent_credential_id: Optional[str]
    grants: set[str]                 # e.g. {"write:acme/billing"}
    constraints: dict[str, str] = field(default_factory=dict)
    re_grant_root: Optional[str] = None
    namespace: str = ""


def check_chain_structure(hops: list[Hop], max_depth: int) -> None:
    if len(hops) > max_depth:
        raise DelegationChainStructuralError(
            f"chain depth {len(hops)} exceeds root max_delegation_depth {max_depth}"
        )

    for prev, cur in zip(hops, hops[1:]):
        if cur.parent_credential_id != prev.credential_id:
            raise DelegationChainStructuralError(
                f"hop {cur.credential_id} does not declare parent {prev.credential_id}"
            )

        widened = not cur.grants.issubset(prev.grants)
        if widened and cur.re_grant_root is None:
            raise DelegationChainStructuralError(
                f"scope widened at {cur.credential_id} without re_grant_root"
            )

        dropped = set(prev.constraints) - set(cur.constraints)
        waived = {c for c in dropped if cur.constraints.get(c) == "waived_by"}
        if dropped - waived:
            raise DelegationChainStructuralError(
                f"constraint(s) {sorted(dropped - waived)} dropped at "
                f"{cur.credential_id} without waiver"
            )

        if not cur.namespace.startswith(prev.namespace.rstrip("/*")):
            raise DelegationChainStructuralError(
                f"namespace drift: {cur.namespace} not under {prev.namespace}"
            )


def issue_delegated_credential(hops: list[Hop], max_depth: int) -> str:
    check_chain_structure(hops, max_depth)
    return hops[-1].credential_id
