"""Guarded operator cutover for one committed Telegram room bootstrap.

The coordinator is transport-free.  It validates the complete target before it
retires the explicitly named current customer, delegates registry mutations to
the profile-local canonical customer-admin API, and then releases bootstrap
ingress through :class:`TelegramRoomBootstrapActivation`.
"""

from __future__ import annotations

import hashlib
import importlib
import json
import stat
import sys
from collections.abc import Callable, Mapping
from dataclasses import dataclass, replace
from datetime import date, datetime, timedelta
from pathlib import Path
from types import ModuleType
from typing import Final, TypeGuard, cast
from zoneinfo import ZoneInfo

import yaml

from gateway.platforms.telegram_activation_readiness import (
    ActivationReadinessAuthority,
    ActivationReadinessError,
    DurableActivationReadinessProof,
    persist_activation_readiness_receipt,
    require_activation_readiness,
    validate_activation_readiness_receipt,
)
from gateway.platforms.telegram_nutrition_onboarding_supplemental_review import (
    SupplementalReviewError,
    require_task22_supplemental_for_activation,
)
from gateway.platforms.telegram_nutrition_addresses import (
    AddressConfigurationError,
    parse_customer_private_delivery_address,
)
from gateway.platforms.telegram_room_bootstrap import (
    BootstrapError,
    BootstrapState,
    Role,
    RoomBootstrapSession,
    RoomBootstrapStore,
)
from gateway.platforms.telegram_room_bootstrap_activation import (
    TelegramRoomBootstrapActivation,
)

_KST: Final = ZoneInfo("Asia/Seoul")
_FEATURE_FLAGS: Final = (
    "analytics_shadow",
    "operator_candidates",
    "activation",
    "delivery",
)

class ActivationCutoverError(BootstrapError):
    """The operator request or canonical cutover evidence failed closed."""

class _PostEnableAuthorityLoss(ActivationCutoverError):
    """Authenticated target authority failed after canonical activation."""

@dataclass(frozen=True, slots=True)
class ActivationCutoverReceipt:
    """Redactable result for a preflight, committed cutover, or replay."""

    ready: bool
    phase: str
    session_state: str
    generation: int
    session_digest: str
    target_digest: str
    current_digest: str
    checklist_digest: str
    registry_digest: str
    config_digest: str
    config_delivery: bool
    config_activation: bool
    feature_epoch_delivery: bool
    feature_epoch_activation: bool
    reconciled: bool = False

    def redacted_dict(self) -> dict[str, object]:
        return {
            "ready": self.ready,
            "phase": self.phase,
            "session_state": self.session_state,
            "generation": self.generation,
            "session_digest": self.session_digest,
            "target_digest": self.target_digest,
            "current_digest": self.current_digest,
            "checklist_digest": self.checklist_digest,
            "registry_digest": self.registry_digest,
            "config_digest": self.config_digest,
            "config_delivery": self.config_delivery,
            "config_activation": self.config_activation,
            "feature_epoch_delivery": self.feature_epoch_delivery,
            "feature_epoch_activation": self.feature_epoch_activation,
            "reconciled": self.reconciled,
        }

@dataclass(frozen=True, slots=True)
class _CutoverPlan:
    session: RoomBootstrapSession
    registry_path: Path
    target: object
    current_customer: str
    phase: str
    checklist_digest: str
    registry_digest: str
    config_digest: str
    config_delivery: bool
    config_activation: bool
    feature_epoch_delivery: bool
    feature_epoch_activation: bool

