# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-does-an-instance-detect-a-compromised-federation-trust-anchor-when-the-did-document-is-served-from-a-resolver-the-verifier-itself-controls.md
# Remnant Warrant:    https://corpusfetch.com/c/how-does-an-instance-detect-a-compromised-federation-trust-anchor-when-the-did-document-is-served-from-a-resolver-the-verifier-itself-controls.warrant.json
# Markdown SHA-256:   4a32be8716bab46f5e7547849c1a7ebab4e3ca63748c8c4ac58182ea3fec41b6
# Code AST SHA-256:   c0cf6ba06751458b562e44c3ddc9f2449ab8b8a186529f3db6ede546544318f5
# Verified Classes:   AnchorFreshnessError, AnchorObservation, AnchorState, FederationVerifier
# Defensive Guards:   4 fail-closed invariants (zero external deps)

import time
from dataclasses import dataclass, field
from typing import Optional


class AnchorFreshnessError(RuntimeError):
    """Raised when a federation trust anchor cannot be proven fresh."""
    pass


@dataclass
class AnchorObservation:
    did: str
    version_id: Optional[int]
    verification_methods: frozenset
    resolved_at: float


@dataclass
class AnchorState:
    last_seen_version: dict = field(default_factory=dict)
    last_seen_methods: dict = field(default_factory=dict)


class FederationVerifier:
    def __init__(self, cache_ttl: float = 300.0):
        self.state = AnchorState()
        self.cache_ttl = cache_ttl

    def validate_anchor(
        self,
        obs: AnchorObservation,
        independent_sources: int,
    ) -> None:
        did = obs.did
        now = time.time()

        if now - obs.resolved_at > self.cache_ttl:
            raise AnchorFreshnessError(
                f"anchor {did} observation is stale: "
                f"age={now - obs.resolved_at:.1f}s > ttl={self.cache_ttl}s"
            )

        prev_ver = self.state.last_seen_version.get(did)
        if obs.version_id is None:
            # No monotonic evidence: require independent corroboration
            # AND an unchanged key set since last observation.
            prev_methods = self.state.last_seen_methods.get(did)
            if independent_sources < 2:
                raise AnchorFreshnessError(
                    f"anchor {did} has no versionId and only "
                    f"{independent_sources} source(s); cannot prove freshness"
                )
            if prev_methods is not None and obs.verification_methods != prev_methods:
                raise AnchorFreshnessError(
                    f"anchor {did} key set changed without versionId; "
                    f"possible substitution"
                )
        else:
            if prev_ver is not None and obs.version_id < prev_ver:
                raise AnchorFreshnessError(
                    f"anchor {did} version regression: "
                    f"observed={obs.version_id} < last_seen={prev_ver}; "
                    f"treating as rollback/substitution"
                )
            self.state.last_seen_version[did] = obs.version_id

        self.state.last_seen_methods[did] = obs.verification_methods
