"""One-purpose offline removal of the Task22 live trainer authority.

This module is deliberately not a generic registry editor.  It recognizes one
frozen source state, emits an authenticated append-only two-event receipt, and
can only remove ``task22_dm_rehearsal.trainer``.  Historical onboarding and
publication evidence is read and bound but never rewritten.
"""

from __future__ import annotations

import base64
import ctypes
import errno
import fcntl
import hashlib
import hmac
import importlib
import json
import os
import secrets
import stat
import subprocess
import sys
import tempfile
from collections.abc import Callable, Iterator, Mapping, Sequence
from contextlib import contextmanager
from dataclasses import dataclass
from enum import StrEnum
from pathlib import Path
from typing import Any, Final, Protocol, cast

from gateway.platforms.task22_protected_session_inventory import (
    ProtectedSessionInventoryError,
    scan_protected_session_inventory,
)
from gateway.platforms.telegram_nutrition_onboarding_publication_outbox import (
    GatewayOnboardingPublicationOutbox,
)

PROFILE: Final = "dualcoachtest"
CUSTOMER: Final = "task22_dm_rehearsal"
SESSION: Final = "rb_Ar-NuKG3LH8CqHPXUgc88g"
SOURCE_DIGEST: Final = "b5b95afa818b0c7460141403e0bfe434fb987a31fe5285f6e6e98b70cb738aa1"
TARGET_DIGEST: Final = "69c1382eb663942a7c8c18b9fee29141612ac37e92f1ffb7df0ba1beed44579d"
HISTORICAL_REGISTRY_DIGEST: Final = "93951fae167d8a978ec9edcc2e31ce3cbd4de95442579f293cda2f3715269fa2"
CANDIDATE_DIGEST: Final = "c160085a64454c23f789464170b299543324c743a489bac68ed5430fab42d291"
MANIFEST_DIGEST: Final = "e9bc7dff8060425aa001f6a4501107bb9685be9248a7753f0969f89c2cf5d5c8"
FREEZE_DIGEST: Final = "f02f8b5ee211e571c70cf928da4b7862ae20e9cf56189815789c13e1cb4af995"
PRE_SUPPLEMENTAL_DIGEST: Final = "359adbba7bca9cfebfdeb049bc8a06c970e415d3250178910e187b745ca5987d"
OWNER_JOURNAL_DIGEST: Final = "e774b2b01edb90391389ea304b7d10f06f908302cf28aac39a57e3c52b289474"
SEMANTIC_DIFF: Final = ({"op": "remove", "path": f"/customers/{CUSTOMER}/trainer"},)
_SCHEMA: Final = "task22-trainer-authority-removal-event-v1"
_RECEIPT_SCHEMA: Final = "task22-trainer-authority-removal-receipt-v1"
_DIRECTORY: Final = "trainer-authority-removal-v1"
_LOCK: Final = ".trainer-authority-removal-v1.lock"
_TRANSACTION_ID: Final = hashlib.sha256(
    f"{_SCHEMA}\0{PROFILE}\0{CUSTOMER}\0{SESSION}\0{SOURCE_DIGEST}\0{TARGET_DIGEST}".encode()
).hexdigest()
_PRIVATE_FILE: Final = 0o600
_PRIVATE_DIRECTORY: Final = 0o700

class TrainerAuthorityRemovalError(RuntimeError):
    """The pinned migration cannot be proved safe."""

class RemovalPhase(StrEnum):
    PREPARED = "PREPARED"
    COMMITTED = "COMMITTED"

@dataclass(frozen=True, slots=True)
class RemovalPaths:
    profile_root: Path
    candidate_manifest: Path
    freeze_receipt: Path
    pre_supplemental: Path
    registry: Path
    owner_journal: Path
    baseline_candidate: Path
    supplemental_journal: Path
    supplemental_lock: Path
    adoption_receipt: Path
    outbox_root: Path
    transaction_root: Path
    prepared: Path
    committed: Path
    transaction_lock: Path
    gateway_lock: Path

    @classmethod
    def canonical(
        cls,
        profile_root: Path | str,
        *,
        candidate_manifest: Path | str,
        freeze_receipt: Path | str,
        pre_supplemental: Path | str,
    ) -> "RemovalPaths":
        root = Path(profile_root).absolute()
        onboarding = root / "data" / "customers" / CUSTOMER / "nutrition-onboarding"
        migrations = onboarding / "migrations"
        transaction = migrations / _DIRECTORY / _TRANSACTION_ID
        outbox = root / "data" / "onboarding" / "telegram-publication-outbox-v1"
        return cls(
            profile_root=root,
            candidate_manifest=Path(candidate_manifest).absolute(),
            freeze_receipt=Path(freeze_receipt).absolute(),
            pre_supplemental=Path(pre_supplemental).absolute(),
            registry=root / "customers" / "registry.json",
            owner_journal=migrations / "owner-v1.json",
            baseline_candidate=onboarding / "baseline-candidate.json",
            supplemental_journal=migrations / "task22-supplemental-owner-review-v1.json",
            supplemental_lock=migrations / ".task22-supplemental-owner-review-v1.lock",
            adoption_receipt=migrations / "task22-callback-storage-adoption-v1.json",
            outbox_root=outbox,
            transaction_root=transaction,
            prepared=transaction / "000-prepared.json",
            committed=transaction / "001-committed.json",
            transaction_lock=migrations / _LOCK,
            gateway_lock=root / "gateway.lock",
        )

    @property
    def retained_before_image(self) -> Path:
        return self.registry.parent / f".{self.registry.name}.{_TRANSACTION_ID}.exchange"

    @property
    def authority_locks(self) -> tuple[Path, ...]:
        data = self.profile_root / "data"
        return (
            data / ".adaptive-authority.lock",
            data / ".nutrition-onboarding-projection.lock",
            data / ".scheduled-deliveries.lock",
            data / "activation-completion-notices.lock",
            data / "onboarding" / "telegram-room-bootstrap-v1" / "ledger.lock",
            data / "onboarding" / "telegram-room-bootstrap-v1" / "role-invites.lock",
            self.outbox_root / ".lock",
            self.outbox_root / ".emergency.lock",
            self.outbox_root / ".owner-callbacks.lock",
            data / "owner-actions" / "customer-service-state.json.lock",
            data / "owner-actions" / "draft-deliveries.json.lock",
            self.owner_journal.parent.parent / ".lock",
        )

