#!/usr/bin/env python3
"""Offline reproduction of Telegram update 629525116 against live read-only state."""
from __future__ import annotations

import asyncio
import contextlib
import hashlib
import importlib
import json
import os
from pathlib import Path
import socket
import sys
import traceback
from types import MethodType, SimpleNamespace
from typing import Any

import yaml

SOURCE = Path("/home/cube/projects/richard/hermes-agent")
PROFILE = Path("/home/cube/.hermes/profiles/dualcoachtest")
CUSTOMER_ID = "8527916639"
UPDATE_ID = 629525116
TEXT = "checkin"
sys.path.insert(0, str(SOURCE))
os.environ["HERMES_HOME"] = str(PROFILE)


def digest(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def authority_snapshot() -> dict[str, str]:
    roots = [
        PROFILE / "config.yaml",
        PROFILE / "customers",
        PROFILE / "data/customer-activation-audit.jsonl",
        PROFILE / "data/customer-activation-journal.json",
        PROFILE / "data/nutrition-onboarding-projection-journal.jsonl",
        PROFILE / "data/customers/task26_live_2e_r2_20260815_8527916639",
        PROFILE / "data/owner-actions",
    ]
    files: list[Path] = []
    for root in roots:
        if root.is_file():
            files.append(root)
        elif root.is_dir():
            files.extend(path for path in root.rglob("*") if path.is_file())
    return {str(path.relative_to(PROFILE)): digest(path) for path in sorted(set(files))}


def safe(value: Any) -> Any:
    if value is None or isinstance(value, (bool, int, float, str)):
        return value
    if isinstance(value, Path):
        return str(value)
    if isinstance(value, (tuple, list)):
        return [safe(item) for item in value]
    if isinstance(value, dict):
        return {str(key): safe(item) for key, item in value.items()}
    key = getattr(value, "key", None)
    if key is not None:
        return {"type": type(value).__name__, "key": safe(key)}
    return {"type": type(value).__name__}


TRACE: list[dict[str, Any]] = []


def mark(method: str, phase: str, **state: Any) -> None:
    TRACE.append({"method": method, "phase": phase, **safe(state)})


class OfflineNetworkError(RuntimeError):
    pass


def deny_network(*args: Any, **kwargs: Any) -> None:
    raise OfflineNetworkError("network disabled by offline reproduction")


class FakeBot:
    def __init__(self) -> None:
        self.calls: list[dict[str, Any]] = []

    async def send_message(self, **kwargs: Any) -> object:
        self.calls.append(safe(kwargs))
        mark("FakeBot.send_message", "return", kwargs=kwargs, message_id=9001)
        return SimpleNamespace(message_id=9001)

    async def edit_message_text(self, **kwargs: Any) -> object:
        self.calls.append({"edit": safe(kwargs)})
        mark("FakeBot.edit_message_text", "return", kwargs=kwargs, message_id=9002)
        return SimpleNamespace(message_id=9002)


class FakeMessage:
    def __init__(self) -> None:
        self.text = TEXT
        self.from_user = SimpleNamespace(id=int(CUSTOMER_ID))
        self.chat = SimpleNamespace(id=int(CUSTOMER_ID), type="private", is_forum=False)
        self.chat_id = int(CUSTOMER_ID)
        self.message_thread_id = None
        self.message_id = 116
        self.reply_to_message = None
        self.replies: list[dict[str, Any]] = []

    async def reply_text(self, text: str, **kwargs: Any) -> object:
        call = {"text": text, **safe(kwargs)}
        self.replies.append(call)
        mark("FakeMessage.reply_text", "return", call=call, message_id=9003)
        return SimpleNamespace(message_id=9003)


def wrap_sync(owner: object, name: str) -> None:
    original = getattr(owner, name)
    def wrapped(self: object, *args: Any, **kwargs: Any) -> Any:
        mark(name, "enter", args=args, kwargs=kwargs)
        try:
            result = original(*args, **kwargs)
        except BaseException as exc:
            mark(name, "raise", exception_type=type(exc).__name__, exception=str(exc))
            raise
        mark(name, "return", result=result)
        return result
    setattr(owner, name, MethodType(wrapped, owner))


def wrap_async(owner: object, name: str) -> None:
    original = getattr(owner, name)
    async def wrapped(self: object, *args: Any, **kwargs: Any) -> Any:
        mark(name, "enter", args=args, kwargs=kwargs)
        try:
            result = await original(*args, **kwargs)
        except BaseException as exc:
            mark(name, "raise", exception_type=type(exc).__name__, exception=str(exc))
            raise
        mark(name, "return", result=result)
        return result
    setattr(owner, name, MethodType(wrapped, owner))


async def main() -> int:
    before = authority_snapshot()
    socket.create_connection = deny_network  # type: ignore[assignment]
    socket.socket.connect = deny_network  # type: ignore[assignment]

    PlatformConfig = importlib.import_module("gateway.config").PlatformConfig
    nutrition_module = importlib.import_module("gateway.platforms.nutrition_coaching")
    TelegramAdapter = importlib.import_module("gateway.platforms.telegram").TelegramAdapter

    raw = yaml.safe_load((PROFILE / "config.yaml").read_text(encoding="utf-8"))
    telegram_raw = raw["platforms"]["telegram"]
    config = PlatformConfig.from_dict({
        "enabled": True, "token": "offline-no-network", "extra": telegram_raw["extra"]
    })
    config.profile_root = str(PROFILE)
    setattr(nutrition_module, "profile_authority_lock", lambda _root: contextlib.nullcontext())

    adapter = TelegramAdapter(config)
    adapter._bot = FakeBot()
    coordinator = adapter._get_nutrition_coaching()
    if coordinator is None:
        raise RuntimeError(adapter._nutrition_coaching_error or "coordinator unavailable")
    coordinator._ensure_live_registry = lambda: True

    # Keep unrelated lazy ingress services inert. The exact registered-customer
    # routing, coordinator, bridge, render, and send methods remain intact.
    adapter._get_nutrition_onboarding_preview = lambda: None
    async def no_onboarding(_self: object, _update: object, _message: object) -> bool:
        return False
    adapter._handle_nutrition_onboarding_text = MethodType(no_onboarding, adapter)
    adapter._room_bootstrap_reserves_ingress = lambda _message: False
    async def no_review(_self: object, _update: object, _message: object) -> bool:
        return False
    adapter._reserve_adaptive_review_update = MethodType(no_review, adapter)

    wrap_sync(adapter, "_nutrition_address")
    wrap_async(adapter, "_render_nutrition_text")
    wrap_async(adapter, "_send_nutrition_topic")
    wrap_sync(coordinator, "resolve")
    wrap_sync(coordinator, "handle_text")

    message = FakeMessage()
    update = SimpleNamespace(update_id=UPDATE_ID, effective_message=message,
        message=message, edited_message=None, channel_post=None, edited_channel_post=None)
    result: dict[str, Any] = {}
    try:
        await adapter._handle_text_message(update, SimpleNamespace())
        result["completed"] = True
    except BaseException as exc:
        result.update(completed=False, exception_type=type(exc).__name__,
            exception_message=str(exc),
            traceback="".join(traceback.format_exception(type(exc), exc, exc.__traceback__)))

    after = authority_snapshot()
    changed = sorted(path for path in set(before) | set(after) if before.get(path) != after.get(path))
    resolved = next(iter(coordinator._routes.values()))
    result.update(
        source={
            "telegram_py_sha256": digest(SOURCE / "gateway/platforms/telegram.py"),
            "nutrition_coaching_py_sha256": digest(SOURCE / "gateway/platforms/nutrition_coaching.py")},
        input={"update_id": UPDATE_ID, "user_id": CUSTOMER_ID, "chat_id": CUSTOMER_ID,
            "chat_type": "private", "topic_id": "0", "text": TEXT},
        config={"registry_path": str(adapter._nutrition_coaching_config.registry_path),
            "adaptive_nutrition_enabled": adapter._adaptive_nutrition_config is not None},
        coordinator={"type": type(coordinator).__name__, "registry_path": str(coordinator._registry_path),
            "registered_routes": safe(coordinator._registered_routes),
            "enabled_route_keys": safe(tuple(coordinator._routes)),
            "customer_key": resolved.customer.spec.customer_key,
            "customer_enabled": resolved.customer.spec.enabled,
            "customer_data_root": str(resolved.customer.data_root),
            "bridge_active_binding": resolved.bridge.has_active_binding()},
        fake_bot_calls=adapter._bot.calls, fake_message_replies=message.replies,
        method_trace=TRACE, live_authority_file_count=len(before),
        live_authority_unchanged=before == after, changed_live_authority_files=changed,
        hypotheses=[
            "H1: private-DM address normalization does not resolve the activated registry route",
            "H2: committed-registry reload or customer service-state validation raises before dispatch",
            "H3: inactive wizard handling returns a notice but Telegram DM send routing raises",
            "H4: the inactive wizard bridge itself raises while reading missing/malformed binding state"])
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return 0 if result.get("completed") is False and result["live_authority_unchanged"] else 1


if __name__ == "__main__":
    raise SystemExit(asyncio.run(main()))
