#!/usr/bin/env python3
"""Owner CLI for transport-free Telegram nutrition-room bootstrap authority."""

from __future__ import annotations

import argparse
import asyncio
import json
import sys
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
from typing import TypeGuard, cast

import yaml

REPO_ROOT = Path(__file__).resolve().parents[1]
if str(REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(REPO_ROOT))

from gateway.platforms.telegram_activation_readiness import (  # noqa: E402
    ActivationReadinessAuthority,
    load_provider_membership_observation,
)
from gateway.platforms.telegram_room_bootstrap_cutover import (  # noqa: E402
    TelegramRoomBootstrapCutover,
)
from gateway.platforms.telegram_pending_marker_intake import (  # noqa: E402
    resolve_pending_marker_identities,
)
from gateway.platforms.telegram_room_bootstrap import (  # noqa: E402
    BootstrapError,
    CustomerDraft,
    RecoveryAttempt,
    RecoverySlot,
    RoomBootstrapSession,
    RoomBootstrapStore,
    load_customer_draft,
    room_bootstrap_state_dir,
)
from gateway.platforms.telegram_room_bootstrap_registration import (  # noqa: E402
    TelegramRoomBootstrapRegistration,
)

def _profile_root(args: argparse.Namespace) -> Path:
    profile = getattr(args, "profile_root", None)
    if profile is None:
        raise ValueError("--profile-root is required")
    root = Path(profile).expanduser()
    if root.is_symlink():
        raise ValueError("profile root symlinks are not allowed")
    resolved = root.resolve(strict=True)
    if not resolved.is_dir():
        raise ValueError("profile root must be a directory")
    return resolved

def _state_dir(args: argparse.Namespace) -> Path:
    explicit = getattr(args, "state_dir", None)
    if explicit is not None:
        return Path(explicit).expanduser()
    return room_bootstrap_state_dir(_profile_root(args))

def _add_authority_location(parser: argparse.ArgumentParser) -> None:
    location = parser.add_mutually_exclusive_group(required=True)
    location.add_argument("--state-dir")
    location.add_argument("--profile-root")

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 _profile_telegram_token(profile_root: Path) -> str:
    """Read only the selected profile's Telegram credential into local memory."""
    from agent.secret_scope import build_profile_secret_scope

    token = build_profile_secret_scope(profile_root).get("TELEGRAM_BOT_TOKEN", "").strip()
    if not token:
        raise ValueError("dedicated Telegram credential is unavailable")
    return token

def _rehearsal_marker_cutoff(profile_root: Path) -> datetime | None:
    """Use the latest supported reset receipt as the stale-update boundary."""
    if profile_root.name != "dualcoachtest":
        raise ValueError("pending-marker preparation is restricted to dualcoachtest")
    archives = profile_root / "data" / "rehearsal-reset-archives"
    if not archives.is_dir():
        return None
    receipts = tuple(archives.glob("*/receipt.json"))
    if not receipts:
        return None
    latest = max(receipts, key=lambda path: path.stat().st_mtime_ns)
    return datetime.fromtimestamp(latest.stat().st_mtime, tz=timezone.utc)

def _rehearsal_staff_authority(
    profile_root: Path,
    bot_username: str,
) -> tuple[str, str]:
    try:
        config = _string_object_dict(
            yaml.safe_load((profile_root / "config.yaml").read_text(encoding="utf-8"))
        )
        platforms = _string_object_dict(config["platforms"])
        telegram = _string_object_dict(platforms["telegram"])
        extra = _string_object_dict(telegram["extra"])
        adaptive = _string_object_dict(extra["adaptive_nutrition"])
        operator = _string_object_dict(adaptive["review_operator"])
        dedicated = _string_object_dict(adaptive["separate_bot"])
        owner_id = operator["user_id"]
        chat_id = operator["chat_id"]
        configured_bot = dedicated["bot_username"]
    except (KeyError, OSError, TypeError, ValueError, yaml.YAMLError) as exc:
        raise ValueError("dedicated rehearsal staff authority is unavailable") from exc
    if (
        not isinstance(owner_id, str)
        or not isinstance(chat_id, str)
        or not chat_id.startswith("-")
        or dedicated.get("dedicated") is not True
        or configured_bot != bot_username.removeprefix("@")
    ):
        raise ValueError("dedicated rehearsal staff authority is unavailable")
    return owner_id, chat_id

def _profile_owner_id(profile_root: Path) -> str:
    try:
        payload = _string_object_dict(
            json.loads(
                (profile_root / "customers" / "registry.json").read_text(
                    encoding="utf-8"
                )
            )
        )
        owner = _string_object_dict(payload["owner"])
        user_id = owner["user_id"]
    except (KeyError, OSError, TypeError, ValueError, json.JSONDecodeError) as exc:
        raise ValueError("canonical rehearsal owner is unavailable") from exc
    if not isinstance(user_id, str) or not user_id:
        raise ValueError("canonical rehearsal owner is unavailable")
    return user_id

