# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/under-what-conditions-must-an-instance-reject-a-delegated-aaif-credential-whose-delegation-chain-is-cryptographically-valid.md
# Remnant Warrant:    https://corpusfetch.com/c/under-what-conditions-must-an-instance-reject-a-delegated-aaif-credential-whose-delegation-chain-is-cryptographically-valid.warrant.json
# Markdown SHA-256:   636aa85d3784fe7e09905e98430f5524715535c387f89b8638a5d72989c370de
# Code AST SHA-256:   52d769b2961b052d4932a066f074ef22e20b4ae025ea73d03b9792ba40b6b43b
# Verified Classes:   DelegationChainRejected
# Defensive Guards:   5 fail-closed invariants (zero external deps)

class DelegationChainRejected(RuntimeError):
    """Raised when an AAIF delegation chain verifies cryptographically but is
    operationally unauthorized for the presenting instance."""
    pass


def authorize_delegated_action(chain, instance_aud, action, resource):
    # chain: list of links, root first, terminal last. Each link is a dict with
    # keys: aud, nbf, exp, delegation_constraints (optional), max_hops (optional).
    if not chain:
        raise DelegationChainRejected("empty delegation chain")

    # Per-link audience: the terminal link must name this instance.
    terminal = chain[-1]
    if instance_aud not in terminal.get("aud", []):
        raise DelegationChainRejected(
            f"terminal link audience {terminal.get('aud')!r} does not include {instance_aud!r}"
        )

    # Constraint intersection, inherited downward. Root absence == unconstrained;
    # any non-root absence inherits the accumulated set (never widens it).
    effective = None  # None means "unconstrained so far" (root only)
    for idx, link in enumerate(chain):
        constraints = link.get("delegation_constraints")
        if constraints is None:
            if idx == 0:
                effective = None
            # else: inherit `effective` unchanged -- do NOT treat as unconstrained
        else:
            if effective is None:
                effective = set(constraints.get("allowed_actions", []))
            else:
                effective = effective & set(constraints.get("allowed_actions", []))

        # Hop budget: declared link is hop 0; terminal is hop len(chain)-1-idx.
        max_hops = link.get("max_hops")
        if max_hops is not None:
            hops_below = (len(chain) - 1) - idx
            if hops_below > max_hops:
                raise DelegationChainRejected(
                    f"link {idx} declares max_hops={max_hops} but chain has "
                    f"{hops_below} hops below it"
                )

    if effective is not None and action not in effective:
        raise DelegationChainRejected(
            f"action {action!r} not in intersected allowed_actions {sorted(effective)}"
        )

    # Temporal nesting: each link's window contained in predecessor's.
    for idx in range(1, len(chain)):
        parent, child = chain[idx - 1], chain[idx]
        if child["nbf"] < parent["nbf"] or child["exp"] > parent["exp"]:
            raise DelegationChainRejected(
                f"link {idx} window [{child['nbf']},{child['exp']}] not nested in "
                f"parent window [{parent['nbf']},{parent['exp']}]"
            )

    return True
