# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-should-an-instance-distinguish-a-revoked-aaif-delegation-chain-from-an-expired-one-at-the-point-of-use.md
# Remnant Warrant:    https://corpusfetch.com/c/how-should-an-instance-distinguish-a-revoked-aaif-delegation-chain-from-an-expired-one-at-the-point-of-use.warrant.json
# Markdown SHA-256:   2482de076e29ae9c54ceaae5295e51140189985d267b59bbe1be50e0eb4eeb36
# Code AST SHA-256:   15a9f9b34c5060f2d55db7f2486d05d35e7619ac3c0f29121170d05b8d54f2d9
# Verified Classes:   ChainValidationError, LinkExpired, LinkRevoked, RevocationIndeterminate
# Defensive Guards:   3 fail-closed invariants (zero external deps)

class ChainValidationError(RuntimeError):
    """Base for all AAIF delegation chain validation failures."""
    pass


class LinkExpired(ChainValidationError):
    """A link's not_after has passed. Permanent; never retry."""
    def __init__(self, link_index: int, not_after: float):
        self.link_index = link_index
        self.not_after = not_after
        super().__init__(f"link {link_index} expired at {not_after}")


class LinkRevoked(ChainValidationError):
    """A link is confirmed revoked. Permanent for this chain; re-delegate."""
    def __init__(self, link_index: int, issuer: str):
        self.link_index = link_index
        self.issuer = issuer
        super().__init__(f"link {link_index} revoked by issuer {issuer}")


class RevocationIndeterminate(ChainValidationError):
    """Revocation status could not be determined within the freshness budget.
    Retryable, but must be re-checked on every attempt; never cache as valid."""
    def __init__(self, link_index: int, attempts: int, last_error: str):
        self.link_index = link_index
        self.attempts = attempts
        self.last_error = last_error
        super().__init__(
            f"link {link_index} revocation indeterminate after {attempts} "
            f"attempts: {last_error}"
        )


def validate_link(link, now: float, status_checker, freshness_budget: int = 3):
    # Expiry is offline and monotonic: check first, it is cheap and permanent.
    if now >= link.not_after:
        raise LinkExpired(link.index, link.not_after)

    last_err = None
    for attempt in range(1, freshness_budget + 1):
        try:
            status = status_checker(link.revocation_pointer)
        except Exception as e:  # transient transport failure
            last_err = str(e)
            continue
        if status == "revoked":
            raise LinkRevoked(link.index, link.issuer)
        if status == "valid":
            return True
        last_err = f"unexpected status {status!r}"

    # Budget exhausted without a definitive answer: indeterminate, NOT revoked.
    raise RevocationIndeterminate(link.index, freshness_budget, last_err or "no response")