def _activation_readiness_authority(
    args: argparse.Namespace,
) -> ActivationReadinessAuthority:
    """Bind a provider receipt to canonical identity and consent authority."""
    receipt_path = getattr(args, "provider_membership_receipt", None)
    if not receipt_path:
        raise ValueError("provider membership receipt is required")
    observation = load_provider_membership_observation(receipt_path)
    registry_path = Path(args.profile_root) / "customers" / "registry.json"
    try:
        raw: object = json.loads(registry_path.read_text(encoding="utf-8"))
        payload = _string_object_dict(raw)
        owner = _string_object_dict(payload["owner"])
        customers = payload["customers"]
        if not _is_object_list(customers):
            raise ValueError
        rows = [_string_object_dict(row) for row in customers]
        matches = [
            row
            for row in rows
            if row.get("customer_key") == args.customer_key
        ]
        if len(matches) != 1:
            raise ValueError
        consent = _string_object_dict(matches[0]["ai_processing_consent"])
        recorded_on = consent["recorded_on"]
        notice_version = consent["notice_version"]
        owner_user_id = owner["user_id"]
        owner_chat_id = owner["chat_id"]
        owner_topic_id = owner["topic_id"]
        if not all(
            isinstance(value, str)
            for value in (
                recorded_on,
                notice_version,
                owner_user_id,
                owner_chat_id,
                owner_topic_id,
            )
        ):
            raise ValueError
        consent_recorded_on = date.fromisoformat(str(recorded_on))
        consent_version = str(notice_version)
        owner_user_id = str(owner_user_id)
        owner_chat_id = str(owner_chat_id)
        owner_topic_id = str(owner_topic_id)
    except (KeyError, OSError, TypeError, ValueError, json.JSONDecodeError) as exc:
        raise ValueError(
            "canonical customer consent is unavailable for identity authority"
        ) from exc
    return ActivationReadinessAuthority(
        customer_key=args.customer_key,
        bootstrap_generation=args.generation,
        customer_user_id=args.customer_user_id,
        owner_user_id=owner_user_id,
        owner_chat_id=owner_chat_id,
        owner_topic_id=owner_topic_id,
        consent_notice_version=consent_version,
        consent_recorded_on=consent_recorded_on,
        consent_bootstrap_generation=args.generation,
        provider_observation=observation,
    )

def _session_output(session: RoomBootstrapSession) -> dict[str, object]:
    return {
        "session_id": session.session_id,
        "customer_key": session.customer_key,
        "customer_draft_digest": session.customer_draft_digest,
        "bot_username": session.bot_username,
        "state": session.state.value,
        "generation": session.generation,
        "created_at": session.created_at.isoformat(),
        "updated_at": session.updated_at.isoformat(),
        "expires_at": session.expires_at.isoformat(),
        "chat_id": session.chat_id,
        "anchor_message_id": session.anchor_message_id,
        "owner_id": session.owner_id,
        "bot_id": session.bot_id,
        "topics": {slot.value: topic for slot, topic in session.topics},
        "claim_cards": [
            {"role": card.role.value, "message_id": card.message_id}
            for card in session.claim_cards
        ],
        "role_claims": [
            {
                "role": claim.role.value,
                "user_id": claim.user_id,
                "chat_id": claim.chat_id,
                "topic_id": claim.topic_id,
                "message_id": claim.message_id,
            }
            for claim in session.role_claims
        ],
        "recovery_attempt_generation": session.recovery_attempt_generation,
        "recovery_attempts": [
            _attempt_output(attempt) for attempt in session.recovery_attempts
        ],
        "failure_code": session.failure_code,
    }

def _attempt_output(attempt: RecoveryAttempt) -> dict[str, object]:
    return {
        "session_id": attempt.session_id,
        "slot": attempt.slot.value,
        "chat_id": attempt.chat_id,
        "attempt_generation": attempt.generation,
        "created_at": attempt.created_at.isoformat(),
    }

def _add_recovery_authority(parser: argparse.ArgumentParser) -> None:
    _add_authority_location(parser)
    parser.add_argument("--session-id", required=True)
    parser.add_argument("--slot", required=True, choices=[slot.value for slot in RecoverySlot])
    parser.add_argument("--chat-id", required=True)
    parser.add_argument("--attempt-generation", required=True, type=int)

def _parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="Prepare and inspect durable Telegram nutrition-room bootstrap sessions."
    )
    commands = parser.add_subparsers(dest="command", required=True)

    prepare = commands.add_parser("prepare", help="create an unbound 30-minute bootstrap session")
    _add_authority_location(prepare)
    prepare.add_argument("--draft", required=True)
    prepare.add_argument("--bot-username", required=True)

    pending_prepare = commands.add_parser(
        "prepare-from-pending-markers",
        help="resolve the dedicated pending rehearsal markers and prepare one private-role session",
    )
    pending_prepare.add_argument("--profile-root", required=True)
    pending_prepare.add_argument("--draft", required=True)
    pending_prepare.add_argument("--bot-username", required=True)

    status = commands.add_parser("status", help="show one session or the complete ledger")
    _add_authority_location(status)

[190 more lines in file. Use offset=301 to continue.]


            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,

[353 more lines in file. Use offset=540 to continue.]


def parse_callback(value: object) -> ParsedCallback | None:
    if not isinstance(value, str) or len(value.encode("utf-8")) > 64:
        return None
    match = _CALLBACK_RE.fullmatch(value)
    if match is None:
        return None
    try:
        digest = base64.urlsafe_b64decode(match.group("token") + "=").hex()
        generation = int(match.group("generation"))
    except (ValueError, TypeError):
        return None
    if _DIGEST_RE.fullmatch(digest) is None:
        return None
    action = _CALLBACK_CODE_ACTIONS.get(match.group("code"))
    if action is None:
        return None
    return ParsedCallback(action, digest, generation)

def provisioning_calls(session: RoomBootstrapSession) -> tuple[TransportCall, ...]:
    """Return exact transport calls needed after a group has been bound.

    The caller executes these in order and persists returned topic/message IDs
    through :meth:`RoomBootstrapStore.set_topic` and related CAS methods.
    """
    if session.state is not BootstrapState.PROVISIONING or session.chat_id is None:
        raise BootstrapError("session is not ready for Telegram provisioning")
    return (
        TransportCall("get_chat", (("chat_id", session.chat_id),)),
        TransportCall("get_chat_member", (("chat_id", session.chat_id), ("user_id", "self"))),
        *(TransportCall("create_forum_topic", (("chat_id", session.chat_id), ("name", name))) for name in (
            "Daily check-in", "Coaching", "Trainer review", "Administration"
        )),
    )

