#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11,<3.14"
# dependencies = []
# ///

# ─── How to run ───
# 1. Install uv (if not installed):
#      curl -LsSf https://astral.sh/uv/install.sh | sh
# 2. Run directly (no venv or credentials needed):
#      uv run scripts/run_nutricoach_v140_disposable_e2e.py --network-disabled --scenario weekly-operations --verify-privacy
# 3. Or make executable and run:
#      chmod +x scripts/run_nutricoach_v140_disposable_e2e.py && ./scripts/run_nutricoach_v140_disposable_e2e.py --network-disabled
# ──────────────────

"""Drive inherited weekly Telegram handlers against disposable real stores."""

from __future__ import annotations

import hashlib

import shutil
import sys
import tempfile

from pathlib import Path
from typing import Final

REPOSITORY: Final = Path(__file__).resolve().parents[1]
PROFILE_PACKAGE: Final = REPOSITORY / "dualcoach/profile"
for import_root in (REPOSITORY, PROFILE_PACKAGE):
    if str(import_root) not in sys.path:
        sys.path.insert(0, str(import_root))

from scripts.nutricoach_v140_runtime import JsonValue, ensure_project_runtime, write_json

ensure_project_runtime(REPOSITORY)

from checkin_cli.weekly_operations import WeeklyOperationsCorruption
from gateway.config import PlatformConfig
from gateway.platforms.nutrition_coaching import IncomingAddress
from gateway.platforms.nutrition_weekly_operations import Topic59DayCardProjector
from gateway.platforms.nutrition_weekly_operations_models import Topic59DayState
from scripts.nutricoach_v140_artifacts import (
    CLEANUP,
    RECEIPT,
    TRANSCRIPT,
    write_artifact_manifest,
)
from scripts.nutricoach_v140_authority_fixture import CANDIDATE
from scripts.nutricoach_v140_denial_matrix import run_owner_denial_matrix
from scripts.nutricoach_v140_failure_matrix import run_failure_matrix
from scripts.nutricoach_v140_fake_telegram import FakeTelegramEngine, fake_bot
from scripts.nutricoach_v140_fake_telegram_updates import (
    FORBIDDEN_TRANSCRIPT_VALUES,
    FakeCallbackQuery,
    FakeChat,
    FakeMessage,
)
from scripts.nutricoach_v140_privacy import scan_captured_texts
from scripts.nutricoach_v140_owner_fixture import (
    create_owner_coordinator,
    create_weekly_owner_draft,
    issue_delivery_capability,
    owner_platform_config,
)
from scripts.nutricoach_v140_telegram_facade import (
    DisposableOwnerTelegramAdapter,
    DisposableWeeklyTelegramAdapter,
    Topic59TransportFacade,
)
from scripts.nutricoach_v140_tick_fixture import (
    create_tick_fixture,
    cutoff_time,
    reminder_time,
)
from scripts.nutricoach_v140_topic59_fixture import create_topic59_fixture


class DisposableScenarioError(RuntimeError):
    """A sanitized disposable scenario invariant failed."""


