# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/under-what-conditions-must-an-instance-re-bind-its-agent-identity-mid-session-rather-than-rotate-the-signing-key-in-place.md
# Remnant Warrant:    https://corpusfetch.com/c/under-what-conditions-must-an-instance-re-bind-its-agent-identity-mid-session-rather-than-rotate-the-signing-key-in-place.warrant.json
# Markdown SHA-256:   fbceae8ca46e1ca7754265b6ae21bb23c12a29b81f6a78d0116839c10899b69d
# Code AST SHA-256:   bdca8c2cd96816a0d77ea0b7d7ed0d7e6b865e84bdc1ac658b85c7e879d95f8f
# Verified Classes:   BindingTupleDriftError, BindingTuple
# Defensive Guards:   1 fail-closed invariants (zero external deps)

import time
from dataclasses import dataclass


class BindingTupleDriftError(RuntimeError):
    """Raised when in-place key rotation would be unsound because the
    identity binding tuple has changed since the token was minted."""


@dataclass(frozen=True)
class BindingTuple:
    epoch: int
    capability_hash: str
    nonce: str
    delegation_depth: int


def rotate_or_rebind(
    cached: BindingTuple,
    fetched: BindingTuple,
    issuer_failover_detected: bool,
    sign_fn,
    rebind_fn,
    lock,
):
    """Perform key rotation only if the binding tuple is unchanged.

    The fetch of `fetched` must happen outside this function but the
    comparison and the sign/rebind must happen under `lock` so that an
    issuer migration cannot land between verification and signing.
    """
    with lock:
        drifted = (
            fetched.epoch != cached.epoch
            or fetched.capability_hash != cached.capability_hash
            or fetched.nonce != cached.nonce
            or fetched.delegation_depth != cached.delegation_depth
        )

        if issuer_failover_detected:
            # Epoch counters are not guaranteed monotonic across failover;
            # never trust an "unchanged" comparison here.
            return rebind_fn(reason="issuer_failover")

        if drifted:
            raise BindingTupleDriftError(
                f"binding tuple drifted: cached={cached} fetched={fetched}; "
                f"in-place rotation would produce a token the verifier "
                f"rejects or silently downscopes"
            )

        return sign_fn(cached)


def refresh_cycle(agent, issuer, lock):
    cached = agent.cached_binding_tuple()
    fetched = issuer.fetch_binding_tuple(agent.agent_id)
    try:
        return rotate_or_rebind(
            cached=cached,
            fetched=fetched,
            issuer_failover_detected=issuer.failover_since(agent.last_seen_issuer_epoch),
            sign_fn=agent.sign_new_key,
            rebind_fn=agent.rebind_identity,
            lock=lock,
        )
    except BindingTupleDriftError as exc:
        # Drift is recoverable only by re-binding; surface it explicitly
        # rather than retrying rotation, which will fail identically.
        agent.log.warning("rotation unsound, re-binding: %s", exc)
        return agent.rebind_identity(reason="tuple_drift")