class RoomBootstrapStore:
    """Private digest-verified ledger with generation-CAS mutations."""

    def __init__(self, state_dir: Path | str, *, now: Callable[[], datetime] | None = None) -> None:
        self.state_dir = Path(state_dir)
        self.ledger_path = self.state_dir / "ledger.json"
        self.lock_path = self.state_dir / "ledger.lock"
        self._now = now or (lambda: datetime.now(timezone.utc))
        self._prepare_paths()
        with self._locked():
            if not self.ledger_path.exists():
                self._write_unlocked(())
            else:
                self._read_unlocked()

    def prepare(self, draft: CustomerDraft, *, bot_username: str) -> PreparedRoomBootstrap:
        username = bot_username.removeprefix("@").strip()
        if _BOT_RE.fullmatch(username) is None:
            raise BootstrapError("invalid Telegram bot username")
        now = self._utc_now()
        sid = secrets.token_urlsafe(16)
        if _SID_RE.fullmatch(sid) is None:  # pragma: no cover - secrets contract guard
            raise RuntimeError("generated bootstrap SID has an invalid shape")
        sid_hash = _sha256(sid.encode("ascii"))
        session = RoomBootstrapSession(
            session_id="rb_" + secrets.token_urlsafe(16),
            sid_hash=sid_hash,
            customer_draft=draft,
            customer_draft_digest=draft.digest,
            bot_username=username,
            state=BootstrapState.PREPARED,
            generation=1,
            created_at=now,
            updated_at=now,
            expires_at=now + _UNBOUND_TTL,
        )
        with self._locked():
            sessions = list(self._read_unlocked())
            sessions = self._expire_unbound_values(sessions, now)
            if any(not item.terminal and item.customer_key == draft.customer_key for item in sessions):
                raise BootstrapConflict("customer already has a nonterminal bootstrap session")
            sessions.append(session)
            self._write_unlocked(sessions)
        return PreparedRoomBootstrap(sid, session, build_startgroup_link(username, sid))

    def prepare_rehearsal_customer_invite(
        self,
        draft: CustomerDraft,
        *,
        bot_username: str,
        owner_id: str,
    ) -> PreparedRehearsalCustomerInvite:
        """Atomically persist marker authority and emit one 30-minute DM link."""
        username = bot_username.removeprefix("@").strip()
        if _BOT_RE.fullmatch(username) is None:
            raise BootstrapError("invalid Telegram bot username")
        if any(
            value is not None
            for value in (
                draft.customer_user_id,
                draft.trainer_user_id,
                draft.trainer_chat_id,
                draft.trainer_topic_id,
            )
        ):
            raise BootstrapError("rehearsal draft must not predeclare Telegram identities")
        owner = _telegram_id(owner_id, "owner")
        now = self._utc_now()
        sid = secrets.token_urlsafe(16)
        if _SID_RE.fullmatch(sid) is None:  # pragma: no cover - secrets contract guard
            raise RuntimeError("generated bootstrap SID has an invalid shape")
        private_draft = draft
        session = RoomBootstrapSession(
            session_id="rb_" + secrets.token_urlsafe(16),
            sid_hash=_sha256(sid.encode("ascii")),
            customer_draft=private_draft,
            customer_draft_digest=private_draft.digest,
            bot_username=username,
            state=BootstrapState.PREPARED,
            generation=1,
            created_at=now,
            updated_at=now,
            expires_at=now + _UNBOUND_TTL,
            owner_id=owner,
        )
        with self._locked():
            sessions = list(self._read_unlocked())
            sessions = self._expire_unbound_values(sessions, now)
            if any(
                not item.terminal and item.customer_key == draft.customer_key
                for item in sessions
            ):
                raise BootstrapConflict(
                    "customer already has a nonterminal bootstrap session"
                )
            sessions.append(session)
            self._write_unlocked(sessions)
        return PreparedRehearsalCustomerInvite(
            session=session,
            customer_link=f"https://t.me/{username}?start=rc1_{sid}",
            expires_at=session.expires_at,
        )

    def rehearsal_customer_invite_session(
        self,
        token: str,
    ) -> RoomBootstrapSession:
        """Resolve an unconsumed dedicated link without mutating its authority."""
        match = (
            _REHEARSAL_CUSTOMER_TOKEN_RE.fullmatch(token)
            if isinstance(token, str)
            else None

[1762 more lines in file. Use offset=650 to continue.]


    def transition(
        self,
        session_id: str,
        *,
        expected_generation: int,
        target: BootstrapState,
        failure_code: str | None = None,
    ) -> RoomBootstrapSession:
        if target is BootstrapState.ACTIVE:
            raise BootstrapError(
                "ACTIVE requires committed activation receipt completion"
            )

        def mutate(session: RoomBootstrapSession, now: datetime) -> RoomBootstrapSession:
            allowed = _TRANSITIONS.get(session.state, frozenset())
            if target in {BootstrapState.CANCELLED, BootstrapState.FAILED} and not session.terminal:
                pass
            elif target not in allowed:
                raise BootstrapError(f"invalid bootstrap transition: {session.state.value} -> {target.value}")
            code = _optional_code(failure_code) if target is BootstrapState.FAILED else None
            registration_generation = session.registration_approval_generation
            if (
                session.state is BootstrapState.ROLE_REVIEW
                and target is BootstrapState.REGISTERING
            ):
                registration_generation = session.generation
            return replace(
                session,
                state=target,
                failure_code=code,
                registration_approval_generation=registration_generation,
                generation=session.generation + 1,
                updated_at=now,
            )

        return self._mutate(session_id, expected_generation, mutate)

    def _complete_committed_activation(
        self,
        session_id: str,
        *,
        expected_generation: int,
    ) -> RoomBootstrapSession:
        """Private ledger edge used only by the canonical activation boundary."""

        def mutate(
            session: RoomBootstrapSession,
            now: datetime,
        ) -> RoomBootstrapSession:
            if session.state is not BootstrapState.AWAITING_ACTIVATION:
                raise BootstrapError(
                    "session is not awaiting committed activation completion"
                )
            return replace(
                session,
                state=BootstrapState.ACTIVE,
                failure_code=None,
                generation=session.generation + 1,
                updated_at=now,
            )

        return self._mutate(session_id, expected_generation, mutate)

    def cancel(self, session_id: str, *, expected_generation: int) -> RoomBootstrapSession:
        return self.transition(session_id, expected_generation=expected_generation, target=BootstrapState.CANCELLED)

    def mark_recoverable_failure(
        self,
        session_id: str,
        *,
        expected_generation: int,
        failure_code: str,
    ) -> RoomBootstrapSession:
        """Persist a fail-closed external outcome without discarding recovery state."""
        code = _optional_code(failure_code)

        def mutate(
            session: RoomBootstrapSession,
            now: datetime,
        ) -> RoomBootstrapSession:
            if session.terminal:
                raise BootstrapError("terminal bootstrap cannot record a recovery failure")
            return replace(
                session,
                failure_code=code,
                generation=session.generation + 1,
                updated_at=now,
            )

        return self._mutate(session_id, expected_generation, mutate)

    def unresolved_attempts(
        self,
        session_id: str | None = None,
    ) -> tuple[RecoveryAttempt, ...]:
        """Return typed unresolved provider authorities for operator inspection."""
        with self._locked():
            sessions = self._read_unlocked()
            if session_id is not None:
                sessions = (self._find(sessions, session_id),)
            return tuple(
                attempt
                for session in sessions
                for attempt in session.recovery_attempts
            )

    def reserve_recovery_attempt(
        self,
        session_id: str,

[1342 more lines in file. Use offset=1070 to continue.]


    def bind_consent_card(
        self,
        session_id: str,
        *,
        message_id: str,
        expected_generation: int,
        publication_attempt: int,
    ) -> RoomBootstrapSession:
        """Bind the exact provider receipt for the current publication attempt."""
        message = _telegram_id(message_id, "message")

        def mutate(session: RoomBootstrapSession, now: datetime) -> RoomBootstrapSession:
            if (
                session.state is not BootstrapState.AWAITING_CONSENT
                or type(publication_attempt) is not int
                or publication_attempt != session.consent_publication_attempt
                or publication_attempt < 1
            ):
                raise GenerationConflict("consent publication attempt is stale")
            existing = session.consent_card_message_id
            if existing is not None and existing != message:
                raise BootstrapConflict("consent card receipt is already bound")
            if existing == message:
                return session
            return replace(session, consent_card_message_id=message, updated_at=now)

        return self._mutate(session_id, expected_generation, mutate)

    def reconcile_committed_consent(
        self,
        session_id: str,
        *,
        expected_generation: int,
        publication_attempt: int,
        consent_card_message_id: str,
    ) -> RoomBootstrapSession:
        """Retire one exact card after current canonical consent is proven."""
        message = _telegram_id(consent_card_message_id, "message")

        def mutate(session: RoomBootstrapSession, now: datetime) -> RoomBootstrapSession:
            if (
                session.state is not BootstrapState.AWAITING_CONSENT
                or session.recovery_attempts
                or type(publication_attempt) is not int
                or publication_attempt != session.consent_publication_attempt
                or session.consent_card_message_id != message
            ):
                raise GenerationConflict("committed consent authority is stale or inexact")
            return replace(
                session,
                state=BootstrapState.AWAITING_ACTIVATION,
                consent_card_message_id=None,
                failure_code=None,
                generation=session.generation + 1,
                updated_at=now,
            )

        return self._mutate(session_id, expected_generation, mutate)

    def claim_role(
        self,
        session_id: str,
        *,
        role: Role,
        user_id: str,
        chat_id: str,
        topic_id: str,
        message_id: str,
        expected_generation: int,
    ) -> RoomBootstrapSession:
        user = _telegram_id(user_id, "user")
        chat = _telegram_id(chat_id, "chat")
        topic = _telegram_id(topic_id, "topic")
        message = _telegram_id(message_id, "message")
        def mutate(session: RoomBootstrapSession, now: datetime) -> RoomBootstrapSession:
            expected_topic = session.topic_id(_slot_for_role(role))
            expected_message = session.claim_card_message_id(role)
            if (
                session.state is not BootstrapState.CLAIMING
                or session.chat_id != chat
                or expected_topic != topic
                or expected_message != message
            ):
                raise BootstrapError("role claim is not on the exact claim-card route")
            if session.owner_id == user:
                raise BootstrapConflict("room owner cannot claim a coaching role")
            claims = {claim.role: claim for claim in session.role_claims}
            if role in claims:
                raise GenerationConflict("role claim capability is stale")
            if any(claim.user_id == user for claim in claims.values()):
                raise BootstrapConflict("one Telegram user cannot claim both roles")
            claims[role] = RoleClaim(role, user, chat, topic, message)
            ordered = tuple(sorted(claims.values(), key=lambda claim: claim.role.value))
            return replace(session, role_claims=ordered, updated_at=now)
        return self._mutate(session_id, expected_generation, mutate)

    def expire_unbound(self) -> tuple[RoomBootstrapSession, ...]:
        now = self._utc_now()
        with self._locked():

[912 more lines in file. Use offset=1500 to continue.]


from __future__ import annotations

import logging
from typing import Any

from gateway.platforms.telegram_nutrition_onboarding import (
    OPTIONAL_QUESTION_DEFAULTS,
    log_ingress_stage,
)
from gateway.platforms.telegram_nutrition_onboarding_copy import parse_answer
from gateway.platforms.telegram_room_bootstrap import BootstrapState

logger = logging.getLogger(__name__)

def is_consent_withdrawal(text: str | None) -> bool:
    return (
        isinstance(text, str)
        and " ".join(text.split()) == "서비스 중단 및 동의 철회 요청"
    )

class TelegramNutritionOnboardingRuntimeCollectionMixin:
    def _service(self, customer_key: str) -> Any:
        raise NotImplementedError

    def _session_for_customer_message(self, message: Any) -> Any | None:
        raise NotImplementedError

    def _current_authority(self, session: Any) -> Any:
        raise NotImplementedError

    def _evidence(
        self,
        *,
        actor_id: int | str | None,
        message: Any,
        update_id: int | str | None,
    ) -> Any:
        raise NotImplementedError

    async def _publish(
        self,
        session: Any,
        service: Any,
        status: Any,
        *,
        reply_anchor_message_id: int | None = None,
    ) -> None:
        raise NotImplementedError

    async def handle_text(self, update: Any, message: Any) -> bool:
        if is_consent_withdrawal(getattr(message, "text", None)):
            return False
        session = self._session_for_customer_message(message)
        if session is None:
            return False
        update_id = getattr(update, "update_id", None)
        log_ingress_stage("ingress", update_id, logger=logger)
        if session.state is not BootstrapState.AWAITING_ACTIVATION:
            log_ingress_stage(
                "authority",
                update_id,
                reason_code="onboarding_state_unavailable",
                logger=logger,
            )
            return False
        service = self._service(session.customer_key)
        try:
            authority = self._current_authority(session)
        except (OSError, ValueError):
            log_ingress_stage(
                "authority",
                update_id,
                reason_code="authority_unavailable",
                logger=logger,
            )
            await message.reply_text(
                "온보딩 권한 또는 동의가 변경됐습니다."
            )
            return True
        log_ingress_stage("authority", update_id, logger=logger)
        log_ingress_stage("route", update_id, logger=logger)
        status = service.status()
        try:
            publication = service.store.load_session(session.session_id)
        except ValueError:
            log_ingress_stage(
                "validation",
                update_id,
                reason_code="onboarding_session_unavailable",
                logger=logger,
            )
            return True
        reply = getattr(message, "reply_to_message", None)
        actor_id = getattr(
            getattr(message, "from_user", None),
            "id",
            None,
        )
        reply_message_id = getattr(reply, "message_id", None)
        if reply is not None and str(
            getattr(reply, "message_id", "")
        ) != str(publication.message_id):
            log_ingress_stage(
                "validation",
                update_id,
                reason_code="stale_reply",
                logger=logger,
            )
            return True
        if reply is None and status.next_field is None:
            log_ingress_stage(
                "validation",
                update_id,
                reason_code="no_active_question",
                logger=logger,
            )
            return True
        evidence = self._evidence(
            actor_id=getattr(
                getattr(message, "from_user", None),
                "id",
                None,
            ),
            message=message,
            update_id=getattr(update, "update_id", None),
        )
        replayed_status_reader = getattr(
            self,
            "replayed_status_if_consumed",
            None,
        )
        replayed_status = (
            replayed_status_reader(service, update_id)
            if callable(replayed_status_reader)
            else None
        )
        if replayed_status is not None:
            log_ingress_stage(
                "persistence",
                update_id,
                reason_code="duplicate_business_replay",
                logger=logger,
            )
            log_ingress_stage("publication", update_id, logger=logger)
            try:
                await self._publish(
                    session,
                    service,
                    replayed_status,
                    reply_anchor_message_id=int(message.message_id),
                )
            except (OSError, RuntimeError, TypeError, ValueError):
                log_ingress_stage(
                    "publication",
                    update_id,
                    reason_code="publication_failed",
                    logger=logger,
                )
                raise
            log_ingress_stage("receipt", update_id, logger=logger)
            return True
        log_ingress_stage("validation", update_id, logger=logger)
        if status.next_field is None:
            payload = publication.payload
            field = payload.get("clarification_field")
            answers_digest = payload.get("answers_digest")
            if (
                status.state.value != "customer_attestation"
                or not isinstance(field, str)
                or not isinstance(answers_digest, str)
            ):
                log_ingress_stage(
                    "validation",
                    update_id,
                    reason_code="reconciliation_state_invalid",
                    logger=logger,
                )

[109 more lines in file. Use offset=181 to continue.]


"""Private, profile-gated bridge for the Telegram physique check-in wizard.

This module deliberately has no import-time dependency on the profile-local
wizard package.  A normal Hermes profile therefore cannot accidentally enable
or load the check-in implementation merely by importing the Telegram adapter.
"""

from __future__ import annotations

import re
import sys
import time
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Final, Protocol, cast
from zoneinfo import ZoneInfo

from gateway.platforms.physique_checkin_bindings import BindingStore, WizardBinding
from gateway.platforms.physique_checkin_config import PhysiqueCheckinConfig
from gateway.platforms.physique_checkin_prompts import WizardPrompt, build_wizard_prompt

_SESSION_ID: Final[re.Pattern[str]] = re.compile(r"^[a-f0-9]{32}$")
_CALLBACK_STEP_CODES: Final[dict[str, str]] = {
    "weight_change_percent": "wc",
    "fatigue": "ft",
    "exercise_feasibility": "ef",
    "meal_deviation": "md",
}
_CALLBACK_CODE_STEPS: Final[dict[str, str]] = {
    code: step for step, code in _CALLBACK_STEP_CODES.items()
}
_STEP: Final[re.Pattern[str]] = re.compile(
    r"^(?:launch|done|bodyweight|sleep_duration|sleep_quality|condition|pain|calories|macros|"
    r"meals|water|digestion|appetite_stress|training_plan|weight_change_percent|fatigue|"
    r"exercise_feasibility|meal_deviation|wc|ft|ef|md|optional_note|summary|edit_menu|"
    r"completion|training_summary|workout_quality|performance|intensity|operator_note|safety_ack|"
    r"Q-SLEEP-CAUSE|Q-SLEEP-ADJUST|Q-COND-SYMPTOM|Q-COND-INTENSITY|Q-PERF-REASON|Q-PERF-NEXT)$"
)
_ACTION: Final[re.Pattern[str]] = re.compile(
    r"^(?:start|a[0-8]|e(?:[0-9]|1[0-2])|p|k|l)$"
)
_ADAPTIVE_STEPS: Final[frozenset[str]] = frozenset({
    "Q-SLEEP-CAUSE",
    "Q-SLEEP-ADJUST",
    "Q-COND-SYMPTOM",
    "Q-COND-INTENSITY",
    "Q-PERF-REASON",
    "Q-PERF-NEXT",
})
_ADAPTIVE_CHOICES: Final[dict[str, tuple[tuple[str, str], ...]]] = {
    "Q-SLEEP-CAUSE": (("카페인", "caffeine"), ("야근", "overtime"), ("스트레스", "stress"), ("기타", "other")),
    "Q-SLEEP-ADJUST": (("예", "yes"), ("아니오", "no")),
    "Q-COND-SYMPTOM": (("피로", "fatigue"), ("근육통", "muscle_soreness"), ("감기 기운", "cold"), ("기타", "other")),
    "Q-COND-INTENSITY": (("유지", "maintain"), ("하향", "reduce"), ("휴식", "rest")),
    "Q-PERF-REASON": (("시간 부족", "time_shortage"), ("컨디션", "condition"), ("통증", "pain"), ("기타", "other")),
    "Q-PERF-NEXT": (("예", "yes"), ("아니오", "no")),
}
_REGISTERED_MORNING_CHOICES: Final[dict[str, tuple[tuple[str, str], ...]]] = {
    "fatigue": (("낮음", "low"), ("보통", "moderate"), ("높음", "high")),
    "exercise_feasibility": (
        ("가능", "possible"),
        ("제한적", "limited"),
        ("불가능", "impossible"),
    ),
    "meal_deviation": (("없음", "none"), ("일부", "partial"), ("큼", "material")),
}
_ADAPTIVE_PROMPT_TEXT: Final[dict[str, str]] = {
    "Q-SLEEP-CAUSE": "수면이 짧거나 질이 낮았던 가장 가까운 이유를 골라주세요.",
    "Q-SLEEP-ADJUST": "오늘 수면을 위해 조정 가능한 행동을 받아들일까요.",
    "Q-COND-SYMPTOM": "오늘 컨디션 저하와 가장 가까운 상태를 골라주세요.",
    "Q-COND-INTENSITY": "오늘 훈련 강도를 어떻게 조정할까요.",
    "Q-PERF-REASON": "수행이 계획보다 낮았던 가장 가까운 이유를 골라주세요.",
    "Q-PERF-NEXT": "다음 세션에서 조정을 적용할까요.",
}
_URGENT_TEXT: Final[re.Pattern[str]] = re.compile(
    r"흉통|가슴\s*통증|실신|호흡\s*곤란|chest\s*pain",
    re.IGNORECASE,
)
_KST: Final[ZoneInfo] = ZoneInfo("Asia/Seoul")

@dataclass(frozen=True, slots=True)
class CallbackData:
    """A Telegram-safe opaque callback address, never a health-data carrier."""

    session_id: str
    step: str
    version: int
    action: str

    def encode(self) -> str:
        step = _CALLBACK_STEP_CODES.get(self.step, self.step)
        value = f"pc1:{self.session_id}:{step}:{self.version}:{self.action}"
        if len(value.encode("utf-8")) > 64:
            raise ValueError("callback exceeds Telegram's 64-byte limit")
        return value

    @classmethod
    def parse(cls, value: str) -> CallbackData | None:
        if len(value.encode("utf-8")) > 64:
            return None
        parts = value.split(":")
        if len(parts) != 5 or parts[0] != "pc1":
            return None
        _, session_id, step, version_text, action = parts
        if not _SESSION_ID.fullmatch(session_id) or not _STEP.fullmatch(step) or not _ACTION.fullmatch(action):
            return None
        try:
            version = int(version_text)
        except ValueError:
            return None
        if version < 0 or version > 1_000_000:
            return None
        parsed = cls(session_id, _CALLBACK_CODE_STEPS.get(step, step), version, action)
        try:
            parsed.encode()
        except ValueError:
            return None
        return parsed

@dataclass(frozen=True, slots=True)
class WizardReply:
    """Adapter result; deliberately excludes typed health/training content."""

    handled: bool
    accepted: bool
    notice: str
    prompt: WizardPrompt | None
    callback_data: str | None

class _WizardService(Protocol):
    def start_morning(self, context: object, kst_day: str) -> object: ...
    def start_workout(self, context: object, kst_day: str) -> object: ...
    def start_nutrition(self, context: object, kst_day: str) -> object: ...
    def start_nutrition_correction(self, context: object, kst_day: str) -> object: ...
    def start_trainer_session(self, context: object, kst_day: str) -> object: ...
    def start_schedule_reference(self, context: object, kst_day: str) -> object: ...
    def answer(self, context: object, session_id: str, expected_version: int, action: str, value: str | None = None) -> object: ...

@dataclass(frozen=True, slots=True)
class _WizardContext:
    """The profile wizard needs the auth boundary and optional customer scope."""

    owner_id: str
    topic_id: str
    customer_key: str | None = None

class PhysiqueCheckinBridge:
    """Validate exact Telegram boundaries before delegating to profile state."""

    def __init__(
        self,
        config: PhysiqueCheckinConfig,
        service: object | None = None,
        binding_path: Path | None = None,
        *,
        customer_key: str | None = None,
    ) -> None:
        self.config = config
        scope = customer_key.strip() if isinstance(customer_key, str) else ""
        self.customer_key = scope if 1 <= len(scope) <= 64 else None
        self._service = self._load_profile_service() if service is None else cast(_WizardService, service)
        self._binding_store = BindingStore(binding_path or self._default_binding_path())
        self._bindings, self._active_session_id = self._binding_store.load(
            config.owner_id, config.chat_id, config.topic_id, self._now(None),
        )

    def open_launcher(self, flow: str, *, message_id: str = "", now_epoch: int | None = None) -> WizardReply:
        """Create/resume a draft; caller binds the actual sent card message id."""
        if flow in {"trainer_session", "schedule_reference"} and self.customer_key is None:
            return WizardReply(True, False, "트레이너 기록 고객 범위를 확인할 수 없습니다.", None, None)
        now = self._now(now_epoch)
        context = self._context()
        day = datetime.now(_KST).date().isoformat()
        if flow == "morning":
            result = self._service.start_morning(context, day)
        elif flow == "nutrition_daily":
            result = self._service.start_nutrition(context, day)
        elif flow == "trainer_session":
            result = self._service.start_trainer_session(context, day)
        elif flow == "schedule_reference":
            result = self._service.start_schedule_reference(context, day)
        else:
            result = self._service.start_workout(context, day)
        session_id, version = self._result_identity(result)
        self._bindings[session_id] = WizardBinding(
            session_id, self.config.owner_id, self.config.chat_id, self.config.topic_id,
            "launch", version, str(message_id), now + self.config.expires_seconds,
        )
        self._persist()
        callback = CallbackData(session_id, "launch", version, "start").encode()
        return WizardReply(True, True, "", None, callback)

    def open_trainer_launcher(self, *, message_id: str = "", now_epoch: int | None = None) -> WizardReply:
        """Open the bounded trainer session entry card."""
        return self.open_launcher("trainer_session", message_id=message_id, now_epoch=now_epoch)
    def open_schedule_reference_launcher(
        self, *, message_id: str = "", now_epoch: int | None = None
    ) -> WizardReply:
        """Open the trainer-scoped schedule source-fact entry card."""
        return self.open_launcher("schedule_reference", message_id=message_id, now_epoch=now_epoch)

    def open_nutrition_correction(self, *, now_epoch: int | None = None) -> WizardReply:
        now = self._now(now_epoch)
        day = datetime.now(_KST).date().isoformat()
        result = self._service.start_nutrition_correction(self._context(), day)
        session_id, version = self._result_identity(result)
        if _SESSION_ID.fullmatch(session_id) is None:
            return WizardReply(True, False, "수정할 오늘 체크인이 없습니다.", None, None)
        step = str(getattr(result, "step", "bodyweight"))
        binding = WizardBinding(
            session_id, self.config.owner_id, self.config.chat_id, self.config.topic_id,
            step, version, "", now + self.config.expires_seconds,
            awaiting_text=self._expects_text(step),
        )
        self._bindings[session_id] = binding
        self._active_session_id = session_id
        self._persist()
        return WizardReply(True, True, "기존 기록은 보존하고 수정 기록을 새로 시작합니다.", self._prompt(binding), None)

    def open_trainer_correction(self, *, now_epoch: int | None = None) -> WizardReply:
        """Open an editable correction while preserving the saved trainer event."""
        starter = getattr(self._service, "start_trainer_session_correction", None)
        if not callable(starter):
            return WizardReply(True, False, "저장된 기록의 수정 기능을 사용할 수 없습니다.", None, None)
        now = self._now(now_epoch)
        day = datetime.now(_KST).date().isoformat()
        result = starter(self._context(), day)
        session_id, version = self._result_identity(result)
        if _SESSION_ID.fullmatch(session_id) is None:
            return WizardReply(True, False, "수정할 오늘 트레이너 기록이 없습니다.", None, None)
        step = str(getattr(result, "step", "summary"))
        binding = WizardBinding(
            session_id, self.config.owner_id, self.config.chat_id, self.config.topic_id,
            step, version, "", now + self.config.expires_seconds,
            awaiting_text=self._expects_text(step),
        )
        self._bindings[session_id] = binding
        self._active_session_id = session_id
        self._persist()
        return WizardReply(
            True,
            True,
            "기존 기록은 보존하고 수정본을 새로 만듭니다.",
            self._prompt(binding),
            None,
        )

    def active_is_finalized(self) -> bool:
        binding = self._bindings.get(self._active_session_id or "")
        storage = getattr(self._service, "_storage", None)
        session = storage.load(binding.session_id) if binding is not None and storage is not None else None
        return session is not None and getattr(session, "finalized_event_id", None) is not None

    def bind_launcher_message(self, session_id: str, message_id: str) -> bool:
        binding = self._bindings.get(session_id)
        if binding is None or not message_id:
            return False
        binding.message_id = str(message_id)
        self._persist()
        return True

    def bind_prompt_message(self, session_id: str, message_id: str) -> bool:
        return self.bind_launcher_message(session_id, message_id)

    def bind_active_prompt_message(self, message_id: str) -> bool:
        """Bind a follow-up typed-answer prompt to the same private session."""
        binding = self._bindings.get(self._active_session_id or "")
        if binding is None or not message_id:
            return False
        binding.message_id = str(message_id)
        self._persist()
        return True
    def has_binding(self, session_id: object) -> bool:

[872 more lines in file. Use offset=281 to continue.]


"""
Telegram platform adapter.

Uses python-telegram-bot library for:
- Receiving messages from users/groups
- Sending responses back
- Handling media and commands
"""

import asyncio
from contextlib import nullcontext
import hashlib
import dataclasses
import inspect
import json
import logging
import os
import secrets
import tempfile
import html as _html
import re
from types import SimpleNamespace
from datetime import date, datetime, time, timedelta, timezone
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Any, Callable, Mapping, Protocol, TypeGuard, TypedDict, cast, runtime_checkable
from zoneinfo import ZoneInfo

logger = logging.getLogger(__name__)

if TYPE_CHECKING:
    from telegram import CallbackQuery

    from gateway.platforms.telegram_room_bootstrap import (
        RoomBootstrapSession,
        RoomBootstrapStore,
        RoomBootstrapTransport,
    )

try:
    from telegram import Update, Bot, Message, InlineKeyboardButton, InlineKeyboardMarkup
    import telegram as _telegram

    ReplyKeyboardMarkup = getattr(_telegram, "ReplyKeyboardMarkup", Any)
    KeyboardButton = getattr(_telegram, "KeyboardButton", Any)
    ForceReply = getattr(_telegram, "ForceReply", Any)
    ReplyParameters = getattr(_telegram, "ReplyParameters", Any)
    LinkPreviewOptions = getattr(_telegram, "LinkPreviewOptions", None)
    from telegram.ext import (
        Application,
        CommandHandler,
        CallbackQueryHandler,
        MessageHandler as TelegramMessageHandler,
        ContextTypes,
        filters,
    )
    import telegram.ext as _telegram_ext

    ChatMemberHandler = getattr(_telegram_ext, "ChatMemberHandler", Any)
    TypeHandler = getattr(_telegram_ext, "TypeHandler", Any)
    ApplicationHandlerStop = getattr(
        _telegram_ext, "ApplicationHandlerStop", RuntimeError
    )
    from telegram.constants import ParseMode, ChatType
    from telegram.request import HTTPXRequest
    TELEGRAM_AVAILABLE = True
except ImportError:
    TELEGRAM_AVAILABLE = False
    Update = Any
    Bot = Any
    Message = Any
    InlineKeyboardButton = Any
    InlineKeyboardMarkup = Any
    ReplyKeyboardMarkup = Any
    KeyboardButton = Any
    ForceReply = Any
    ReplyParameters = Any
    LinkPreviewOptions = None
    Application = Any
    ChatMemberHandler = Any
    CommandHandler = Any
    CallbackQueryHandler = Any
    TelegramMessageHandler = Any
    TypeHandler = Any
    ApplicationHandlerStop = RuntimeError
    HTTPXRequest = Any
    filters = None
    ParseMode = None
    ChatType = None

    # Mock ContextTypes so type annotations using ContextTypes.DEFAULT_TYPE
    # don't crash during class definition when the library isn't installed.
    class _MockContextTypes:
        DEFAULT_TYPE = Any

    ContextTypes = _MockContextTypes

_ROOM_BOOTSTRAP_SID_RE = re.compile(r"^[A-Za-z0-9_-]{22}$")
_ROOM_BOOTSTRAP_START_RE = re.compile(
    r"^/start(?:@[A-Za-z0-9_]+)?\s+rb1_([A-Za-z0-9_-]{22})$"
)
_ROOM_ROLE_START_RE = re.compile(
    r"^/start(?:@[A-Za-z0-9_]+)?\s+(ri1_[A-Za-z0-9_-]{32})$"
)
_REHEARSAL_CUSTOMER_START_RE = re.compile(
    r"^/start(?:@[A-Za-z0-9_]+)?\s+(rc1_[A-Za-z0-9_-]{22})$"
)
_ROOM_BOOTSTRAP_CALLBACK_RE = re.compile(
    r"^rb1:([A-Za-z0-9_-]{22}):(customer|trainer):([0-9]+)$"
)
def room_bootstrap_link(username: object, sid: object) -> str:
    """Return Telegram's add-to-group link for one opaque bootstrap session."""
    clean_username = str(username or "").strip().lstrip("@")
    clean_sid = str(sid or "").strip()
    if not re.fullmatch(r"[A-Za-z0-9_]{5,32}", clean_username):
        raise ValueError("Telegram bot username is invalid")
    if _ROOM_BOOTSTRAP_SID_RE.fullmatch(clean_sid) is None:
        raise ValueError("room bootstrap session id is invalid")
    return f"https://t.me/{clean_username}?startgroup=rb1_{clean_sid}"

import sys
from pathlib import Path as _Path
sys.path.insert(0, str(_Path(__file__).resolve().parents[2]))

from gateway.config import Platform, PlatformConfig
from gateway.platforms.base import (
    BasePlatformAdapter,
    MessageEvent,
    MessageType,
    ProcessingOutcome,
    SendResult,
    cache_image_from_bytes,
    cache_audio_from_bytes,
    cache_video_from_bytes,
    cache_document_from_bytes,
    resolve_proxy_url,
    SUPPORTED_VIDEO_TYPES,
    SUPPORTED_DOCUMENT_TYPES,
    SUPPORTED_IMAGE_DOCUMENT_TYPES,
    utf16_len,
)
from gateway.platforms.telegram_network import (
    TelegramFallbackTransport,
    discover_fallback_ips,
    parse_fallback_ip_env,
)
from gateway.platforms.physique_checkin import (
    CallbackData,
    PhysiqueCheckinBridge,
    PhysiqueCheckinConfig,
    WizardPrompt,

[15600 more lines in file. Use offset=151 to continue.]


ledger_sha256=e3271199c1cb38476c44c55fe5318f5543a4152b4717033677e0c1c65f4971da
ledger_stat=inode=33165847 mode=600 size=1745 mtime=2026-08-12 19:23:52.354554527 +0900 ctime=2026-08-12 19:23:52.359912856 +0900
lock_stat=inode=33166501 mode=600 size=0 mtime=2026-08-10 14:25:48.159732895 +0900 ctime=2026-08-12 19:23:52.360912863 +0900
service=MainPID=675692
ExecMainStartTimestamp=Wed 2026-08-12 18:36:04 KST
ActiveState=active
SubState=running
ledger_session={"anchor_message_id":null,"chat_id":null,"created_at":"2026-08-10T06:11:05.116858+00:00","customer_key":null,"expires_at":"2026-08-10T07:29:02.306006+00:00","failure_code":null,"generation":8,"recovery_attempt_generation":1,"recovery_attempts":[],"role_claims":[{"chat_id":"8527916639","message_id":"71","role":"customer","topic_id":"0","user_id":"8527916639"},{"chat_id":"-1004484458766","message_id":"9","role":"trainer","topic_id":"0","user_id":"6949941558"}],"session_id":"rb_Ar-NuKG3LH8CqHPXUgc88g","state":"EXPIRED","updated_at":"2026-08-12T10:23:52.354302+00:00"}