class TelegramRoomBootstrapCutover:
    """Preflight and execute one explicit test-to-real customer cutover."""

    def __init__(
        self,
        profile_root: Path | str,
        store: RoomBootstrapStore,
        *,
        package_root: Path | str | None = None,
        now: Callable[[], datetime] | None = None,
        provider_ready: Callable[[], bool] | None = None,
    ) -> None:
        root = _safe_directory(Path(profile_root), "activation profile root")
        self.profile_root = root
        self.store = store
        requested_package = Path(
            package_root or root / "workspace" / "checkin_cli"
        )
        self.package_root = _safe_directory(
            requested_package,
            "profile-local package root",
        )
        self._now: Callable[[], datetime] = now or (
            lambda: datetime.now(_KST)
        )
        if provider_ready is None:
            from gateway.platforms.nutrition_coaching import (
                preflight_nutrition_generation_provider,
            )

            provider_ready = preflight_nutrition_generation_provider
        self._provider_ready = provider_ready
        self.completion = TelegramRoomBootstrapActivation(
            root,
            store,
            package_root=self.package_root,
            now=lambda: self._now().astimezone(ZoneInfo("UTC")),
        )

    def preflight(
        self,
        session_id: str,
        *,
        expected_generation: int,
        customer_key: str,
        data_root: Path | str,
        checklist_path: Path | str,
        kst_date: date,
        expected_current_customer: str,
        confirm_disable_current_customer: str,
        readiness_authority: ActivationReadinessAuthority,
    ) -> ActivationCutoverReceipt:
        """Perform a read-only validation of every cutover prerequisite."""
        admin, coaching = self._profile_modules()
        try:
            with admin.profile_authority_lock(self.profile_root):
                plan = self._plan(
                    admin,
                    coaching,
                    session_id,
                    expected_generation=expected_generation,
                    customer_key=customer_key,
                    data_root=Path(data_root),
                    checklist_path=Path(checklist_path),
                    kst_date=kst_date,
                    expected_current_customer=expected_current_customer,
                    confirm_disable_current_customer=confirm_disable_current_customer,
                    readiness_authority=readiness_authority,
                )
            return self._receipt(plan)
        except ActivationCutoverError:
            raise
        except Exception as exc:
            raise ActivationCutoverError(
                "cutover prerequisite validation failed"
            ) from exc

    def execute(
        self,
        session_id: str,
        *,
        expected_generation: int,
        customer_key: str,
        data_root: Path | str,
        checklist_path: Path | str,
        kst_date: date,
        expected_current_customer: str,
        confirm_disable_current_customer: str,
        readiness_authority: ActivationReadinessAuthority,
    ) -> ActivationCutoverReceipt:
        """Commit or reconcile the exact cutover, then complete bootstrap."""
        self._authenticate_request(
            session_id,
            expected_generation=expected_generation,
            customer_key=customer_key,
        )
        admin, coaching = self._profile_modules()
        requested_data = Path(data_root)
        requested_checklist = Path(checklist_path)
        with admin.profile_authority_lock(self.profile_root):
            try:
                authenticated_plan = self._plan(
                    admin,
                    coaching,
                    session_id,
                    expected_generation=expected_generation,
                    customer_key=customer_key,
                    data_root=requested_data,
                    checklist_path=requested_checklist,
                    kst_date=kst_date,
                    expected_current_customer=expected_current_customer,
                    confirm_disable_current_customer=confirm_disable_current_customer,
                    readiness_authority=readiness_authority,
                )
            except _PostEnableAuthorityLoss:
                self._disable_target_if_enabled(admin, customer_key, session_id)
                raise

            try:
                return self._execute_locked(
                    admin,
                    coaching,
                    session_id,
                    authenticated_plan=authenticated_plan,
                    expected_generation=expected_generation,
                    customer_key=customer_key,
                    requested_data=requested_data,
                    requested_checklist=requested_checklist,
                    kst_date=kst_date,
                    expected_current_customer=expected_current_customer,
                    confirm_disable_current_customer=confirm_disable_current_customer,
                    readiness_authority=readiness_authority,
                )
            except BootstrapError:
                self._disable_target_if_enabled(admin, customer_key, session_id)
                raise
            except Exception as exc:
                if (
                    type(exc).__module__ == "checkin_cli.customer_admin"
                    and type(exc).__name__ == "CustomerAdminError"
                ):
                    self._disable_target_if_enabled(admin, customer_key, session_id)
                raise

    def _execute_locked(
        self,
        admin: ModuleType,
        coaching: ModuleType,
        session_id: str,
        *,
        authenticated_plan: _CutoverPlan,
        expected_generation: int,
        customer_key: str,
        requested_data: Path,
        requested_checklist: Path,
        kst_date: date,
        expected_current_customer: str,
        confirm_disable_current_customer: str,
        readiness_authority: ActivationReadinessAuthority,
    ) -> ActivationCutoverReceipt:
        plan = authenticated_plan
        if plan.phase == "ready":
            persist_activation_readiness_receipt(
                self.profile_root,
                plan.session,
                plan.target,
                readiness_authority,
                consent_version=coaching.CONSENT_VERSION,
                now=self._now().astimezone(ZoneInfo("UTC")),
            )
            # Prove runtime initialization can succeed before retiring the current
            # customer. activate_customer repeats this idempotently under the same
            # canonical profile lock.
            admin.initialize_adaptive_customer(requested_data)
            self._require_feature_epoch_false(requested_data, admin=admin)
            admin.disable_customer(
                plan.registry_path,
                expected_current_customer,
            )
            plan = self._plan(
                    admin,
                    coaching,
                    session_id,
                    expected_generation=expected_generation,
                    customer_key=customer_key,
                    data_root=requested_data,
                    checklist_path=requested_checklist,
                    kst_date=kst_date,
                    expected_current_customer=expected_current_customer,
                    confirm_disable_current_customer=confirm_disable_current_customer,
                readiness_authority=readiness_authority,
            )
        if plan.phase == "current_disabled":
            admin.activate_customer(
                self.profile_root,
                requested_data,
                customer_key,
                requested_checklist,
                kst_date=kst_date,
            )
            plan = self._plan(
                    admin,
                    coaching,
                    session_id,
                    expected_generation=expected_generation,
                    customer_key=customer_key,
                    data_root=requested_data,
                    checklist_path=requested_checklist,
                    kst_date=kst_date,
                    expected_current_customer=expected_current_customer,
                    confirm_disable_current_customer=confirm_disable_current_customer,
                readiness_authority=readiness_authority,
            )
        if plan.phase == "complete":
            return self._receipt(plan, reconciled=True)
        if plan.phase != "target_activated":
            raise ActivationCutoverError("cutover recovery phase is invalid")
        completed = self.completion.complete(
            session_id,
            expected_generation=expected_generation,
            readiness_proof=DurableActivationReadinessProof(
                session_id=session_id,
                readiness_generation=plan.session.generation,
                phase="target_activated",
            ),
        )
        final_plan = replace(
            plan,
            session=completed.session,
            phase="complete",
        )
        return self._receipt(final_plan, reconciled=completed.reconciled)

    def _authenticate_request(
        self,
        session_id: str,
        *,
        expected_generation: int,
        customer_key: str,
    ) -> RoomBootstrapSession:
        if type(session_id) is not str or type(expected_generation) is not int:
            raise ActivationCutoverError("cutover selector or generation is invalid")
        if type(customer_key) is not str:
            raise ActivationCutoverError("cutover customer selector is invalid")
        live = self.store.get(session_id)
        if live.generation != expected_generation:
            raise ActivationCutoverError("cutover generation is stale")
        if live.customer_key != customer_key:
            raise ActivationCutoverError("bootstrap customer selector mismatch")
        return live

    def _disable_target_if_enabled(
        self,
        admin: ModuleType,
        customer_key: str,
        session_id: str,
    ) -> None:
        registry_path = admin._resolve_registry_path(self.profile_root)
        document = admin._read(registry_path)
        target = next(
            (
                row
                for row in document.customers
                if getattr(row, "customer_key", None) == customer_key
            ),
            None,
        )
        if target is None or getattr(target, "enabled", None) is not True:
            return
        try:
            if admin.validate_committed_activation(
                self.profile_root,
                registry_path,
                customer_key,
            ) is not True:
                return
        except Exception:
            return
        admin.disable_customer(registry_path, customer_key)
        live = self.store.get(session_id)
        if live.state in {
            BootstrapState.AWAITING_ACTIVATION,
            BootstrapState.ACTIVE,
        }:
            self.completion.fail_activation_recovery(
                session_id,
                expected_generation=live.generation,
                failure_code="activation_authority_rejected",
            )

    def _require_provider_authentication(self) -> None:
        try:
            ready = self._provider_ready() is True
        except Exception:
            ready = False
        if not ready:
            raise ActivationCutoverError(
                "PROVIDER_AUTHENTICATION_REQUIRED: run `hermes auth` for the configured "
                "Hermes auxiliary provider, then retry the existing activation request"
            )

    def _plan(
        self,
        admin: ModuleType,
        coaching: ModuleType,
        session_id: str,
        *,
        expected_generation: int,
        customer_key: str,
        data_root: Path,
        checklist_path: Path,
        kst_date: date,
        expected_current_customer: str,
        confirm_disable_current_customer: str,
        readiness_authority: ActivationReadinessAuthority,
    ) -> _CutoverPlan:
        if (
            type(session_id) is not str
            or type(expected_generation) is not int
            or type(customer_key) is not str
            or type(expected_current_customer) is not str
            or customer_key == expected_current_customer
        ):
            raise ActivationCutoverError("cutover selector or customer authority is invalid")
        if confirm_disable_current_customer != expected_current_customer:
            raise ActivationCutoverError("explicit disable confirmation does not match")
        current_day = self._now().astimezone(_KST).date()
        if type(kst_date) is not date or kst_date != current_day:
            raise ActivationCutoverError("activation date is not the current KST date")

        live = self.store.get(session_id)
        if live.generation != expected_generation:
            raise ActivationCutoverError("cutover generation is stale")
        if live.customer_key != customer_key:
            raise ActivationCutoverError("bootstrap customer selector mismatch")
        if (
            live.state is BootstrapState.FAILED
            and live.failure_code == "activation_authority_rejected"
        ):
            raise ActivationCutoverError(
                "terminal activation recovery requires a new supported bootstrap"
            )
        if live.state not in {BootstrapState.AWAITING_ACTIVATION, BootstrapState.ACTIVE}:
            raise ActivationCutoverError("session is not AWAITING_ACTIVATION or completed")

        registry_path = admin._resolve_registry_path(self.profile_root)
        # This canonical validator also reconciles or rejects malformed activation
        # journals before any new authority operation.
        admin.validate_committed_activation(self.profile_root, registry_path)
        document = admin._read(registry_path)
        targets = tuple(
            row for row in document.customers
            if getattr(row, "customer_key", None) == customer_key
        )
        currents = tuple(
            row for row in document.customers
            if getattr(row, "customer_key", None) == expected_current_customer
        )
        if len(targets) != 1:
            raise ActivationCutoverError("canonical target customer is missing or ambiguous")
        if len(currents) != 1:
            raise ActivationCutoverError("expected currently enabled customer is missing")
        target = targets[0]
        current = currents[0]
        enabled = tuple(row for row in document.customers if row.enabled)
        enabled_keys = tuple(row.customer_key for row in enabled)
        if enabled_keys == (expected_current_customer,):
            phase = "ready"
        elif not enabled_keys and not current.enabled and not target.enabled:
            phase = "current_disabled"
        elif enabled_keys == (customer_key,) and not current.enabled:
            phase = "complete" if live.state is BootstrapState.ACTIVE else "target_activated"
        else:
            raise ActivationCutoverError("exact currently enabled customer does not match")
        if live.state is BootstrapState.ACTIVE and phase != "complete":
            raise ActivationCutoverError("completed bootstrap activation evidence mismatches")
        if phase in {"ready", "current_disabled"}:
            self._require_provider_authentication()
        if phase == "current_disabled":
            # A zero-enabled registry is accepted only as the exact crash boundary
            # produced by retiring the named current customer. Reconstruct the
            # immediately preceding registry and verify its canonical receipt.
            recovery_payload = document.model_dump(mode="json")
            recovery_payload["customers"] = [
                row.model_copy(
                    update={"enabled": row.customer_key == expected_current_customer}
                ).model_dump(mode="json")
                for row in document.customers
            ]
            recovery_document = admin.RegistryDocument.model_validate(recovery_payload)
            recovery_current = next(
                row
                for row in recovery_document.customers
                if row.customer_key == expected_current_customer
            )
            admin._require_committed_activation_receipt(
                self.profile_root,
                registry_path,
                recovery_document,
                recovery_current,
            )

        try:
            require_task22_supplemental_for_activation(
                self.profile_root,
                customer_key=live.customer_key,
                session_id=live.session_id,
            )
            if phase == "ready":
                require_activation_readiness(
                    live,
                    target,
                    readiness_authority,
                    consent_version=coaching.CONSENT_VERSION,
                    now=self._now().astimezone(ZoneInfo("UTC")),
                )
            else:
                readiness_generation = (
                    live.generation - 1
                    if phase == "complete"
                    else live.generation
                )
                validate_activation_readiness_receipt(
                    self.profile_root,
                    live,
                    target,
                    consent_version=coaching.CONSENT_VERSION,
                    now=self._now().astimezone(ZoneInfo("UTC")),
                    expected_readiness_generation=readiness_generation,
                )
        except (ActivationReadinessError, SupplementalReviewError) as exc:
            if phase in {"target_activated", "complete"}:
                raise _PostEnableAuthorityLoss(str(exc)) from exc
            if phase != "current_disabled":
                raise ActivationCutoverError(str(exc)) from exc
            require_activation_readiness(
                live,
                target,
                readiness_authority,
                consent_version=coaching.CONSENT_VERSION,
                now=self._now().astimezone(ZoneInfo("UTC")),
            )
            persist_activation_readiness_receipt(
                self.profile_root,
                live,
                target,
                readiness_authority,
                consent_version=coaching.CONSENT_VERSION,
                now=self._now().astimezone(ZoneInfo("UTC")),
            )

        self._validate_routes_and_consent(live, target, coaching)
        starts_on = target.plan.starts_on
        if not starts_on <= kst_date <= starts_on + timedelta(days=27):
            raise ActivationCutoverError("activation is outside the current KST plan window")
        requested_data = _safe_directory(data_root, "real customer data root")
        expected_data = (self.profile_root / "data" / "customers" / customer_key).resolve()
        if requested_data != expected_data:
            raise ActivationCutoverError("real customer data root does not match target")
        requested_checklist = _safe_file(checklist_path, "customer G1-G5 checklist")
        checklist_payload = self._customer_specific_checklist(
            requested_checklist,
            customer_key=customer_key,
            data_root=requested_data,
            registry_path=registry_path,
            kst_date=kst_date,
            consent_version=coaching.CONSENT_VERSION,
        )

        if phase in {"ready", "current_disabled"}:
            admin._validate_activation_data_root(
                self.profile_root,
                requested_data,
                expected_data,
                document,
                target,
            )
            target.validate_local_activation_prerequisites()
            projected = target.model_copy(update={"enabled": True})
            payload = document.model_dump(mode="json")
            payload["customers"] = [
                row.model_copy(update={"enabled": row.customer_key == customer_key}).model_dump(mode="json")
                for row in document.customers
            ]
            projected_document = admin.RegistryDocument.model_validate(payload)
            projected_document.validate_enabled_customer(projected)
            admin._read_activation_checklist(
                requested_checklist,
                self.profile_root,
                requested_data,
                registry_path,
                target,
            )
        else:
            admin.validate_committed_activation(
                self.profile_root,
                registry_path,
                customer_key,
            )

        config_digest, config_delivery, config_activation = self._config_flags()
        feature_delivery, feature_activation = self._require_feature_epoch_false(
            requested_data,
            admin=admin,
            required=phase in {"target_activated", "complete"},
        )
        return _CutoverPlan(
            live,
            registry_path,
            target,
            expected_current_customer,
            phase,
            _digest_bytes(json.dumps(checklist_payload, sort_keys=True, separators=(",", ":")).encode()),
            _digest_bytes(registry_path.read_bytes()),
            config_digest,
            config_delivery,
            config_activation,
            feature_delivery,
            feature_activation,
        )

    @staticmethod
    def _validate_routes_and_consent(
        session: RoomBootstrapSession,
        target: object,
        coaching: ModuleType,
    ) -> None:
        customer = session.role_claim(Role.CUSTOMER)
        telegram = getattr(target, "telegram", None)
        consent = getattr(target, "ai_processing_consent", None)
        try:
            customer_address = parse_customer_private_delivery_address({
                "user_id": getattr(telegram, "user_id", None),
                "chat_id": getattr(telegram, "chat_id", None),
                "topic_id": getattr(telegram, "topic_id", None),
            })
        except AddressConfigurationError as exc:
            raise ActivationCutoverError("canonical target route mismatch") from exc
        if (
            customer is None
            or str(customer_address.user_id) != customer.user_id
        ):
            raise ActivationCutoverError("canonical target route mismatch")
        if (
            getattr(consent, "granted", None) is not True
            or getattr(consent, "recorded_on", None) is None
            or getattr(consent, "notice_version", None) != coaching.CONSENT_VERSION
        ):
            raise ActivationCutoverError("current-version customer consent is not granted")

    def _customer_specific_checklist(
        self,
        path: Path,
        *,
        customer_key: str,
        data_root: Path,
        registry_path: Path,
        kst_date: date,
        consent_version: str,
    ) -> dict[str, object]:
        try:
            raw: object = json.loads(path.read_text(encoding="utf-8"))
            payload = _string_object_dict(raw)
        except (OSError, UnicodeDecodeError, ValueError, json.JSONDecodeError) as exc:
            raise ActivationCutoverError("customer checklist is malformed") from exc
        required = {
            "customer_key": customer_key,
            "profile_root": str(self.profile_root),
            "data_root": str(data_root),
            "registry_path": str(registry_path),
            "checked_on_kst": kst_date.isoformat(),
            "consent_version": consent_version,
        }
        for key, expected in required.items():
            actual = payload.get(key)
            if key.endswith("root") or key.endswith("path"):
                try:
                    matches = Path(str(actual)).resolve(strict=True) == Path(expected).resolve(strict=True)
                except (OSError, RuntimeError):
                    matches = False
            else:
                matches = actual == expected
            if not matches:
                if key == "checked_on_kst":
                    raise ActivationCutoverError("customer checklist is not current")
                raise ActivationCutoverError("customer-specific checklist evidence mismatches")
        return payload

    def _config_flags(self) -> tuple[str, bool, bool]:
        candidates = (self.profile_root / "config.yaml", self.profile_root / "config.yml")
        existing = tuple(path for path in candidates if path.exists() or path.is_symlink())
        if len(existing) != 1:
            raise ActivationCutoverError("profile config is missing or ambiguous")
        path = _safe_file(existing[0], "profile config")
        if path.stat().st_size > 1024 * 1024:
            raise ActivationCutoverError("profile config is unsafe")
        try:
            raw: object = yaml.safe_load(path.read_text(encoding="utf-8"))
            payload = _string_object_mapping(raw)
        except (OSError, UnicodeDecodeError, ValueError, yaml.YAMLError) as exc:
            raise ActivationCutoverError("profile config is malformed") from exc
        adaptive: object = payload.get("adaptive_nutrition")
        platforms = payload.get("platforms")
        if _is_string_object_mapping(platforms):
            telegram = platforms.get("telegram")
            if _is_string_object_mapping(telegram):
                extra = telegram.get("extra")
                if (
                    _is_string_object_mapping(extra)
                    and "adaptive_nutrition" in extra
                ):
                    adaptive = extra.get("adaptive_nutrition")
        if not _is_string_object_mapping(adaptive):
            raise ActivationCutoverError("adaptive nutrition config is unavailable")
        delivery = adaptive.get("delivery_enabled", adaptive.get("delivery"))
        activation = adaptive.get("activation", False)
        if delivery is not False or activation is not False or adaptive.get("delivery", False) is True:
            raise ActivationCutoverError("delivery and adaptive activation flags must remain false")
        return _digest_bytes(path.read_bytes()), False, False

    @staticmethod
    def _require_feature_epoch_false(
        data_root: Path,
        *,
        admin: ModuleType,
        required: bool = False,
    ) -> tuple[bool, bool]:
        path = data_root / "nutrition-plans" / "feature-epoch.json"
        if not path.exists() and not path.is_symlink():
            if required:
                raise ActivationCutoverError("adaptive feature epoch is unavailable")
            return False, False
        safe = _safe_file(path, "adaptive feature epoch")
        try:
            raw: object = json.loads(safe.read_text(encoding="utf-8"))
            payload = _string_object_dict(raw)
        except (OSError, UnicodeDecodeError, ValueError, json.JSONDecodeError) as exc:
            raise ActivationCutoverError("adaptive feature epoch is malformed") from exc
        if set(payload) != {
            "schema_version", "epoch", "config_digest", *_FEATURE_FLAGS
        }:
            raise ActivationCutoverError("adaptive feature epoch is malformed")
        flags = {flag: payload.get(flag) for flag in _FEATURE_FLAGS}
        try:
            expected_digest = admin.feature_config_digest(payload.get("epoch"), flags)
        except (TypeError, ValueError) as exc:
            raise ActivationCutoverError("adaptive feature epoch is malformed") from exc
        if (
            payload.get("schema_version") != "1.0"
            or payload.get("config_digest") != expected_digest
        ):
            raise ActivationCutoverError("adaptive feature epoch is malformed")
        if any(value is not False for value in flags.values()):
            raise ActivationCutoverError("delivery and adaptive activation flags must remain false")
        return False, False

    def _profile_modules(self) -> tuple[ModuleType, ModuleType]:
        package = self.package_root / "checkin_cli"
        if package.is_symlink() or not package.is_dir():
            raise ActivationCutoverError("profile-local checkin_cli is unavailable")
        root_text = str(self.package_root)
        if root_text not in sys.path:
            sys.path.insert(0, root_text)
        admin = importlib.import_module("checkin_cli.customer_admin")
        coaching = importlib.import_module("checkin_cli.customer_coaching")
        for module in (admin, coaching):
            source = Path(str(module.__file__)).resolve()
            if not source.is_relative_to(package.resolve()):
                raise ActivationCutoverError("loaded checkin_cli is not profile-local")
        return admin, coaching

    @staticmethod
    def _receipt(
        plan: _CutoverPlan,
        *,
        reconciled: bool = False,
    ) -> ActivationCutoverReceipt:
        return ActivationCutoverReceipt(
            ready=True,
            phase=plan.phase,
            session_state=plan.session.state.value,
            generation=plan.session.generation,
            session_digest=_digest_text(plan.session.session_id),
            target_digest=_digest_text(plan.session.customer_key),
            current_digest=_digest_text(plan.current_customer),
            checklist_digest=plan.checklist_digest,
            registry_digest=plan.registry_digest,
            config_digest=plan.config_digest,
            config_delivery=plan.config_delivery,
            config_activation=plan.config_activation,
            feature_epoch_delivery=plan.feature_epoch_delivery,
            feature_epoch_activation=plan.feature_epoch_activation,
            reconciled=reconciled,
        )

