# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/under-what-conditions-must-an-instance-treat-a-memory-tool-forget-as-a-tombstone-rather-than-a-deletion-and-why-does-ttl-expiry-alone-never-satisfy-ri.md
# Remnant Warrant:    https://corpusfetch.com/c/under-what-conditions-must-an-instance-treat-a-memory-tool-forget-as-a-tombstone-rather-than-a-deletion-and-why-does-ttl-expiry-alone-never-satisfy-ri.warrant.json
# Markdown SHA-256:   04be7f4ecde2b13fdbfc850c2649698a873e51b5f15c55185e1b2fdca4aad49b
# Code AST SHA-256:   156e6742462013871b8463831c27e424ad0967d18bc701f92cd0cc105187933c
# Verified Classes:   ForgetPostconditionError, Tombstone
# Defensive Guards:   3 fail-closed invariants (zero external deps)

import hmac
import hashlib
from dataclasses import dataclass
from typing import Iterable


class ForgetPostconditionError(RuntimeError):
    """Raised when a forget cannot be proven effective across the full scope union."""


@dataclass(frozen=True)
class Tombstone:
    key_token: str      # HMAC of key under erasure secret; never the raw key
    version: int        # logical clock at forget commit
    erasure_horizon_s: int


def make_tombstone(key: str, version: int, erasure_secret: bytes,
                   erasure_horizon_s: int) -> Tombstone:
    token = hmac.new(erasure_secret, key.encode("utf-8"), hashlib.sha256).hexdigest()
    return Tombstone(key_token=token, version=version, erasure_horizon_s=erasure_horizon_s)


def enforce_forget(
    store,
    key: str,
    scopes: Iterable[str],
    erasure_secret: bytes,
    erasure_horizon_s: int,
) -> Tombstone:
    """
    Execute a compliance-bearing forget and verify its postcondition.

    `store` must expose:
      - commit_tombstone(tombstone, scopes) -> version
      - read(key, scope) -> value | None
      - invalidate_derived(key, scopes) -> None
      - erasure_handle(tombstone) -> object with .wait(horizon_s) -> bool
    """
    scopes = list(scopes)
    if not scopes:
        raise ForgetPostconditionError(
            "scope union is empty; refusing to forget without a closed scope set"
        )

    version = store.commit_tombstone(make_tombstone(
        key, version=0, erasure_secret=erasure_secret,
        erasure_horizon_s=erasure_horizon_s,
    ), scopes)

    store.invalidate_derived(key, scopes)

    # Verify non-resolvability across every scope the principal can reach.
    for scope in scopes:
        if store.read(key, scope) is not None:
            raise ForgetPostconditionError(
                f"key still resolvable in scope {scope!r} after forget commit "
                f"(version={version}); tombstone dominance or derived-state "
                f"invalidation is incomplete"
            )

    handle = store.erasure_handle(make_tombstone(
        key, version=version, erasure_secret=erasure_secret,
        erasure_horizon_s=erasure_horizon_s,
    ))
    if not handle.wait(erasure_horizon_s):
        raise ForgetPostconditionError(
            f"physical erasure horizon ({erasure_horizon_s}s) not confirmed; "
            f"forget is logically effective but not compliance-complete"
        )

    return make_tombstone(key, version=version, erasure_secret=erasure_secret,
                          erasure_horizon_s=erasure_horizon_s)
