# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/why-does-an-instance-forget-a-memory-it-can-still-prove-it-wrote-tombstone-provenance-and-the-deletion-acknowledgment-gap-in-read-write-forget-memory.md
# Remnant Warrant:    https://corpusfetch.com/c/why-does-an-instance-forget-a-memory-it-can-still-prove-it-wrote-tombstone-provenance-and-the-deletion-acknowledgment-gap-in-read-write-forget-memory.warrant.json
# Markdown SHA-256:   c3205fbb385b71a7922cfb6c6096705a5d7243224805c56dd20ea5797b0ac06e
# Code AST SHA-256:   8e20824aea5ba20e9ce758e19bca4afdc9d3d2f6bb43dc2cc3a2771ffd082f71
# Verified Classes:   ProvenanceGapError, ResurrectionError, ForgetIntent, MemoryClient
# Defensive Guards:   6 fail-closed invariants (zero external deps)

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


class ProvenanceGapError(RuntimeError):
    """Raised when a forget cannot be proven to have committed a tombstone."""
    pass


class ResurrectionError(RuntimeError):
    """Raised when a key becomes readable after its tombstone committed."""
    pass


@dataclass
class ForgetIntent:
    key: str
    actor: str
    reason: str
    value_hash: str
    requested_at: float
    tombstone_id: Optional[str] = None
    verified: bool = False


@dataclass
class MemoryClient:
    store: dict = field(default_factory=dict)
    tombstones: dict = field(default_factory=dict)
    local_intents: list = field(default_factory=list)
    convergence_window_s: float = 1.5

    def _hash(self, value: str) -> str:
        return hashlib.sha256(value.encode()).hexdigest()

    def write(self, key: str, value: str) -> None:
        # A write after a tombstone resurrects the key; caller must serialize.
        if key in self.tombstones:
            raise ResurrectionError(
                f"write to {key!r} would resurrect a tombstoned key "
                f"(tombstone_id={self.tombstones[key]})"
            )
        self.store[key] = value

    def read(self, key: str) -> Optional[str]:
        return self.store.get(key)

    def forget(self, key: str, actor: str, reason: str) -> ForgetIntent:
        value = self.store.get(key)
        if value is None:
            raise ProvenanceGapError(
                f"forget({key!r}): key not readable; cannot bind intent to a value hash"
            )
        intent = ForgetIntent(
            key=key,
            actor=actor,
            reason=reason,
            value_hash=self._hash(value),
            requested_at=time.time(),
        )
        # Logical deletion on the read path.
        del self.store[key]
        # Tombstone commit is a SEPARATE step. If it fails, the read path
        # already reports NotFound -- the gap this article describes.
        try:
            tombstone_id = self._commit_tombstone(intent)
        except Exception as exc:  # audit sink backpressure, etc.
            self.local_intents.append(intent)
            raise ProvenanceGapError(
                f"forget({key!r}) removed from reads but tombstone NOT committed: {exc}"
            ) from exc
        intent.tombstone_id = tombstone_id
        intent.verified = True
        self.local_intents.append(intent)
        return intent

    def _commit_tombstone(self, intent: ForgetIntent) -> str:
        # Stand-in for the audit sink; in production this can 429/drop.
        tid = f"ts-{intent.key}-{int(intent.requested_at * 1000)}"
        self.tombstones[intent.key] = tid
        return tid

    def verify_forget(self, intent: ForgetIntent) -> bool:
        # Verify by tombstone, NOT by re-reading the key.
        if intent.tombstone_id is None:
            raise ProvenanceGapError(
                f"intent for {intent.key!r} has no tombstone_id; cannot verify"
            )
        if self.tombstones.get(intent.key) != intent.tombstone_id:
            raise ProvenanceGapError(
                f"tombstone for {intent.key!r} missing or mismatched"
            )
        if self.read(intent.key) is not None:
            # Replica lag or resurrection -- do NOT re-issue forget.
            time.sleep(self.convergence_window_s)
            if self.read(intent.key) is not None:
                raise ResurrectionError(
                    f"{intent.key!r} readable after tombstone {intent.tombstone_id}"
                )
        return True