def _is_string_object_mapping(
    value: object,
) -> TypeGuard[Mapping[str, object]]:
    if not isinstance(value, Mapping):
        return False
    mapping = cast(Mapping[object, object], value)
    return all(isinstance(key, str) for key in mapping)

def _string_object_mapping(value: object) -> Mapping[str, object]:
    if not _is_string_object_mapping(value):
        raise ValueError
    return value

def _is_string_object_dict(value: object) -> TypeGuard[dict[str, object]]:
    if not isinstance(value, dict):
        return False
    mapping = cast(dict[object, object], value)
    return all(isinstance(key, str) for key in mapping)

def _string_object_dict(value: object) -> dict[str, object]:
    if not _is_string_object_dict(value):
        raise ValueError
    return value

def _safe_directory(path: Path, label: str) -> Path:
    _reject_symlink_chain(path)
    if path.is_symlink() or not path.exists() or not path.is_dir():
        raise ActivationCutoverError(f"{label} is unavailable or unsafe")
    return path.resolve(strict=True)

def _safe_file(path: Path, label: str) -> Path:
    _reject_symlink_chain(path)
    if path.is_symlink() or not path.exists() or not path.is_file():
        raise ActivationCutoverError(f"{label} is unavailable or unsafe")
    metadata = path.stat()
    if not stat.S_ISREG(metadata.st_mode):
        raise ActivationCutoverError(f"{label} is unavailable or unsafe")
    return path.resolve(strict=True)