@dataclass(frozen=True, slots=True)
class RemovalSnapshot:
    transaction_id: str
    phase: RemovalPhase | None
    registry_digest: str
    ready: bool

@dataclass(frozen=True, slots=True)
class _RegistryEntry:
    data: bytes
    identity: dict[str, object]

@dataclass(frozen=True, slots=True)
class _ExchangeResult:
    pinned_source_identity: dict[str, object]
    exchanged_source_identity: dict[str, object]
    prepared_target_identity: dict[str, object]
    exchanged_target_identity: dict[str, object]
    retained_before_image_name: str

class CanonicalCustomerSpec(Protocol):
    @property
    def customer_key(self) -> str: ...

    @property
    def enabled(self) -> bool: ...

    @property
    def trainer(self) -> object | None: ...

class CanonicalCustomerRuntime(Protocol):
    @property
    def spec(self) -> CanonicalCustomerSpec: ...

class CanonicalRegistry(Protocol):
    @property
    def customers(self) -> Sequence[CanonicalCustomerRuntime]: ...

ServiceFenceCheck = Callable[[Path], Mapping[str, object]]
CanonicalLoader = Callable[[Path], CanonicalRegistry]
FaultHook = Callable[[str], None]

def _no_fault(_boundary: str) -> None:
    return None

