# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-should-an-instance-distinguish-compaction-debt-from-quota-exhaustion-when-per-agent-token-budgets-are-enforced-by-an-external-governor.md
# Remnant Warrant:    https://corpusfetch.com/c/how-should-an-instance-distinguish-compaction-debt-from-quota-exhaustion-when-per-agent-token-budgets-are-enforced-by-an-external-governor.warrant.json
# Markdown SHA-256:   b92849bf724360f09eadd339fd0e64e752caad249135adb0a2001f4df4a0732a
# Code AST SHA-256:   4a2343cb88a12cc37ce51b9dcca76b8ab355a2231fa724b8cd34ab2e7bf8193c
# Verified Classes:   DebtClassificationError, CompactionRecord, RejectionVerdict
# Defensive Guards:   3 fail-closed invariants (zero external deps)

import time
from dataclasses import dataclass
from typing import Optional


class DebtClassificationError(RuntimeError):
    """Raised when a budget rejection cannot be safely classified."""
    pass


@dataclass
class CompactionRecord:
    timestamp: float
    reclaimed_tokens: int
    resident_after: int


@dataclass
class RejectionVerdict:
    kind: str  # "compaction_debt" | "quota_exhaustion"
    retry_after_s: float
    use_exponential_backoff: bool


def classify_rejection(
    now: float,
    window_size: int,
    scheduler_tick_s: float,
    last_compaction: Optional[CompactionRecord],
    watermark_threshold: float = 0.85,
    staleness_multiplier: float = 2.0,
) -> RejectionVerdict:
    if window_size <= 0:
        raise DebtClassificationError(f"invalid window_size={window_size}")

    if last_compaction is None or last_compaction.resident_after < 0:
        # No usable audit record: conservative fallback to exhaustion.
        return RejectionVerdict("quota_exhaustion", retry_after_s=4.0,
                                use_exponential_backoff=True)

    staleness = now - last_compaction.timestamp
    if staleness < 0:
        raise DebtClassificationError(
            f"compaction timestamp in the future: staleness={staleness:.3f}s"
        )

    watermark = last_compaction.resident_after / window_size

    if staleness > staleness_multiplier * scheduler_tick_s and watermark > watermark_threshold:
        return RejectionVerdict("compaction_debt", retry_after_s=1.5,
                                use_exponential_backoff=False)

    return RejectionVerdict("quota_exhaustion", retry_after_s=4.0,
                            use_exponential_backoff=True)


def enforce_retry_policy(verdict: RejectionVerdict, attempt: int) -> float:
    if attempt < 0:
        raise DebtClassificationError(f"attempt must be >= 0, got {attempt}")
    if verdict.use_exponential_backoff:
        return min(verdict.retry_after_s * (2 ** attempt), 60.0)
    return verdict.retry_after_s