def _reject_symlink_chain(path: Path) -> None:
    absolute = path.absolute()
    current = Path(absolute.anchor)
    for part in absolute.parts[1:]:
        current /= part
        try:
            if current.is_symlink():
                raise ActivationCutoverError("cutover path symlinks are not allowed")
        except OSError as exc:
            raise ActivationCutoverError("cutover path is unavailable") from exc

def _digest_bytes(value: bytes) -> str:
    return hashlib.sha256(value).hexdigest()

def _digest_text(value: str) -> str:
    return _digest_bytes(value.encode("utf-8"))

__all__ = [
    "ActivationCutoverError",
    "ActivationCutoverReceipt",
    "TelegramRoomBootstrapCutover",
]


"""Committed-activation completion boundary for Telegram room bootstrap.

This module never activates a customer. It only releases reserved bootstrap
ingress after the profile-local customer authority proves that activation was
already committed canonically.
"""

from __future__ import annotations

import hashlib
import importlib
import sys
from collections.abc import Callable
from dataclasses import dataclass, replace
from datetime import datetime, timezone
from pathlib import Path
from types import ModuleType

from gateway.platforms.telegram_activation_readiness import (
    ActivationReadinessError,
    DurableActivationReadinessProof,
    validate_activation_readiness_receipt,
)

from gateway.platforms.telegram_nutrition_addresses import (
    AddressConfigurationError,
    parse_customer_private_delivery_address,
)
from gateway.platforms.telegram_nutrition_onboarding_supplemental_review import (
    SupplementalReviewError,
    require_task22_supplemental_for_activation,
)
from gateway.platforms.telegram_room_bootstrap import (
    BootstrapError,
    BootstrapState,
    Role,
    RoomBootstrapSession,
    RoomBootstrapStore,
)

class ActivationCompletionConflict(BootstrapError):
    """Canonical activation evidence does not exactly match the bootstrap."""

@dataclass(frozen=True, slots=True)
class CommittedActivationReceipt:
    """Receipt that bootstrap ingress was released from committed evidence."""

    session: RoomBootstrapSession
    customer_key: str
    reconciled: bool