class TrainerAuthorityRemoval:
    def __init__(
        self,
        paths: RemovalPaths,
        *,
        service_fence_check: ServiceFenceCheck | None = None,
        canonical_loader: CanonicalLoader | None = None,
        fault_hook: FaultHook = _no_fault,
    ) -> None:
        self.paths = paths
        self._service_fence_check = service_fence_check or _default_service_fence
        self._canonical_loader = canonical_loader or _canonical_loader
        self._fault = fault_hook
        self._last_service_fence_proof: dict[str, object] = {}
        self._inventory: dict[str, object] = {}
        self._inventory_digest = ""

    def preflight(self) -> RemovalSnapshot:
        """Read-only validation under the complete existing authority lock set."""
        self._validate_roots()
        self._assert_quiescent()
        inventory_a = self._inventory_digest
        with self._ordered_locks(create=False):
            self._assert_quiescent(ignore_lock_holders=True)
            self._assert_inventory_stable(inventory_a)
            adoption_binding = self._callback_adoption_binding()
            registry = _read_private(self.paths.registry, "customer registry")
            digest = _sha(registry)
            prepared = self._read_event(self.paths.prepared, optional=True)
            committed = self._read_event(self.paths.committed, optional=True)
            phase = self._recovery_state(digest, prepared, committed)
            if phase is None:
                self._source_bindings(registry)
            elif phase is RemovalPhase.PREPARED:
                assert prepared is not None
                self._assert_event_inventory_current(prepared)
                self._verify_prepared(prepared)
                if prepared.get("callback_storage_adoption_binding") != adoption_binding:
                    raise TrainerAuthorityRemovalError(
                        "prepared removal adoption binding drifted"
                    )
            else:
                assert prepared is not None and committed is not None
                self._assert_event_inventory_current(prepared)
                self._verify_committed(prepared, committed)
                if prepared.get("callback_storage_adoption_binding") != adoption_binding:
                    raise TrainerAuthorityRemovalError(
                        "committed removal adoption binding drifted"
                    )
                self._verify_target(registry, load=True)
                self._verify_live_commit_binding(committed)
                self._assert_retained_before_image_absent()
        return RemovalSnapshot(_TRANSACTION_ID, phase, digest, True)

    def execute(self) -> RemovalSnapshot:
        """Prepare/recover/commit the sole authorized transition."""
        self._validate_roots()
        self._assert_quiescent()
        inventory_a = self._inventory_digest
        with self._ordered_locks() as lock_descriptors:
            self._assert_quiescent(ignore_lock_holders=True)
            self._assert_inventory_stable(inventory_a)
            adoption_binding = self._callback_adoption_binding()
            registry = _read_private(self.paths.registry, "customer registry")
            digest = _sha(registry)
            prepared = self._read_event(self.paths.prepared, optional=True)
            committed = self._read_event(self.paths.committed, optional=True)
            phase = self._recovery_state(digest, prepared, committed)
            if phase is RemovalPhase.COMMITTED:
                typed_prepared = cast(dict[str, object], prepared)
                typed_committed = cast(dict[str, object], committed)
                self._assert_event_inventory_current(typed_prepared)
                self._verify_committed(typed_prepared, typed_committed)
                if typed_prepared.get("callback_storage_adoption_binding") != adoption_binding:
                    raise TrainerAuthorityRemovalError(
                        "committed removal adoption binding drifted"
                    )
                self._verify_target(registry, load=True)
                self._verify_live_commit_binding(typed_committed, lock_descriptors=lock_descriptors)
                self._cleanup_retained_before_image(typed_prepared, typed_committed)
                return RemovalSnapshot(_TRANSACTION_ID, phase, digest, True)

            bindings: dict[str, object] | None = None
            if prepared is None:
                bindings = self._source_bindings(registry)
                target = cast(bytes, bindings.pop("target_registry_bytes_raw"))
            else:
                self._assert_event_inventory_current(prepared)
                self._verify_prepared(prepared)
                if prepared.get("callback_storage_adoption_binding") != adoption_binding:
                    raise TrainerAuthorityRemovalError(
                        "prepared removal adoption binding drifted"
                    )
                target = base64.b64decode(
                    cast(str, prepared["target_registry_bytes_b64"]), validate=True
                )

            with _registry_parent(self.paths.registry) as parent_fd:
                if prepared is None:
                    assert bindings is not None
                    source_entry = _read_registry_at(
                        parent_fd, self.paths.registry.name, "customer registry"
                    )
                    if source_entry.data != registry or _sha(source_entry.data) != SOURCE_DIGEST:
                        raise TrainerAuthorityRemovalError(
                            "source registry changed before exchange preparation; keep service fenced"
                        )
                    target_entry = _prepare_exchange_target(
                        parent_fd,
                        self.paths.retained_before_image.name,
                        target,
                        self._fault,
                    )
                    event = self._event(
                        phase=RemovalPhase.PREPARED,
                        sequence=0,
                        previous_event_digest=None,
                        payload={
                            **bindings,
                            "source_registry_bytes_b64": base64.b64encode(registry).decode("ascii"),
                            "target_registry_bytes_b64": base64.b64encode(target).decode("ascii"),
                            "pinned_source_identity": source_entry.identity,
                            "prepared_target_identity": target_entry.identity,
                            "retained_before_image_name": self.paths.retained_before_image.name,
                            "exchange_protocol": "linux-renameat2-rename-exchange-v1",
                            "registry_parent_binding": _directory_binding(
                                self.paths.profile_root,
                                self.paths.registry.parent,
                                parent_fd,
                            ),
                        },
                    )
                    self._write_event(self.paths.prepared, event)
                    self._fault("after_prepared_rename")
                    prepared = event

                exchange = self._exchange_and_validate(parent_fd, prepared, target)
                committed_entry = _read_registry_at(
                    parent_fd, self.paths.registry.name, "customer registry"
                )
                self._verify_target(committed_entry.data, load=True)
                self._fault("after_loader_verification")
                with _directory_descriptor(self.paths.transaction_root) as transaction_fd:
                    registry_parent_binding = _directory_binding(
                        self.paths.profile_root, self.paths.registry.parent, parent_fd
                    )
                    transaction_parent_binding = _directory_binding(
                        self.paths.profile_root,
                        self.paths.transaction_root,
                        transaction_fd,
                    )
                    authority_lock_bindings = _lock_bindings(
                        self.paths.profile_root, lock_descriptors
                    )
                    if (
                        registry_parent_binding != prepared.get("registry_parent_binding")
                        or not _matches_exact(exchange.exchanged_target_identity, committed_entry)
                    ):
                        raise TrainerAuthorityRemovalError(
                            "terminal registry authority drifted; recovery_required; keep service fenced"
                        )
                    commit = self._event(
                        phase=RemovalPhase.COMMITTED,
                        sequence=1,
                        previous_event_digest=cast(str, prepared["event_digest"]),
                        payload={
                            "prepared_event_digest": prepared["event_digest"],
                            "registry_digest": TARGET_DIGEST,
                            "canonical_loader_verified": True,
                            "trainer_absent": True,
                            "customer_disabled": True,
                            "rollback_policy": "forward_only_never_restore_trainer",
                            "pinned_source_identity": exchange.pinned_source_identity,
                            "exchanged_source_identity": exchange.exchanged_source_identity,
                            "prepared_target_identity": exchange.prepared_target_identity,
                            "exchanged_target_identity": exchange.exchanged_target_identity,
                            "retained_before_image_name": exchange.retained_before_image_name,
                            "exchange_validated": True,
                            "registry_parent_binding": registry_parent_binding,
                            "transaction_parent_binding": transaction_parent_binding,
                            "authority_lock_bindings": authority_lock_bindings,
                            "protected_process_inventory_digest": prepared["protected_process_inventory_digest"],
                            "protected_process_inventory_summary": prepared["protected_process_inventory_summary"],
                            "process_inventory_transaction_digest": prepared["process_inventory_transaction_digest"],
                            "quiescence_fence_digest": prepared["quiescence_fence_digest"],
                        },
                    )
                    self._assert_terminal_commit_binding(
                        commit,
                        parent_fd,
                        transaction_fd,
                        lock_descriptors,
                    )
                    self._write_event_at(transaction_fd, self.paths.committed.name, commit)
                    self._fault("after_committed_rename")
                    try:
                        self._assert_terminal_commit_binding(
                            commit,
                            parent_fd,
                            transaction_fd,
                            lock_descriptors,
                        )
                    except TrainerAuthorityRemovalError:
                        self._remove_event_at(transaction_fd, self.paths.committed.name, commit)
                        raise
                    self._cleanup_retained_before_image(
                        prepared, commit, parent_fd=parent_fd
                    )
            return RemovalSnapshot(
                _TRANSACTION_ID, RemovalPhase.COMMITTED, TARGET_DIGEST, True
            )

    def _assert_terminal_commit_binding(
        self,
        committed: Mapping[str, object],
        registry_parent_fd: int,
        transaction_parent_fd: int,
        lock_descriptors: Sequence[tuple[Path, int]],
        *,
        require_retained_source: bool = True,
    ) -> None:
        if committed.get("registry_parent_binding") != _directory_binding(
            self.paths.profile_root, self.paths.registry.parent, registry_parent_fd
        ):
            raise TrainerAuthorityRemovalError(
                "canonical registry parent drifted; recovery_required; keep service fenced"
            )
        if committed.get("transaction_parent_binding") != _directory_binding(
            self.paths.profile_root,
            self.paths.transaction_root,
            transaction_parent_fd,
        ):
            raise TrainerAuthorityRemovalError(
                "transaction parent drifted; recovery_required; keep service fenced"
            )
        if committed.get("authority_lock_bindings") != _lock_bindings(
            self.paths.profile_root, lock_descriptors
        ):
            raise TrainerAuthorityRemovalError(
                "authority lock drifted; recovery_required; keep service fenced"
            )
        target = _read_registry_at(
            registry_parent_fd, self.paths.registry.name, "terminal registry target"
        )
        if not _matches_exact(_event_identity(committed, "exchanged_target_identity"), target):
            raise TrainerAuthorityRemovalError(
                "terminal registry target drifted; recovery_required; keep service fenced"
            )
        if require_retained_source:
            source = _read_registry_at(
                registry_parent_fd,
                cast(str, committed.get("retained_before_image_name")),
                "terminal retained registry source",
            )
            if not _matches_exact(
                _event_identity(committed, "exchanged_source_identity"), source
            ):
                raise TrainerAuthorityRemovalError(
                    "terminal retained registry source drifted; recovery_required; keep service fenced"
                )

    def _verify_live_commit_binding(
        self,
        committed: Mapping[str, object],
        *,
        lock_descriptors: Sequence[tuple[Path, int]] | None = None,
    ) -> None:
        with (
            _registry_parent(self.paths.registry) as registry_parent_fd,
            _directory_descriptor(self.paths.transaction_root) as transaction_parent_fd,
        ):
            descriptors = lock_descriptors or tuple(
                (path, _open_private_regular(path, "authority lock"))
                for path in (self.paths.gateway_lock, *self.paths.authority_locks, self.paths.transaction_lock)
            )
            try:
                self._assert_terminal_commit_binding(
                    committed,
                    registry_parent_fd,
                    transaction_parent_fd,
                    descriptors,
                    require_retained_source=False,
                )
            finally:
                if lock_descriptors is None:
                    for _path, descriptor in descriptors:
                        os.close(descriptor)

    def _verify_supplemental_replay_commit_binding(
        self, committed: Mapping[str, object]
    ) -> None:
        """Permit only the expected post-startup gateway-lock rotation.

        The authenticated committed receipt retains the original gateway lock
        identity. Startup owns a newly created gateway.lock, so replay proves
        that historical identity from the receipt but compares every other
        current authority lock exactly.
        """
        paths = (
            self.paths.gateway_lock,
            *self.paths.authority_locks,
            self.paths.transaction_lock,
        )
        with (
            _registry_parent(self.paths.registry) as registry_parent_fd,
            _directory_descriptor(self.paths.transaction_root) as transaction_parent_fd,
        ):
            descriptors = tuple(
                (path, _open_private_regular(path, "authority lock"))
                for path in paths
            )
            try:
                expected_locks = committed.get("authority_lock_bindings")
                _verify_lock_bindings(
                    expected_locks,
                    self.paths.profile_root,
                    paths,
                )
                current_locks = _lock_bindings(self.paths.profile_root, descriptors)
                if current_locks[1:] != cast(list[dict[str, object]], expected_locks)[1:]:
                    raise TrainerAuthorityRemovalError(
                        "authority lock drifted; recovery_required; keep service fenced"
                    )
                if committed.get("registry_parent_binding") != _directory_binding(
                    self.paths.profile_root,
                    self.paths.registry.parent,
                    registry_parent_fd,
                ) or committed.get("transaction_parent_binding") != _directory_binding(
                    self.paths.profile_root,
                    self.paths.transaction_root,
                    transaction_parent_fd,
                ):
                    raise TrainerAuthorityRemovalError(
                        "supplemental replay authority parent drifted; keep service fenced"
                    )
                target = _read_registry_at(
                    registry_parent_fd,
                    self.paths.registry.name,
                    "terminal registry target",
                )
                if not _matches_exact(
                    _event_identity(committed, "exchanged_target_identity"), target
                ):
                    raise TrainerAuthorityRemovalError(
                        "terminal registry target drifted; recovery_required; keep service fenced"
                    )
            finally:
                for _path, descriptor in reversed(descriptors):
                    os.close(descriptor)

    def _exchange_and_validate(
        self,
        parent_fd: int,
        prepared: Mapping[str, object],
        target: bytes,
    ) -> _ExchangeResult:
        source_identity = _event_identity(prepared, "pinned_source_identity")
        target_identity = _event_identity(prepared, "prepared_target_identity")
        retained_name = cast(str, prepared.get("retained_before_image_name"))
        if retained_name != self.paths.retained_before_image.name:
            raise TrainerAuthorityRemovalError("prepared exchange name is invalid")

        canonical = _read_registry_at(parent_fd, self.paths.registry.name, "customer registry")
        retained = _read_registry_at(parent_fd, retained_name, "prepared registry target")
        pre_exchange = (
            _matches_exact(source_identity, canonical)
            and _matches_exact(target_identity, retained)
            and retained.data == target
        )
        post_exchange = (
            _matches_pinned(target_identity, canonical)
            and canonical.data == target
            and _matches_pinned(source_identity, retained)
            and retained.data == base64.b64decode(
                cast(str, prepared["source_registry_bytes_b64"]), validate=True
            )
        )
        if pre_exchange:
            self._fault("before_registry_temp")
            _assert_registry_parent_binding(self.paths.registry.parent, parent_fd)
            current_target = _read_registry_at(parent_fd, retained_name, "prepared registry target")
            if not _matches_exact(target_identity, current_target) or current_target.data != target:
                raise TrainerAuthorityRemovalError(
                    "prepared registry target was rebound; recovery_required; keep service fenced"
                )
            _rename_exchange(parent_fd, self.paths.registry.name, retained_name)
            self._fault("after_registry_exchange")
            self._fault("after_registry_rename")
            self._fault("before_registry_directory_fsync")
            os.fsync(parent_fd)
        elif not post_exchange:
            raise TrainerAuthorityRemovalError(
                "registry exchange recovery state is ambiguous; recovery_required; "
                f"retained before-image={retained_name}; keep service fenced"
            )

        try:
            canonical = _read_registry_at(parent_fd, self.paths.registry.name, "exchanged registry target")
            displaced = _read_registry_at(parent_fd, retained_name, "displaced registry source")
        except (FileNotFoundError, TrainerAuthorityRemovalError) as exc:
            raise TrainerAuthorityRemovalError(
                "registry exchange validation is ambiguous; recovery_required; keep service fenced"
            ) from exc
        source_bytes = base64.b64decode(
            cast(str, prepared["source_registry_bytes_b64"]), validate=True
        )
        target_valid = _matches_pinned(target_identity, canonical) and canonical.data == target
        source_valid = (
            _matches_pinned(source_identity, displaced)
            and displaced.data == source_bytes
            and _sha(displaced.data) == SOURCE_DIGEST
        )
        if not target_valid or not source_valid:
            self._rollback_invalid_exchange(
                parent_fd,
                retained_name,
                target_identity,
                canonical,
                displaced,
            )
        return _ExchangeResult(
            pinned_source_identity=source_identity,
            exchanged_source_identity=displaced.identity,
            prepared_target_identity=target_identity,
            exchanged_target_identity=canonical.identity,
            retained_before_image_name=retained_name,
        )

    def _rollback_invalid_exchange(
        self,
        parent_fd: int,
        retained_name: str,
        target_identity: Mapping[str, object],
        canonical: _RegistryEntry,
        displaced: _RegistryEntry,
    ) -> None:
        if not _matches_pinned(target_identity, canonical):
            raise TrainerAuthorityRemovalError(
                "registry exchange target changed; rollback ambiguous; recovery_required; "
                "keep service fenced"
            )
        self._fault("before_registry_rollback")
        try:
            current_canonical = _read_registry_at(
                parent_fd, self.paths.registry.name, "rollback registry target"
            )
            current_displaced = _read_registry_at(
                parent_fd, retained_name, "rollback displaced source"
            )
        except (FileNotFoundError, TrainerAuthorityRemovalError) as exc:
            raise TrainerAuthorityRemovalError(
                "registry exchange rollback names changed; recovery_required; keep service fenced"
            ) from exc
        if not _matches_exact(canonical.identity, current_canonical) or not _matches_exact(
            displaced.identity, current_displaced
        ):
            raise TrainerAuthorityRemovalError(
                "registry exchange rollback identities changed; recovery_required; keep service fenced"
            )
        _assert_registry_parent_binding(self.paths.registry.parent, parent_fd)
        _rename_exchange(parent_fd, self.paths.registry.name, retained_name)
        os.fsync(parent_fd)
        restored = _read_registry_at(parent_fd, self.paths.registry.name, "rolled back registry")
        retained_target = _read_registry_at(parent_fd, retained_name, "rolled back target")
        if not _matches_pinned(displaced.identity, restored) or not _matches_pinned(
            target_identity, retained_target
        ):
            raise TrainerAuthorityRemovalError(
                "registry exchange rollback outcome is ambiguous; recovery_required; keep service fenced"
            )
        raise TrainerAuthorityRemovalError(
            "displaced source did not match the exact pinned registry; exchange rolled back; "
            "recovery_required; keep service fenced"
        )

    def _cleanup_retained_before_image(
        self,
        prepared: Mapping[str, object],
        committed: Mapping[str, object],
        *,
        parent_fd: int | None = None,
    ) -> None:
        retained_name = cast(str, committed.get("retained_before_image_name"))
        if retained_name != self.paths.retained_before_image.name:
            raise TrainerAuthorityRemovalError("committed retained before-image name is invalid")
        expected = _event_identity(committed, "exchanged_source_identity")
        source_bytes = base64.b64decode(
            cast(str, prepared["source_registry_bytes_b64"]), validate=True
        )
        with _optional_registry_parent(self.paths.registry, parent_fd) as descriptor:
            try:
                retained = _read_registry_at(descriptor, retained_name, "retained before-image")
            except FileNotFoundError:
                return
            if not _matches_exact(expected, retained) or retained.data != source_bytes:
                raise TrainerAuthorityRemovalError(
                    "retained before-image cleanup is ambiguous; recovery_required; keep service fenced"
                )
            self._fault("before_retained_before_image_cleanup")
            revalidated = _read_registry_at(descriptor, retained_name, "retained before-image")
            if not _matches_exact(retained.identity, revalidated):
                raise TrainerAuthorityRemovalError(
                    "retained before-image changed before cleanup; recovery_required; keep service fenced"
                )
            os.unlink(retained_name, dir_fd=descriptor)
            os.fsync(descriptor)
            self._fault("after_retained_before_image_cleanup")

    def _assert_retained_before_image_absent(self) -> None:
        if self.paths.retained_before_image.exists() or self.paths.retained_before_image.is_symlink():
            raise TrainerAuthorityRemovalError(
                "committed removal cleanup is incomplete; recovery_required; keep service fenced"
            )

    def _source_bindings(self, registry_bytes: bytes) -> dict[str, object]:
        if _sha(registry_bytes) != SOURCE_DIGEST:
            raise TrainerAuthorityRemovalError("source registry digest is not authorized")
        source = _object(registry_bytes, "customer registry")
        target = _target_bytes(source)
        if _sha(target) != TARGET_DIGEST:
            raise TrainerAuthorityRemovalError("target registry digest is not authorized")
        _prove_semantic_diff(source, _object(target, "target registry"))
        row = _customer(source)
        if row.get("enabled") is not False:
            raise TrainerAuthorityRemovalError("customer must remain disabled")
        self._assert_no_activation_or_delivery()
        owner_bytes = _read_private(self.paths.owner_journal, "owner-v1 journal")
        if _sha(owner_bytes) != OWNER_JOURNAL_DIGEST:
            raise TrainerAuthorityRemovalError("owner-v1 journal digest changed")
        owner = _object(owner_bytes, "owner-v1 journal")
        source_digests = owner.get("source_file_digests")
        typed_source_digests = (
            cast(dict[str, object], source_digests)
            if isinstance(source_digests, dict)
            else None
        )
        if (
            owner.get("phase") != "PUBLICATION_COMMITTED"
            or not isinstance(owner.get("transaction_id"), str)
            or typed_source_digests is None
            or typed_source_digests.get("registry") != HISTORICAL_REGISTRY_DIGEST
        ):
            raise TrainerAuthorityRemovalError("historical owner-v1 authority is invalid")
        answers = _object(
            _read_private(self.paths.baseline_candidate, "baseline candidate"),
            "baseline candidate",
        ).get("source_answers")
        if not isinstance(answers, dict) or len(answers) != 22:
            raise TrainerAuthorityRemovalError("the sealed 22-answer set is incomplete")
        publications = self._publication_bindings()
        candidate_bytes = _read_private(self.paths.candidate_manifest, "candidate manifest")
        candidate = _object(candidate_bytes, "candidate manifest")
        freeze = _object(_read_private(self.paths.freeze_receipt, "freeze receipt"), "freeze receipt")
        pre = _object(
            _read_private(self.paths.pre_supplemental, "pre-supplemental evidence"),
            "pre-supplemental evidence",
        )
        if (
            _sha(candidate_bytes) != MANIFEST_DIGEST
            or candidate.get("candidate_digest") != CANDIDATE_DIGEST
            or freeze.get("release_candidate_digest") != CANDIDATE_DIGEST
            or freeze.get("manifest_digest") != MANIFEST_DIGEST
            or pre.get("release_candidate_digest") != CANDIDATE_DIGEST
            or pre.get("manifest_digest") != MANIFEST_DIGEST
            or pre.get("freeze_receipt_digest") != FREEZE_DIGEST
            or pre.get("evidence_digest") != PRE_SUPPLEMENTAL_DIGEST
            or cast(dict[str, Any], pre.get("files", {})).get("registry", {}).get("sha256") != SOURCE_DIGEST
        ):
            raise TrainerAuthorityRemovalError("candidate/freeze/pre-supplemental pins changed")
        self._assert_supplemental_absent()
        adoption_binding = self._callback_adoption_binding()
        inventory_summary = _inventory_summary(self._inventory)
        transaction_inventory_digest = _digest(
            {
                "transaction_id": _TRANSACTION_ID,
                "protected_process_inventory_digest": self._inventory_digest,
            }
        )
        return {
            "profile": PROFILE,
            "customer_key": CUSTOMER,
            "session_id": SESSION,
            "source_registry_digest": SOURCE_DIGEST,
            "target_registry_digest": TARGET_DIGEST,
            "historical_owner_v1_registry_digest": HISTORICAL_REGISTRY_DIGEST,
            "semantic_diff": list(SEMANTIC_DIFF),
            "customer_disabled": True,
            "activation_count": 0,
            "delivery_count": 0,
            "owner_v1_journal_digest": OWNER_JOURNAL_DIGEST,
            "owner_v1_transaction_id": owner["transaction_id"],
            "answer_count": 22,
            "answers_digest": _digest(answers),
            "attestation": publications["23"],
            "generation_25": publications["25"],
            "generation_26": publications["26"],
            "generation_27_absent": True,
            "generation_28_absent": True,
            "supplemental_authority_absent": True,
            "callback_storage_adoption_binding": adoption_binding,
            "candidate_digest": CANDIDATE_DIGEST,
            "candidate_manifest_digest": MANIFEST_DIGEST,
            "freeze_receipt_digest": FREEZE_DIGEST,
            "pre_supplemental_evidence_digest": PRE_SUPPLEMENTAL_DIGEST,
            "service_manager_fence_proof": self._last_service_fence_proof,
            "protected_process_inventory": self._inventory,
            "protected_process_inventory_digest": self._inventory_digest,
            "protected_process_inventory_summary": inventory_summary,
            "process_inventory_transaction_digest": transaction_inventory_digest,
            "quiescence_fence_digest": _digest(
                {
                    "service_manager_fence_proof": self._last_service_fence_proof,
                    "protected_process_inventory_digest": self._inventory_digest,
                }
            ),
            "quiescence_proof": {
                "profile_processes": 0,
                "profile_open_files": 0,
                "locks_nonblocking": True,
                "inventory_stable_across_lock_boundary": True,
            },
            "canonical_lock_order": [
                "gateway.lock",
                *(str(path.relative_to(self.paths.profile_root)) for path in self.paths.authority_locks),
                str(self.paths.transaction_lock.relative_to(self.paths.profile_root)),
            ],
            "migration_source_digest": _sha(Path(__file__).read_bytes()),
            "target_registry_bytes_raw": target,
        }

    def _callback_adoption_binding(self) -> dict[str, object]:
        try:
            from gateway.platforms.task22_callback_storage_adoption import (
                verify_callback_storage_adoption_receipt,
            )

            return verify_callback_storage_adoption_receipt(
                self.paths.profile_root,
                receipt_path=self.paths.adoption_receipt,
                require_empty_pair=True,
            )
        except (OSError, RuntimeError, ValueError) as exc:
            raise TrainerAuthorityRemovalError(
                "initialized-empty callback storage adoption receipt is unavailable"
            ) from exc

    def _publication_bindings(self) -> dict[str, object]:
        outbox = GatewayOnboardingPublicationOutbox(self.paths.profile_root, initialize=False)
        # Quiescence plus the migration's held outbox lock provides the same
        # exclusion as records(); avoid recursively flocking a second descriptor.
        records = tuple(
            item for item in outbox._read_primary_unlocked()
            if item.session_id == SESSION
        )
        result: dict[str, object] = {}
        expected = {23: ("customer", "customer_attestation"), 25: ("owner", "owner_review"), 26: ("owner", "ready")}
        for generation, (role, state) in expected.items():
            matches = tuple(item for item in records if item.generation == generation)
            if (
                len(matches) != 1
                or matches[0].state != "COMMITTED"
                or matches[0].role != role
                or matches[0].payload.get("state") != state
                or matches[0].receipt_integrity is None
            ):
                raise TrainerAuthorityRemovalError(f"generation {generation} binding is invalid")
            item = matches[0]
            result[str(generation)] = {
                "generation": generation,
                "role": role,
                "state": state,
                "payload_digest": item.payload_digest,
                "dispatch_identity": item.dispatch_identity,
                "receipt_integrity": item.receipt_integrity,
                "message_id": item.message_id,
            }
        if any(item.generation >= 27 for item in records):
            raise TrainerAuthorityRemovalError("generation 27/28 or later authority is present")
        return result

    def _assert_no_activation_or_delivery(self) -> None:
        root = self.paths.profile_root
        forbidden = (
            root / "data" / "customer-activation-journal.json",
            root / "data" / "activation-completion-notices.jsonl",
            root / "data" / "customer-activation-audit.jsonl",
            root / "data" / "scheduled-deliveries.jsonl",
            root / "data" / "scheduled-deliveries-fence.json",
            root / "data" / "customer-schedule-claims",
            root / "data" / "nutrition-plans" / "feature-epoch.json",
            root / "data" / "owner-actions" / "draft-deliveries.json",
        )
        if any(_contains_authority(path) for path in forbidden):
            raise TrainerAuthorityRemovalError("activation or delivery authority is present")
        service = root / "data" / "owner-actions" / "customer-service-state.json"
        if service.exists():
            states = _object(_read_private(service, "customer service state"), "customer service state").get("states")
            if states not in ({}, None):
                raise TrainerAuthorityRemovalError("activation or delivery service state is present")

    def _assert_supplemental_absent(self) -> None:
        for path in (self.paths.supplemental_journal, self.paths.supplemental_lock):
            if path.exists() or path.is_symlink():
                raise TrainerAuthorityRemovalError("supplemental authority already exists")
        callback = self.paths.outbox_root / "owner-callbacks.json"
        if callback.exists() or callback.is_symlink():
            document = _object(_read_private(callback, "owner callback ledger"), "owner callback ledger")
            records = document.get("records")
            if not isinstance(records, list) or records:
                raise TrainerAuthorityRemovalError("supplemental callback authority already exists")

    def _verify_target(self, registry_bytes: bytes, *, load: bool) -> None:
        if _sha(registry_bytes) != TARGET_DIGEST:
            raise TrainerAuthorityRemovalError("target registry digest is invalid")
        target = _object(registry_bytes, "target registry")
        row = _customer(target)
        if row.get("enabled") is not False or "trainer" in row:
            raise TrainerAuthorityRemovalError("target registry authority is invalid")
        if load:
            try:
                loaded = self._canonical_loader(self.paths.profile_root)
            except Exception as exc:
                raise TrainerAuthorityRemovalError("canonical registry loader rejected target") from exc
            match = tuple(item for item in loaded.customers if item.spec.customer_key == CUSTOMER)
            if len(match) != 1 or match[0].spec.enabled is not False or match[0].spec.trainer is not None:
                raise TrainerAuthorityRemovalError("canonical registry loader did not prove trainer absence")

    def _event(
        self,
        *,
        phase: RemovalPhase,
        sequence: int,
        previous_event_digest: str | None,
        payload: Mapping[str, object],
    ) -> dict[str, object]:
        unsigned: dict[str, object] = {
            "schema": _SCHEMA,
            "transaction_id": _TRANSACTION_ID,
            "phase": phase.value,
            "sequence": sequence,
            "previous_event_digest": previous_event_digest,
            **payload,
        }
        digest = _digest(unsigned)
        signed = {**unsigned, "event_digest": digest}
        return {**signed, "integrity": hmac.new(self._key(), _canonical(signed), hashlib.sha256).hexdigest()}

    def _verify_prepared(self, event: Mapping[str, object]) -> None:
        self._verify_event(event, RemovalPhase.PREPARED, 0, None)
        expected = {
            "profile": PROFILE,
            "customer_key": CUSTOMER,
            "session_id": SESSION,
            "source_registry_digest": SOURCE_DIGEST,
            "target_registry_digest": TARGET_DIGEST,
            "historical_owner_v1_registry_digest": HISTORICAL_REGISTRY_DIGEST,
            "semantic_diff": list(SEMANTIC_DIFF),
            "candidate_digest": CANDIDATE_DIGEST,
            "candidate_manifest_digest": MANIFEST_DIGEST,
            "freeze_receipt_digest": FREEZE_DIGEST,
            "pre_supplemental_evidence_digest": PRE_SUPPLEMENTAL_DIGEST,
            "owner_v1_journal_digest": OWNER_JOURNAL_DIGEST,
            "answer_count": 22,
            "generation_27_absent": True,
            "generation_28_absent": True,
            "supplemental_authority_absent": True,
            "retained_before_image_name": self.paths.retained_before_image.name,
            "exchange_protocol": "linux-renameat2-rename-exchange-v1",
        }
        inventory = event.get("protected_process_inventory")
        inventory_digest = event.get("protected_process_inventory_digest")
        if (
            not isinstance(inventory, dict)
            or inventory_digest != _digest(inventory)
            or event.get("protected_process_inventory_summary")
            != _inventory_summary(cast(dict[str, object], inventory))
            or event.get("process_inventory_transaction_digest")
            != _digest(
                {
                    "transaction_id": _TRANSACTION_ID,
                    "protected_process_inventory_digest": inventory_digest,
                }
            )
            or event.get("quiescence_fence_digest")
            != _digest(
                {
                    "service_manager_fence_proof": event.get("service_manager_fence_proof"),
                    "protected_process_inventory_digest": inventory_digest,
                }
            )
        ):
            raise TrainerAuthorityRemovalError(
                "prepared process inventory binding is invalid"
            )
        adoption = event.get("callback_storage_adoption_binding")
        typed_adoption = (
            cast(dict[str, object], adoption)
            if isinstance(adoption, dict)
            else None
        )
        if (
            typed_adoption is None
            or typed_adoption.get("schema") != "task22-callback-storage-adoption-v1"
            or typed_adoption.get("classification") != "initialized_empty_canonical_callback_storage"
            or typed_adoption.get("callback_record_count") != 0
            or typed_adoption.get("grants") != []
        ):
            raise TrainerAuthorityRemovalError("prepared removal adoption binding is invalid")
        if any(event.get(key) != value for key, value in expected.items()):
            raise TrainerAuthorityRemovalError("prepared removal event pins are invalid")
        try:
            old = base64.b64decode(cast(str, event["source_registry_bytes_b64"]), validate=True)
            target = base64.b64decode(cast(str, event["target_registry_bytes_b64"]), validate=True)
        except (KeyError, TypeError, ValueError) as exc:
            raise TrainerAuthorityRemovalError("prepared registry bytes are invalid") from exc
        if _sha(old) != SOURCE_DIGEST or _sha(target) != TARGET_DIGEST:
            raise TrainerAuthorityRemovalError("prepared registry bytes do not match pins")
        source_identity = _event_identity(event, "pinned_source_identity")
        target_identity = _event_identity(event, "prepared_target_identity")
        if (
            source_identity.get("sha256") != SOURCE_DIGEST
            or source_identity.get("length") != len(old)
            or target_identity.get("sha256") != TARGET_DIGEST
            or target_identity.get("length") != len(target)
        ):
            raise TrainerAuthorityRemovalError("prepared exchange identities are invalid")
        _verify_directory_binding(
            event.get("registry_parent_binding"),
            self.paths.profile_root,
            self.paths.registry.parent,
        )
        _prove_semantic_diff(_object(old, "source registry"), _object(target, "target registry"))

    def _verify_committed(self, prepared: Mapping[str, object], committed: Mapping[str, object]) -> None:
        self._verify_prepared(prepared)
        self._verify_event(
            committed,
            RemovalPhase.COMMITTED,
            1,
            cast(str, prepared.get("event_digest")),
        )
        if (
            committed.get("prepared_event_digest") != prepared.get("event_digest")
            or committed.get("registry_digest") != TARGET_DIGEST
            or committed.get("canonical_loader_verified") is not True
            or committed.get("trainer_absent") is not True
            or committed.get("customer_disabled") is not True
            or committed.get("rollback_policy") != "forward_only_never_restore_trainer"
            or committed.get("exchange_validated") is not True
            or committed.get("retained_before_image_name")
            != prepared.get("retained_before_image_name")
            or committed.get("pinned_source_identity")
            != prepared.get("pinned_source_identity")
            or committed.get("prepared_target_identity")
            != prepared.get("prepared_target_identity")
            or committed.get("protected_process_inventory_digest")
            != prepared.get("protected_process_inventory_digest")
            or committed.get("protected_process_inventory_summary")
            != prepared.get("protected_process_inventory_summary")
            or committed.get("process_inventory_transaction_digest")
            != prepared.get("process_inventory_transaction_digest")
            or committed.get("quiescence_fence_digest")
            != prepared.get("quiescence_fence_digest")
        ):
            raise TrainerAuthorityRemovalError("committed removal receipt is invalid")
        registry_parent_binding = _verify_directory_binding(
            committed.get("registry_parent_binding"),
            self.paths.profile_root,
            self.paths.registry.parent,
        )
        if registry_parent_binding != prepared.get("registry_parent_binding"):
            raise TrainerAuthorityRemovalError("committed registry parent binding is invalid")

