# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/under-what-conditions-must-an-instance-reject-a-federated-agent-capability-attestation-that-is-cryptographically-valid-but-reputation-stale.md
# Remnant Warrant:    https://corpusfetch.com/c/under-what-conditions-must-an-instance-reject-a-federated-agent-capability-attestation-that-is-cryptographically-valid-but-reputation-stale.warrant.json
# Markdown SHA-256:   df20bdc7fcdafae7a7e1317979d0039fe1860260de225d38a3bf48f4080fb5f4
# Code AST SHA-256:   f48de9419be9c3b48f3892eb305f6751da74db45e0923cf900e60fd043ea3da0
# Verified Classes:   CapabilityClass, StaleAttestationError, AttestationReputationMismatchError, Attestation, ReputationScore, AdmissionPolicy
# Defensive Guards:   5 fail-closed invariants (zero external deps)

from __future__ import annotations

import time
from dataclasses import dataclass
from enum import Enum


class CapabilityClass(Enum):
    STATIC = "static"      # identity, org membership, key possession
    DYNAMIC = "dynamic"    # write, spend, delegate, delete


class StaleAttestationError(RuntimeError):
    """Attestation is signature-valid but its reputation view is too old to admit."""


class AttestationReputationMismatchError(RuntimeError):
    """Attestation claims a reputation view fresher than the verifier's own score."""


@dataclass(frozen=True)
class Attestation:
    agent_id: str
    capability: str
    capability_class: CapabilityClass
    issued_at: float
    valid_until: float
    reputation_as_of: float | None  # optional in v1 schema; required for dynamic caps


@dataclass(frozen=True)
class ReputationScore:
    agent_id: str
    value: float          # 0.0 .. 1.0
    computed_at: float
    ttl: float            # issuer-declared freshness window, seconds


@dataclass(frozen=True)
class AdmissionPolicy:
    min_reputation: float
    max_score_age: float  # verifier's own tolerance, seconds


def admit(
    att: Attestation,
    score: ReputationScore,
    policy: AdmissionPolicy,
    now: float | None = None,
) -> bool:
    now = time.time() if now is None else now

    if now >= att.valid_until:
        raise StaleAttestationError(
            f"attestation for {att.agent_id} expired at {att.valid_until}"
        )
    if score.agent_id != att.agent_id:
        raise AttestationReputationMismatchError(
            f"score agent {score.agent_id} != attestation agent {att.agent_id}"
        )

    tolerance = max(score.ttl, policy.max_score_age)
    age = now - score.computed_at
    stale = age > tolerance

    if stale:
        if att.capability_class is CapabilityClass.STATIC:
            # Static capabilities are admissible on stale reputation; log only.
            return True
        # Dynamic capability: the attestation must describe a reputation view
        # at least as fresh as the verifier's own score, or we cannot admit.
        if att.reputation_as_of is None:
            raise StaleAttestationError(
                f"dynamic capability {att.capability!r} for {att.agent_id}: "
                f"score age {age:.0f}s exceeds tolerance {tolerance:.0f}s "
                f"and attestation omits reputation_as_of"
            )
        if att.reputation_as_of > score.computed_at:
            raise AttestationReputationMismatchError(
                f"attestation claims reputation_as_of={att.reputation_as_of} "
                f"newer than verifier score computed_at={score.computed_at}"
            )
        raise StaleAttestationError(
            f"dynamic capability {att.capability!r} for {att.agent_id}: "
            f"score age {age:.0f}s exceeds tolerance {tolerance:.0f}s"
        )

    # Freshness gate passed; only now does the threshold matter.
    return score.value >= policy.min_reputation