class TelegramRoomBootstrapActivation:
    """Complete one exact bootstrap after canonical activation has committed."""

    def __init__(
        self,
        profile_root: Path | str,
        store: RoomBootstrapStore,
        *,
        package_root: Path | str | None = None,
        now: Callable[[], datetime] | None = None,
    ) -> None:
        root = Path(profile_root)
        if root.is_symlink() or not root.exists() or not root.is_dir():
            raise BootstrapError("activation profile root is unavailable")
        self.profile_root = root.resolve()
        self.store = store
        self.package_root = Path(
            package_root or self.profile_root / "workspace" / "checkin_cli"
        ).resolve()
        self._now: Callable[[], datetime] = now or (
            lambda: datetime.now(timezone.utc)
        )

    def complete(
        self,
        session_id: str,
        *,
        expected_generation: int,
        readiness_proof: DurableActivationReadinessProof,
    ) -> CommittedActivationReceipt:
        """Validate fresh canonical evidence and release one exact reservation."""
        admin, coaching = self._profile_modules()
        try:
            registry_path = admin._resolve_registry_path(self.profile_root)
            with admin.profile_authority_lock(self.profile_root):
                live = self.store.get(session_id)
                if (
                    type(expected_generation) is not int
                    or expected_generation != live.generation
                ):
                    raise ActivationCompletionConflict(
                        "activation completion generation is stale"
                    )
                if live.state not in {
                    BootstrapState.AWAITING_ACTIVATION,
                    BootstrapState.ACTIVE,
                }:
                    raise ActivationCompletionConflict(
                        "session is not awaiting activation completion"
                    )

                document = admin._read(registry_path)
                customer = self._validate_canonical_customer(live, document, coaching)
                require_task22_supplemental_for_activation(
                    self.profile_root,
                    customer_key=live.customer_key,
                    session_id=live.session_id,
                )
                if admin.validate_committed_activation(
                    self.profile_root,
                    registry_path,
                    live.customer_key,
                ) is not True:
                    raise ActivationCompletionConflict(
                        "committed activation receipt is unavailable"
                    )

                expected_readiness_generation = (
                    live.generation - 1
                    if live.state is BootstrapState.ACTIVE
                    else live.generation
                )
                if (
                    readiness_proof.session_id != live.session_id
                    or readiness_proof.phase != "target_activated"
                    or readiness_proof.readiness_generation
                    != expected_readiness_generation
                ):
                    raise ActivationCompletionConflict(
                        "durable activation readiness proof is stale or mismatched"
                    )
                validate_activation_readiness_receipt(
                    self.profile_root,
                    live,
                    customer,
                    consent_version=self._consent_version(coaching),
                    now=self._now(),
                    expected_readiness_generation=expected_readiness_generation,
                )

                if live.state is BootstrapState.ACTIVE:
                    self._reserve_customer_notice(live, registry_path)
                    return CommittedActivationReceipt(
                        live,
                        live.customer_key,
                        reconciled=True,
                    )
                completed = self._complete_store_activation(live)
                self._reserve_customer_notice(completed, registry_path)
                return CommittedActivationReceipt(
                    completed,
                    live.customer_key,
                    reconciled=False,
                )
        except ActivationCompletionConflict:
            raise
        except (ActivationReadinessError, SupplementalReviewError) as exc:
            raise ActivationCompletionConflict(str(exc)) from exc
        except Exception as exc:
            raise ActivationCompletionConflict(
                "committed activation evidence is unavailable or mismatched"
            ) from exc

    def _complete_store_activation(
        self,
        session: RoomBootstrapSession,
    ) -> RoomBootstrapSession:
        """Invoke the store's capability-gated committed activation edge."""
        operation: object = getattr(
            self.store,
            "_complete_committed_activation",
            None,
        )
        if not callable(operation):
            raise ActivationCompletionConflict(
                "committed activation store authority is unavailable"
            )
        completed: object = operation(
            session.session_id,
            expected_generation=session.generation,
        )
        if not isinstance(completed, RoomBootstrapSession):
            raise ActivationCompletionConflict(
                "committed activation store authority returned invalid evidence"
            )
        return completed

    def fail_activation_recovery(
        self,
        session_id: str,
        *,
        expected_generation: int,
        failure_code: str,
    ) -> RoomBootstrapSession:
        """Record terminal failure after canonical activation must be disabled."""
        live = self.store.get(session_id)
        if live.state not in {
            BootstrapState.AWAITING_ACTIVATION,
            BootstrapState.ACTIVE,
        }:
            raise ActivationCompletionConflict(
                "session cannot record activation recovery failure"
            )
        if live.state is BootstrapState.AWAITING_ACTIVATION:
            return self.store.transition(
                session_id,
                expected_generation=expected_generation,
                target=BootstrapState.FAILED,
                failure_code=failure_code,
            )

        def fail_active(
            session: RoomBootstrapSession,
            now: datetime,
        ) -> RoomBootstrapSession:
            if session.state is not BootstrapState.ACTIVE:
                raise ActivationCompletionConflict(
                    "session cannot record activation recovery failure"
                )
            return replace(
                session,
                state=BootstrapState.FAILED,
                failure_code=failure_code,
                generation=session.generation + 1,
                updated_at=now,
            )

        operation: object = getattr(self.store, "_mutate", None)
        if not callable(operation):
            raise ActivationCompletionConflict(
                "activation recovery store authority is unavailable"
            )
        failed: object = operation(
            session_id,
            expected_generation,
            fail_active,
        )
        if not isinstance(failed, RoomBootstrapSession):
            raise ActivationCompletionConflict(
                "activation recovery store authority returned invalid evidence"
            )
        return failed

    def _reserve_customer_notice(
        self,
        session: RoomBootstrapSession,
        registry_path: Path,
    ) -> None:
        """Durably reserve the exact customer notice; provider I/O stays in gateway."""
        from gateway.platforms.telegram_activation_notice import ActivationNoticeStore

        admin, _ = self._profile_modules()
        document = admin._read(registry_path)
        rows = tuple(
            row
            for row in getattr(document, "customers", ())
            if getattr(row, "customer_key", None) == session.customer_key
        )
        if len(rows) != 1:
            raise ActivationCompletionConflict(
                "activation notice customer DM is unavailable"
            )
        persisted = getattr(rows[0], "telegram", None)
        try:
            customer = parse_customer_private_delivery_address(
                {
                    "user_id": getattr(persisted, "user_id", None),
                    "chat_id": getattr(persisted, "chat_id", None),
                    "topic_id": getattr(persisted, "topic_id", None),
                }
            )
        except AddressConfigurationError as exc:
            raise ActivationCompletionConflict(
                "activation notice customer DM is unavailable"
            ) from exc
        journal = self.profile_root / "data" / "customer-activation-journal.json"
        audit = self.profile_root / "data" / "customer-activation-audit.jsonl"
        try:
            authority_digest = hashlib.sha256(
                registry_path.read_bytes() + journal.read_bytes() + audit.read_bytes()
            ).hexdigest()
        except OSError as exc:
            raise ActivationCompletionConflict(
                "activation notice authority is unavailable"
            ) from exc
        ActivationNoticeStore(self.profile_root).reserve(
            customer_key=session.customer_key,
            starts_on=session.customer_draft.starts_on,
            daily_time=session.customer_draft.daily_time,
            destination={
                "user_id": str(customer.user_id),
                "chat_id": str(customer.chat_id),
                "topic_id": str(customer.topic_id),
            },
            authority_digest=authority_digest,
        )

    def _profile_modules(self) -> tuple[ModuleType, ModuleType]:
        package = self.package_root / "checkin_cli"
        if not package.is_dir() or not (package / "customer_admin.py").is_file():
            raise BootstrapError("profile-local checkin_cli is unavailable")
        root_text = str(self.package_root)
        if root_text not in sys.path:
            sys.path.insert(0, root_text)
        admin = importlib.import_module("checkin_cli.customer_admin")
        coaching = importlib.import_module("checkin_cli.customer_coaching")
        for module in (admin, coaching):
            source = Path(str(module.__file__)).resolve()
            if not source.is_relative_to(package.resolve()):
                raise BootstrapError("loaded checkin_cli is not profile-local")
        return admin, coaching

    @staticmethod
    def _consent_version(coaching: ModuleType) -> str:
        value: object = getattr(coaching, "CONSENT_VERSION", None)
        if not isinstance(value, str):
            raise ActivationCompletionConflict(
                "current customer AI processing consent version is unavailable"
            )
        return value

    @staticmethod
    def _validate_canonical_customer(
        session: RoomBootstrapSession,
        document: object,
        coaching: ModuleType,
    ) -> object:
        customers = tuple(getattr(document, "customers", ()))
        matches = tuple(
            row
            for row in customers
            if getattr(row, "customer_key", None) == session.customer_key
        )
        enabled = tuple(
            row for row in customers if getattr(row, "enabled", None) is True
        )
        if len(matches) != 1 or enabled != matches:
            raise ActivationCompletionConflict(
                "canonical enabled customer does not exactly match bootstrap"
            )
        row = matches[0]
        customer_claim = session.role_claim(Role.CUSTOMER)
        telegram = getattr(row, "telegram", None)
        try:
            customer = parse_customer_private_delivery_address(
                {
                    "user_id": getattr(telegram, "user_id", None),
                    "chat_id": getattr(telegram, "chat_id", None),
                    "topic_id": getattr(telegram, "topic_id", None),
                }
            )
        except AddressConfigurationError as exc:
            raise ActivationCompletionConflict(
                "canonical Telegram routes are not production-ready"
            ) from exc
        if (
            customer_claim is None
            or str(customer.user_id) != customer_claim.user_id
        ):
            raise ActivationCompletionConflict(
                "canonical Telegram routes do not exactly match bootstrap"
            )
        consent = getattr(row, "ai_processing_consent", None)
        if (
            getattr(consent, "granted", None) is not True
            or getattr(consent, "recorded_on", None) is None
            or getattr(consent, "notice_version", None)
            != TelegramRoomBootstrapActivation._consent_version(coaching)
        ):
            raise ActivationCompletionConflict(
                "current customer AI processing consent is unavailable"
            )
        return row

__all__ = [
    "ActivationCompletionConflict",
    "CommittedActivationReceipt",
    "TelegramRoomBootstrapActivation",
]


"""Fail-closed privacy and migration gate for production activation."""

from __future__ import annotations

import hashlib
import json
import os
import tempfile
from collections.abc import Mapping
from dataclasses import dataclass
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
from typing import Final, Literal, Never, TypedDict, TypeGuard, cast

from gateway.platforms.telegram_nutrition_addresses import (
    AddressConfigurationError,
    StaffReviewRole,
    is_legacy_shared_topic_configuration,
    parse_customer_private_delivery_address,
    parse_staff_review_address,
)
from gateway.platforms.telegram_room_bootstrap import Role

_MAX_AUTHORITY_AGE: Final = timedelta(minutes=5)
_MAX_CONSENT_AGE: Final = timedelta(days=30)
_OBSERVATION_SCHEMA: Final = "telegram-membership-observation-v2"
_RECEIPT_SCHEMA: Final = "telegram-activation-readiness-v2"
_CHECKS: Final = (
    "customer_dm_binding",
    "staff_review_routing",
    "legacy_shared_topic_migration",
    "customer_staff_separation",
    "privacy_consent",
    "identity_authority",
)

class TelegramMembershipRecord(TypedDict):
    """One provider-observed customer membership in a staff chat."""

    chat_id: int
    status: str

@dataclass(frozen=True, slots=True)
class TelegramMembershipObservation:
    """Complete provider observation for one customer and exact staff scope."""

    schema: str
    provider: str
    customer_user_id: str
    customer_private_chat_id: str
    owner_user_id: str
    scoped_staff_chat_ids: tuple[int, ...] | list[int]
    customer_memberships: (
        tuple[TelegramMembershipRecord, ...] | list[TelegramMembershipRecord]
    )
    observed_at: str
    complete: bool
    receipt_digest: str

    def __post_init__(self) -> None:
        object.__setattr__(
            self,
            "scoped_staff_chat_ids",
            tuple(self.scoped_staff_chat_ids),
        )
        object.__setattr__(
            self,
            "customer_memberships",
            tuple(dict(row) for row in self.customer_memberships),
        )