[Showing lines 1-1071 of 2112 (50.0KB limit). Use offset=1072 to continue.]


--- data/customers/task22_dm_rehearsal/nutrition-onboarding/session.json ---
sha256= d1b4365c9a097f553da4ec08f6a1b560f718b05e15b3775f5d1011441ba24b42 keys= ['schema_version', 'sessions']
summary= {}
sessions type= dict count= 1

--- data/customers/task22_dm_rehearsal/nutrition-onboarding/ready.json ---
sha256= 71a055a3b6da30d33608ef1b4b3feddd0490e5ef5ae737bf10440a0c89954771 keys= ['answers', 'authority_digest', 'baseline_digest', 'consumed_updates', 'cursor', 'customer_key', 'input_reconciliation_digest', 'owner_reviewed', 'readiness_pointer_digest', 'review_flow', 'schema_version', 'state']
summary= {"customer_key": "task22_dm_rehearsal", "state": "ready"}

--- data/customers/task22_dm_rehearsal/nutrition-onboarding/readiness-current.json ---
sha256= 2056daf0033dd68356488e41a2b4b5c5b818b87987e425c5c8fc932e152a1416 keys= ['bundle_digest', 'digest', 'input_reconciliation_digest', 'readiness_receipt_digest', 'revision', 'schema_version']
summary= {}