async def _tick_surface(
    root: Path, engine: FakeTelegramEngine,
) -> dict[str, JsonValue]:
    fixture, coordinator, config, reminder, cutoff = create_tick_fixture(root)
    adapter = DisposableWeeklyTelegramAdapter(config, fake_bot(engine), CANDIDATE)
    reminder_failures = await adapter.run_weekly_tick(
        coordinator, (reminder,), reminder_time(),
    )
    reminder_calls = sum(
        action.method == "sendMessage" and action.chat_id == "300"
        for action in engine.actions
    )
    cutoff_failures = await adapter.run_weekly_tick(
        coordinator, (cutoff,), cutoff_time(),
    )
    calls_before_replay = engine.provider_calls()
    replay_failures = await adapter.run_weekly_tick(
        coordinator, (cutoff,), cutoff_time(),
    )
    if reminder_failures or cutoff_failures or replay_failures:
        raise DisposableScenarioError("production weekly tick reported a failure")
    if reminder_calls != 1 or engine.provider_calls() != calls_before_replay:
        raise DisposableScenarioError("weekly tick was not exactly once")
    rows = len(fixture.store.read())
    sidecar = fixture.store.authority.observed_path / (
        f"{fixture.store.customer_identity_digest}.day-status-v1.jsonl"
    )
    _ = sidecar.write_text("{", encoding="utf-8")
    try:
        _ = fixture.store.read()
    except WeeklyOperationsCorruption:
        corrupt_store_denied = True
    else:
        raise DisposableScenarioError("corrupt weekly store did not fail closed")
    card = next(
        action for action in engine.actions
        if action.chat_id == "200" and action.topic_id == "59"
    )
    return {
        "message_id": str(card.message_id), "provider_calls": calls_before_replay,
        "reminders": reminder_calls, "rows": rows,
        "corrupt_store_denied": corrupt_store_denied,
    }


async def _topic59_surface(
    root: Path, engine: FakeTelegramEngine,
) -> dict[str, JsonValue]:
    fixture = create_topic59_fixture(root / "authority")
    adapter = DisposableWeeklyTelegramAdapter(
        PlatformConfig(enabled=True, token="disabled", extra=fixture.authority.extra),
        fake_bot(engine), CANDIDATE,
    )
    projector = Topic59DayCardProjector(root / "topic59.jsonl")
    transport = Topic59TransportFacade(adapter)
    missed = await projector.publish(
        fixture.request(Topic59DayState.MISSED), transport,
    )
    late = await projector.publish(
        fixture.request(Topic59DayState.LATE_SUBMITTED), transport,
    )
    if missed.message_id is None or missed.message_id != late.message_id:
        raise DisposableScenarioError("late card did not edit its bound message")
    message = engine.message("200", missed.message_id)
    if "지연 제출" not in message.text:
        raise DisposableScenarioError("late card edit did not reach Telegram")
    return {"message_id": missed.message_id, "edited_message_id": late.message_id}