@dataclass(frozen=True, slots=True)
class ActivationReadinessAuthority:
    """Fresh identity, consent, and provider-observed membership evidence."""

    customer_key: str
    bootstrap_generation: int
    customer_user_id: str
    owner_user_id: str
    owner_chat_id: str
    owner_topic_id: str
    consent_notice_version: str
    consent_recorded_on: date
    consent_bootstrap_generation: int
    provider_observation: TelegramMembershipObservation

@dataclass(frozen=True, slots=True)
class ActivationReadinessReceipt:
    """Non-sensitive proof that every production topology check passed."""

    ready: bool
    checks: tuple[str, ...]

@dataclass(frozen=True, slots=True)
class DurableActivationReadinessProof:
    """Phase-specific handle to a durable readiness receipt."""

    session_id: str
    readiness_generation: int
    phase: Literal["target_activated"]

@dataclass(frozen=True, slots=True)
class _PersistedReadinessReceipt:
    schema: str
    session_digest: str
    customer_key_digest: str
    bootstrap_generation: int
    customer_binding_digest: str
    owner_chat_id: str
    owner_topic_id: str
    provider_observation: object
    consent_version: str
    consent_recorded_on: date
    committed_at: datetime
    payload_digest: str

class ActivationReadinessError(RuntimeError):
    """One redacted readiness rejection with exact operator remediation."""

    def __init__(self, code: str, corrective_action: str) -> None:
        self.code: str = code
        self.corrective_actions: tuple[str, ...] = (corrective_action,)
        super().__init__(f"{code}: {corrective_action}")

    def redacted_dict(self) -> dict[str, object]:
        return {
            "ready": False,
            "code": self.code,
            "corrective_actions": list(self.corrective_actions),
        }

def _canonical_json(value: object) -> bytes:
    return json.dumps(
        value,
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
    ).encode("utf-8")

def canonical_observation_digest(value: Mapping[str, object]) -> str:
    """Digest one unsigned provider observation."""
    return hashlib.sha256(_canonical_json(dict(value))).hexdigest()

def _observation_unsigned(
    observation: TelegramMembershipObservation,
) -> dict[str, object]:
    return {
        "schema": observation.schema,
        "provider": observation.provider,
        "customer_user_id": observation.customer_user_id,
        "customer_private_chat_id": observation.customer_private_chat_id,
        "owner_user_id": observation.owner_user_id,
        "scoped_staff_chat_ids": list(observation.scoped_staff_chat_ids),
        "customer_memberships": list(observation.customer_memberships),
        "observed_at": observation.observed_at,
        "complete": observation.complete,
    }

def _is_string_object_dict(value: object) -> TypeGuard[dict[str, object]]:
    if not isinstance(value, dict):
        return False
    mapping = cast(dict[object, object], value)
    return all(isinstance(key, str) for key in mapping)

def _string_object_dict(value: object) -> dict[str, object]:
    if not _is_string_object_dict(value):
        raise ValueError
    return value

def _is_object_list(value: object) -> TypeGuard[list[object]]:
    return isinstance(value, list)

def _membership_record(raw: object) -> TelegramMembershipRecord:
    value = _string_object_dict(raw)
    if set(value) != {"chat_id", "status"}:
        raise ValueError
    chat_id = value["chat_id"]
    status = value["status"]
    if type(chat_id) is not int or not isinstance(status, str):
        raise ValueError
    return {"chat_id": chat_id, "status": status}

def _required_string(value: dict[str, object], key: str) -> str:
    candidate = value[key]
    if not isinstance(candidate, str):
        raise ValueError
    return candidate

def _parse_provider_membership_observation(
    raw: object,
) -> TelegramMembershipObservation:
    value = _string_object_dict(raw)
    required = {
        "schema", "provider", "customer_user_id", "customer_private_chat_id",
        "owner_user_id", "scoped_staff_chat_ids", "customer_memberships",
        "observed_at", "complete", "receipt_digest",
    }
    if set(value) != required:
        raise ValueError
    receipt_digest = _required_string(value, "receipt_digest")
    unsigned = {
        key: item for key, item in value.items() if key != "receipt_digest"
    }
    scoped_raw = value["scoped_staff_chat_ids"]
    memberships_raw = value["customer_memberships"]
    complete = value["complete"]
    if (
        receipt_digest != canonical_observation_digest(unsigned)
        or not _is_object_list(scoped_raw)
        or not _is_object_list(memberships_raw)
        or type(complete) is not bool
    ):
        raise ValueError
    scoped_staff_chat_ids: list[int] = []
    for chat_id in scoped_raw:
        if type(chat_id) is not int:
            raise ValueError
        scoped_staff_chat_ids.append(chat_id)
    customer_memberships = [
        _membership_record(row) for row in memberships_raw
    ]
    return TelegramMembershipObservation(
        schema=_required_string(value, "schema"),
        provider=_required_string(value, "provider"),
        customer_user_id=_required_string(value, "customer_user_id"),
        customer_private_chat_id=_required_string(
            value, "customer_private_chat_id"
        ),
        owner_user_id=_required_string(value, "owner_user_id"),
        scoped_staff_chat_ids=scoped_staff_chat_ids,
        customer_memberships=customer_memberships,
        observed_at=_required_string(value, "observed_at"),
        complete=complete,
        receipt_digest=receipt_digest,
    )

def load_provider_membership_observation(
    path: Path | str,
) -> TelegramMembershipObservation:
    """Load one digest-verified provider receipt; omission is never absence."""
    source = Path(path)
    if source.is_symlink() or not source.is_file():
        raise ActivationReadinessError(
            "IDENTITY_AUTHORITY_STALE",
            "provide a complete Telegram provider membership receipt",
        )
    try:
        return _parse_provider_membership_observation(
            json.loads(source.read_text(encoding="utf-8"))
        )
    except (OSError, TypeError, ValueError, json.JSONDecodeError) as exc:
        raise ActivationReadinessError(
            "IDENTITY_AUTHORITY_STALE",
            "provide a complete Telegram provider membership receipt",
        ) from exc

def _reject(code: str, action: str) -> Never:
    raise ActivationReadinessError(code, action)

def _address_source(value: object) -> dict[str, object]:
    return {
        "user_id": getattr(value, "user_id", None),
        "chat_id": getattr(value, "chat_id", None),
        "topic_id": getattr(value, "topic_id", None),
    }

def _parse_consent_date(value: object) -> date:
    if type(value) is date:
        return value
    if isinstance(value, str):
        try:
            parsed = date.fromisoformat(value)
            if parsed.isoformat() == value:
                return parsed
        except ValueError:
            pass
    _reject("PRIVACY_CONSENT_STALE", "record current privacy-v1 customer consent")