--- data/customers/task22_dm_rehearsal/nutrition-onboarding/readiness-receipt-v1.json ---
sha256= ac1d480c3cf6e3e58b0f70392c931d9280725b1cdd4ce6de1a41479cbf039160 keys= ['activation_enabled', 'adjustment_policy_digest', 'baseline_digest', 'calculation_digest', 'delivery_enabled', 'digest', 'expires_at_kst', 'feature_epoch_digest', 'input_reconciliation_digest', 'issued_at_kst', 'owner_review_receipt', 'privacy_consent_digest', 'privacy_consent_version', 'restriction_kb_digest', 'restriction_reconciliation_digest', 'schema_version']
summary= {"activation_enabled": false, "delivery_enabled": false}

--- data/customers/task22_dm_rehearsal/nutrition-onboarding/migrations/task22-supplemental-owner-review-v1.json ---
sha256= f5fc5eebff8970c72270e05b56f88baa83d69404ef4098bacae6ba2ef8af612e keys= ['approval_digest', 'attestation_receipt_integrity', 'authority_digest', 'baseline_candidate_digest', 'baseline_digest', 'bootstrap_digest', 'callback_envelope_digest', 'callback_event_integrity', 'callback_storage_adoption_binding', 'candidate_digest', 'candidate_manifest_digest', 'candidate_manifest_path', 'classifications', 'customer_key', 'file_identities', 'integrity', 'original_journal_digest', 'original_transaction_id', 'owner_route', 'phase_events', 'profile', 'publication_bindings', 'readiness_bundle_digest', 'registry_digest', 'review_commit_digest', 'review_epoch', 'review_update_id', 'schema', 'sealed_sources', 'session_id', 'terminal_publication_binding', 'terminal_receipt_integrity', 'terminal_transition_digest', 'trainer_removal_binding', 'transaction_id']
summary= {"customer_key": "task22_dm_rehearsal", "schema": "task22-supplemental-owner-review-v1", "session_id": "rb_Ar-NuKG3LH8CqHPXUgc88g"}

--- data/onboarding/telegram-room-bootstrap-v1/ledger.json ---
sha256= f3a3dd5b7aec01200f7e577a0367365717aef5b96a528de01c0116a81316ee6d keys= ['payload_digest', 'schema', 'sessions']
summary= {"schema": "telegram-room-bootstrap-v1"}
sessions type= list count= 1