async def _owner_surface(
    root: Path, engine: FakeTelegramEngine,
) -> dict[str, JsonValue]:
    coordinator = create_owner_coordinator(root)
    created = create_weekly_owner_draft(root, coordinator)
    if created.draft is None or created.draft.draft_id is None:
        raise DisposableScenarioError("Monday owner draft was not created")
    draft_id = created.draft.draft_id
    bot = fake_bot(engine)
    adapter = DisposableOwnerTelegramAdapter(
        owner_platform_config(), bot, coordinator,
    )
    owner = IncomingAddress("100", "100", "0")
    message_id = await adapter.publish_owner_card(
        owner, coordinator.draft(draft_id, coordinator.owner),
    )
    edit_data = engine.callback("100", message_id, "e")
    denied_updates = await run_owner_denial_matrix(
        adapter, bot, engine, edit_data, message_id,
    )
    edit_query = FakeCallbackQuery(
        bot, engine, data=edit_data, user_id="100", chat_id="100",
        message_id=message_id, topic_id=None,
    )
    _ = await adapter.handle_owner_callback(edit_query)
    bad_input = await adapter.handle_owner_text(
        FakeMessage("100", 899, None, FakeChat("100", "private"), ""),
        owner, coordinator,
    )
    if not bad_input:
        raise DisposableScenarioError("owner text handler bypassed bad input")
    edited = await adapter.handle_owner_text(
        FakeMessage("100", 900, None, FakeChat("100", "private"), "weekly review"),
        owner, coordinator,
    )
    if not edited:
        raise DisposableScenarioError("owner text handler rejected the edit")
    customer_calls_after_edit = sum(
        action.chat_id == "300" for action in engine.actions
    )
    _ = await adapter.handle_owner_callback(edit_query)
    if sum(action.chat_id == "300" for action in engine.actions) != customer_calls_after_edit:
        raise DisposableScenarioError("stale callback reached customer transport")
    approved_message_id, approve_data = engine.callback_target("100", "a")
    edited_draft_id = approve_data.split(":", 3)[1]
    approve_query = FakeCallbackQuery(
        bot, engine, data=approve_data, user_id="100", chat_id="100",
        message_id=approved_message_id, topic_id=None,
    )
    customer_baseline = sum(action.chat_id == "300" for action in engine.actions)
    _ = await adapter.handle_owner_callback(approve_query)
    if sum(action.chat_id == "300" for action in engine.actions) != customer_baseline:
        raise DisposableScenarioError("customer received weekly copy before approval")
    issue_delivery_capability(coordinator, edited_draft_id)
    send_message_id, send_data = engine.callback_target("100", "s")
    send_query = FakeCallbackQuery(
        bot, engine, data=send_data, user_id="100", chat_id="100",
        message_id=send_message_id, topic_id=None,
    )
    _ = await adapter.handle_owner_callback(send_query)
    customer_after = sum(action.chat_id == "300" for action in engine.actions)
    restarted = DisposableOwnerTelegramAdapter(
        owner_platform_config(), bot, coordinator,
    )
    _ = await restarted.handle_owner_callback(send_query)
    customer_replay = sum(action.chat_id == "300" for action in engine.actions)
    durable = coordinator.draft_generation(edited_draft_id)
    if customer_after - customer_baseline != 1 or customer_replay != customer_after:
        raise DisposableScenarioError("approved customer send was not exactly once")
    if durable is None or durable.state.value != "sent_audited":
        raise DisposableScenarioError("customer send receipt was not durable")
    controls = tuple(action for action in engine.actions if action.has_controls)
    if any(action.chat_id != "100" for action in controls):
        raise DisposableScenarioError("owner controls escaped canonical DM")
    return {
        "approved_customer_sends": 1, "preapproval_customer_sends": 0,
        "owner_controls_dm_only": True, "denied_updates": denied_updates,
        "restart_replay_sends": 0,
        "draft_id_digest": hashlib.sha256(edited_draft_id.encode()).hexdigest(),
    }


async def run_scenario(evidence_dir: Path, verify_privacy: bool) -> None:
    """Run actual inherited handlers and atomically write sanitized evidence."""
    root = Path(tempfile.mkdtemp(prefix="nutricoach-v140-e2e-"))
    engine = FakeTelegramEngine()
    try:
        tick = await _tick_surface(root / "tick", engine)
        topic59 = await _topic59_surface(root / "topic59", engine)
        owner = await _owner_surface(root / "owner", engine)
        failures = await run_failure_matrix(root / "failures")
        privacy = scan_captured_texts(
            engine.captured_texts, FORBIDDEN_TRANSCRIPT_VALUES,
        )
        if verify_privacy and privacy.leaks:
            raise DisposableScenarioError("raw Telegram capture failed privacy checks")
        receipt: dict[str, JsonValue] = {
            "schema": "nutricoach-v140-disposable-telegram-e2e-v3",
            "status": "PASS", "network_calls": 0, "credentials_loaded": 0,
            "tick": tick, "topic59": topic59, "owner": owner,
            "failures": failures, "privacy_leaks": privacy.leaks,
            "privacy": privacy.receipt,
        }
        write_json(evidence_dir / "task-11-telegram-e2e-r3.json", receipt)
        write_json(
            evidence_dir / "task-11-transcript-r3.json",
            privacy.transcript,
        )
    finally:
        shutil.rmtree(root, ignore_errors=True)
        write_json(
            evidence_dir / "task-11-cleanup-r3.json",
            {"temporary_profile_removed": not root.exists(), "network_opened": False},
        )
        if all((evidence_dir / name).is_file() for name in (RECEIPT, TRANSCRIPT, CLEANUP)):
            write_artifact_manifest(evidence_dir, (RECEIPT, TRANSCRIPT, CLEANUP))


if __name__ == "__main__":
    from scripts.nutricoach_v140_cli import main

    raise SystemExit(main(REPOSITORY, run_scenario))