def require_activation_readiness(
    session: object,
    customer: object,
    authority: ActivationReadinessAuthority,
    *,
    consent_version: str,
    now: datetime | None = None,
    require_disabled: bool = True,
    authority_generation: int | None = None,
) -> ActivationReadinessReceipt:
    """Require current consent and exact provider-observed isolated topology."""
    if require_disabled and getattr(customer, "enabled", None) is not False:
        _reject(
            "CUSTOMER_NOT_DISABLED",
            "disable the customer before running production activation readiness",
        )

    customer_source = _address_source(getattr(customer, "telegram", None))
    staff_source = {
        "user_id": authority.owner_user_id,
        "chat_id": authority.owner_chat_id,
        "topic_id": authority.owner_topic_id,
    }
    try:
        legacy = is_legacy_shared_topic_configuration(customer_source, (staff_source,))
    except AddressConfigurationError:
        legacy = False
    if legacy:
        _reject("LEGACY_SHARED_TOPIC", "migrate customer delivery to the customer Bot DM")
    try:
        customer_address = parse_customer_private_delivery_address(customer_source)
    except AddressConfigurationError:
        _reject("CUSTOMER_DM_REQUIRED", "configure the customer Bot DM")
    if customer_address.chat_id != customer_address.user_id:
        _reject(
            "CUSTOMER_DM_MISMATCH",
            "bind the customer Bot DM to the reviewed customer identity",
        )
    try:
        staff_address = parse_staff_review_address(StaffReviewRole.OWNER, staff_source)
    except AddressConfigurationError:
        _reject("STAFF_REVIEW_REQUIRED", "configure a trusted staff review destination")

    customer_claim = getattr(session, "role_claim")(Role.CUSTOMER)
    if (
        customer_claim is None
        or str(customer_address.user_id) != getattr(customer_claim, "user_id", None)
        or str(customer_address.user_id) != authority.customer_user_id
    ):
        _reject(
            "CUSTOMER_DM_MISMATCH",
            "bind the customer Bot DM to the reviewed customer identity",
        )
    if (
        str(staff_address.user_id) != str(getattr(session, "owner_id", None))
        or str(staff_address.user_id) != authority.owner_user_id
    ):
        _reject(
            "STAFF_REVIEW_MISMATCH",
            "bind staff review routing to the reviewed owner identity",
        )

    observed_now = now or datetime.now(timezone.utc)
    expected_generation = (
        getattr(session, "generation", None)
        if authority_generation is None
        else authority_generation
    )
    observation = authority.provider_observation
    try:
        observed_at = datetime.fromisoformat(observation.observed_at)
    except (TypeError, ValueError):
        observed_at = datetime.min
    scope = tuple(observation.scoped_staff_chat_ids)
    memberships = tuple(observation.customer_memberships)
    membership_chats = tuple(row.get("chat_id") for row in memberships)
    valid_statuses = {"member", "administrator", "creator", "restricted", "left", "kicked"}
    if (
        observation.schema != _OBSERVATION_SCHEMA
        or observation.provider != "telegram.getChatMember"
        or observation.complete is not True
        or observation.receipt_digest
        != canonical_observation_digest(_observation_unsigned(observation))
        or observation.customer_user_id != authority.customer_user_id
        or observation.customer_private_chat_id != authority.customer_user_id
        or observation.owner_user_id != authority.owner_user_id
        or scope != (staff_address.chat_id,)
        or membership_chats != scope
        or any(row.get("status") not in valid_statuses for row in memberships)
        or observed_now.tzinfo is None
        or observed_at.tzinfo is None
        or observed_at > observed_now
        or observed_now - observed_at > _MAX_AUTHORITY_AGE
        or authority.bootstrap_generation != expected_generation
        or authority.customer_key != getattr(session, "customer_key", None)
    ):
        _reject(
            "IDENTITY_AUTHORITY_STALE",
            "refresh Telegram identity and membership authority",
        )
    if any(
        row.get("status") in {"member", "administrator", "creator", "restricted"}
        for row in memberships
    ):
        _reject(
            "CUSTOMER_IN_STAFF_GROUP",
            "remove the customer from the staff review group",
        )

    consent = getattr(customer, "ai_processing_consent", None)
    recorded_on = _parse_consent_date(getattr(consent, "recorded_on", None))
    current_day = observed_now.date()
    if (
        getattr(consent, "granted", None) is not True
        or getattr(consent, "notice_version", None) != consent_version
        or authority.consent_notice_version != consent_version
        or authority.consent_recorded_on != recorded_on
        or authority.consent_bootstrap_generation != expected_generation
        or recorded_on > current_day
        or current_day - recorded_on > _MAX_CONSENT_AGE
    ):
        _reject(
            "PRIVACY_CONSENT_STALE",
            f"record current {consent_version} customer consent",
        )
    return ActivationReadinessReceipt(True, _CHECKS)

def activation_readiness_receipt_path(
    profile_root: Path | str,
    session_id: str,
) -> Path:
    name = hashlib.sha256(session_id.encode("utf-8")).hexdigest() + ".json"
    return Path(profile_root) / "data/onboarding/activation-readiness-v2" / name

def _customer_binding_digest(customer: object) -> str:
    consent = getattr(customer, "ai_processing_consent", None)
    value = {
        "customer_key": getattr(customer, "customer_key", None),
        "telegram": _address_source(getattr(customer, "telegram", None)),
        "consent": {
            "granted": getattr(consent, "granted", None),
            "recorded_on": str(getattr(consent, "recorded_on", None)),
            "notice_version": getattr(consent, "notice_version", None),
        },
    }
    return hashlib.sha256(_canonical_json(value)).hexdigest()

def persist_activation_readiness_receipt(
    profile_root: Path | str,
    session: object,
    customer: object,
    authority: ActivationReadinessAuthority,
    *,
    consent_version: str,
    now: datetime,
) -> None:
    require_activation_readiness(
        session, customer, authority, consent_version=consent_version, now=now
    )
    unsigned: dict[str, object] = {
        "schema": _RECEIPT_SCHEMA,
        "session_digest": hashlib.sha256(
            str(getattr(session, "session_id", "")).encode("utf-8")
        ).hexdigest(),
        "customer_key_digest": hashlib.sha256(
            str(getattr(session, "customer_key", "")).encode("utf-8")
        ).hexdigest(),
        "bootstrap_generation": authority.bootstrap_generation,
        "customer_binding_digest": _customer_binding_digest(customer),
        "owner_chat_id": authority.owner_chat_id,
        "owner_topic_id": authority.owner_topic_id,
        "provider_observation": {
            **_observation_unsigned(authority.provider_observation),
            "receipt_digest": authority.provider_observation.receipt_digest,
        },
        "consent_version": authority.consent_notice_version,
        "consent_recorded_on": authority.consent_recorded_on.isoformat(),
        "committed_at": now.isoformat(),
    }
    payload = {**unsigned, "payload_digest": hashlib.sha256(_canonical_json(unsigned)).hexdigest()}
    path = activation_readiness_receipt_path(
        profile_root, str(getattr(session, "session_id", ""))
    )
    path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
    path.parent.chmod(0o700)
    descriptor, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
    try:
        os.fchmod(descriptor, 0o600)
        with os.fdopen(descriptor, "wb", closefd=True) as stream:
            descriptor = -1
            stream.write(_canonical_json(payload) + b"\n")
            stream.flush()
            os.fsync(stream.fileno())
        os.replace(temporary, path)
        temporary = ""
        path.chmod(0o600)
    finally:
        if descriptor >= 0:
            os.close(descriptor)
        if temporary:
            os.unlink(temporary)

def _parse_persisted_readiness_receipt(
    raw: object,
) -> _PersistedReadinessReceipt:
    payload = _string_object_dict(raw)
    if set(payload) != {
        "schema", "session_digest", "customer_key_digest", "bootstrap_generation",
        "customer_binding_digest", "owner_chat_id", "owner_topic_id",
        "provider_observation", "consent_version",
        "consent_recorded_on", "committed_at", "payload_digest",
    }:
        raise ValueError
    generation = payload["bootstrap_generation"]
    if type(generation) is not int:
        raise ValueError
    digest = _required_string(payload, "payload_digest")
    unsigned = {
        key: item for key, item in payload.items() if key != "payload_digest"
    }
    if digest != hashlib.sha256(_canonical_json(unsigned)).hexdigest():
        raise ValueError
    return _PersistedReadinessReceipt(
        schema=_required_string(payload, "schema"),
        session_digest=_required_string(payload, "session_digest"),
        customer_key_digest=_required_string(payload, "customer_key_digest"),
        bootstrap_generation=generation,
        customer_binding_digest=_required_string(
            payload, "customer_binding_digest"
        ),
        owner_chat_id=_required_string(payload, "owner_chat_id"),
        owner_topic_id=_required_string(payload, "owner_topic_id"),
        provider_observation=payload["provider_observation"],
        consent_version=_required_string(payload, "consent_version"),
        consent_recorded_on=date.fromisoformat(
            _required_string(payload, "consent_recorded_on")
        ),
        committed_at=datetime.fromisoformat(
            _required_string(payload, "committed_at")
        ),
        payload_digest=digest,
    )

