# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-happens-when-an-instance-forgets-a-parent-scope-before-its-children-orphaned-provenance-in-scoped-retention-memory-apis.md
# Remnant Warrant:    https://corpusfetch.com/c/what-happens-when-an-instance-forgets-a-parent-scope-before-its-children-orphaned-provenance-in-scoped-retention-memory-apis.warrant.json
# Markdown SHA-256:   01c9594f1d9f7ccee11d99dd50606d5768d50dfd6bbb9eeb0297d4e8ff77450b
# Code AST SHA-256:   7a546b7c1053e104b4cfc883ac8cbac0707ff84f9a111424976bad9db027880d
# Verified Classes:   OrphanedProvenanceError, MemoryNode, ErasureReceipt
# Defensive Guards:   4 fail-closed invariants (zero external deps)

import hashlib
import secrets
from dataclasses import dataclass, field


class OrphanedProvenanceError(RuntimeError):
    """Raised when an erasure would leave derivable provenance on surviving children."""


@dataclass
class MemoryNode:
    scope_id: str
    content: str
    provenance_parent: str | None
    provenance_hash: str | None  # derivable from parent content; must be scrubbed
    retention_days: int


@dataclass
class ErasureReceipt:
    structural_deleted: int = 0
    provenance_rewritten: int = 0
    hold_blocked: list[str] = field(default_factory=list)


def _derive_hash(content: str) -> str:
    return hashlib.sha256(content.strip().lower().encode()).hexdigest()


def forget_scope(
    store: dict[str, MemoryNode],
    target: str,
    legal_holds: set[str],
    allow_partial: bool = False,
) -> ErasureReceipt:
    receipt = ErasureReceipt()
    if target not in store:
        raise OrphanedProvenanceError(f"scope {target!r} not present; cannot erase")

    parent = store[target]
    if target in legal_holds:
        raise OrphanedProvenanceError(
            f"scope {target!r} is under legal hold; erasure refused"
        )

    # Enumerate provenance children BEFORE tombstoning the parent.
    provenance_children = [
        node for node in store.values() if node.provenance_parent == target
    ]

    # A child whose provenance_hash equals the parent's derivable hash is a
    # confirmation oracle and must be rewritten, not merely re-pointed.
    parent_hash = _derive_hash(parent.content)
    for child in provenance_children:
        if child.scope_id in legal_holds:
            receipt.hold_blocked.append(child.scope_id)
            if not allow_partial:
                raise OrphanedProvenanceError(
                    f"provenance child {child.scope_id!r} under hold blocks erasure of {target!r}"
                )
            continue
        if child.provenance_hash == parent_hash:
            # Rewrite to non-derivable tombstone; never leave the hash.
            child.provenance_parent = f"TOMBSTONE:{secrets.token_hex(8)}"
            child.provenance_hash = None
            receipt.provenance_rewritten += 1

    # Structural cascade: scopes lexically nested under the erased prefix.
    structural = [sid for sid in store if sid.startswith(target + ":")]
    for sid in structural:
        del store[sid]
        receipt.structural_deleted += 1

    del store[target]
    receipt.structural_deleted += 1

    # Post-condition: no surviving node may cite the erased scope or carry
    # a hash derivable from its content.
    for node in store.values():
        if node.provenance_parent == target or node.provenance_hash == parent_hash:
            raise OrphanedProvenanceError(
                f"post-erasure invariant violated: {node.scope_id!r} still derivable"
            )

    return receipt