def validate_activation_readiness_receipt(
    profile_root: Path | str,
    session: object,
    customer: object,
    *,
    consent_version: str,
    now: datetime,
    expected_readiness_generation: int,
) -> None:
    path = activation_readiness_receipt_path(
        profile_root, str(getattr(session, "session_id", ""))
    )
    try:
        if path.is_symlink() or not path.is_file() or path.stat().st_mode & 0o077:
            raise ValueError
        raw: object = json.loads(path.read_text(encoding="utf-8"))
        payload = _parse_persisted_readiness_receipt(raw)
        generation = payload.bootstrap_generation
        committed_at = payload.committed_at
        consent_recorded_on = payload.consent_recorded_on
        if (
            payload.schema != _RECEIPT_SCHEMA
            or payload.session_digest != hashlib.sha256(
                str(getattr(session, "session_id", "")).encode("utf-8")
            ).hexdigest()
            or payload.customer_key_digest != hashlib.sha256(
                str(getattr(session, "customer_key", "")).encode("utf-8")
            ).hexdigest()
            or generation != expected_readiness_generation
            or payload.customer_binding_digest != _customer_binding_digest(customer)
            or payload.consent_version != consent_version
            or now.tzinfo is None
            or committed_at.tzinfo is None
            or committed_at > now
            or now - committed_at > _MAX_AUTHORITY_AGE
            or consent_recorded_on > now.date()
            or now.date() - consent_recorded_on > _MAX_CONSENT_AGE
        ):
            raise ValueError
        observation = _parse_provider_membership_observation(
            payload.provider_observation
        )
        require_activation_readiness(
            session,
            customer,
            ActivationReadinessAuthority(
                customer_key=str(getattr(session, "customer_key", "")),
                bootstrap_generation=generation,
                customer_user_id=observation.customer_user_id,
                owner_user_id=observation.owner_user_id,
                owner_chat_id=payload.owner_chat_id,
                owner_topic_id=payload.owner_topic_id,
                consent_notice_version=payload.consent_version,
                consent_recorded_on=consent_recorded_on,
                consent_bootstrap_generation=generation,
                provider_observation=observation,
            ),
            consent_version=consent_version,
            now=now,
            require_disabled=False,
            authority_generation=expected_readiness_generation,
        )
    except (
        ActivationReadinessError,
        OSError,
        TypeError,
        ValueError,
        json.JSONDecodeError,
    ) as exc:
        raise ActivationReadinessError(
            "READINESS_RECEIPT_UNAVAILABLE",
            "disable the customer and refresh production activation readiness",
        ) from exc

__all__ = [
    "ActivationReadinessAuthority",
    "ActivationReadinessError",
    "ActivationReadinessReceipt",
    "DurableActivationReadinessProof",
    "TelegramMembershipObservation",
    "TelegramMembershipRecord",
    "activation_readiness_receipt_path",
    "canonical_observation_digest",
    "load_provider_membership_observation",
    "persist_activation_readiness_receipt",
    "require_activation_readiness",
    "validate_activation_readiness_receipt",
]


{"bytes": 1952, "path": "data/customers/task22_dm_rehearsal/nutrition-onboarding/baseline-candidate.json", "sha256": "58146d67f9cf0d3da6bda7bae3ee2163ae6a4af44a33eda78325469b0ec99656"}
{"bytes": 1012, "customer_key": "task22_dm_rehearsal", "path": "data/customers/task22_dm_rehearsal/nutrition-onboarding/baseline-v1.json", "sha256": "a3b6b15e20a995654c653635e7bd9bb899bac377a9b8ea713e8a30a983f9ddbe"}
{"bytes": 46386, "path": "data/customers/task22_dm_rehearsal/nutrition-onboarding/migrations/owner-v1.json", "phase": "PUBLICATION_COMMITTED", "schema": "nutrition-owner-v1-migration-v2", "sha256": "e774b2b01edb90391389ea304b7d10f06f908302cf28aac39a57e3c52b289474"}
{"bytes": 14134, "customer_key": "task22_dm_rehearsal", "path": "data/customers/task22_dm_rehearsal/nutrition-onboarding/migrations/task22-callback-storage-adoption-v1.json", "schema": "task22-callback-storage-adoption-v1", "session_id": "rb_Ar-NuKG3LH8CqHPXUgc88g", "sha256": "fdf81e9d28d284fe343e09ad29e80bdd5913113faba7aec2a37b3a6a39749e09"}
{"bytes": 24504, "customer_key": "task22_dm_rehearsal", "path": "data/customers/task22_dm_rehearsal/nutrition-onboarding/migrations/task22-supplemental-owner-review-v1.json", "schema": "task22-supplemental-owner-review-v1", "session_id": "rb_Ar-NuKG3LH8CqHPXUgc88g", "sha256": "f5fc5eebff8970c72270e05b56f88baa83d69404ef4098bacae6ba2ef8af612e"}
{"bytes": 32771, "customer_key": "task22_dm_rehearsal", "path": "data/customers/task22_dm_rehearsal/nutrition-onboarding/migrations/trainer-authority-removal-v1/86d979a1d83dab0eab6ff71c0688d5c84cccfdd51b2843663c79c30fc4876eff/000-prepared.json", "phase": "PREPARED", "schema": "task22-trainer-authority-removal-event-v1", "session_id": "rb_Ar-NuKG3LH8CqHPXUgc88g", "sha256": "5ca6c01c1e5b9e73a16b6a92769293bce48514213964c8eceec4434d130695fb"}
{"bytes": 29202, "path": "data/customers/task22_dm_rehearsal/nutrition-onboarding/migrations/trainer-authority-removal-v1/86d979a1d83dab0eab6ff71c0688d5c84cccfdd51b2843663c79c30fc4876eff/001-committed.json", "phase": "COMMITTED", "schema": "task22-trainer-authority-removal-event-v1", "sha256": "47e6213a4fba725ba2f4c1d773a4f41261f7c6eafac89350071bb7502eb51e39"}
{"bytes": 775, "customer_key": "task22_dm_rehearsal", "path": "data/customers/task22_dm_rehearsal/nutrition-onboarding/ready.json", "sha256": "71a055a3b6da30d33608ef1b4b3feddd0490e5ef5ae737bf10440a0c89954771", "state": "ready"}
{"bytes": 964, "path": "data/onboarding/telegram-publication-outbox-v1/owner-callbacks.json", "schema": "telegram-nutrition-onboarding-owner-callback-v1", "sha256": "b2b4e23c98c2f982df4e18fa254f8520b4d5f47d2613831b36f927a398c47ced"}
{"bytes": 1757, "path": "data/onboarding/telegram-room-bootstrap-v1/ledger.json", "schema": "telegram-room-bootstrap-v1", "sessions": [], "sha256": "f3a3dd5b7aec01200f7e577a0367365717aef5b96a528de01c0116a81316ee6d"}
{"bytes": 6334, "path": "data/rehearsal-reset-archives/7c9675d2d050ef2bc4c478f414ea5071/payload/customers/registry.json", "sha256": "5e1a33e10a6419e97c05c4923f322d69e922068726ed8fb12431a83a7324c561"}
{"bytes": 1555, "path": "data/rehearsal-reset-archives/7c9675d2d050ef2bc4c478f414ea5071/payload/data/onboarding/telegram-room-bootstrap-v1/ledger.json", "schema": "telegram-room-bootstrap-v1", "sessions": [], "sha256": "24aef2ddfd61542d389cd66e93cb2099809b21059a8a4b8e03e4448adda69811"}
{"bytes": 973, "path": "data/rehearsal-reset-archives/7c9675d2d050ef2bc4c478f414ea5071/payload/data/onboarding/telegram-room-bootstrap-v1/role-invites.json", "schema": "telegram-room-bootstrap-role-invites-v1", "sha256": "71e46a0d454cfc531e8cd4738e671452bb0dee9efce83dc864e99d0259f838ea"}
provider_receipt_candidates
data/activation-completion-notices.lock
data/customers/task22_dm_rehearsal/nutrition-onboarding/readiness-current.json
data/customers/task22_dm_rehearsal/nutrition-onboarding/readiness-receipt-v1.json
data/gate-d-clean-g5-checklist.json
data/gate-d-delivery-g5-checklist.json
data/migrations/nutrition-readiness-v1/legacy-activation-authority.json
data/rehearsal-reset-archives/69949804ff39cdaf8d58228a70590e83/payload/data/activation-completion-notices.lock
data/rehearsal-reset-archives/7c9675d2d050ef2bc4c478f414ea5071/payload/data/activation-completion-notices.lock
data/rehearsal-reset-archives/9b1289330363d155ec14855690d6997c/payload/data/activation-completion-notices.jsonl
data/rehearsal-reset-archives/9b1289330363d155ec14855690d6997c/payload/data/customer-activation-audit.jsonl
data/rehearsal-reset-archives/9b1289330363d155ec14855690d6997c/payload/data/customer-activation-journal.json
data/rehearsal-reset-archives/9b1289330363d155ec14855690d6997c/payload/data/customers/mother_pilot/nutrition-onboarding/readiness-current.json
data/rehearsal-reset-archives/9b1289330363d155ec14855690d6997c/payload/data/customers/mother_pilot/nutrition-onboarding/readiness-receipt-v1.json
data/virtual-clean-test-g5-checklist.json
data/virtual-clean-test-v2-g5-checklist.json
data/virtual-customer-g5-checklist.json