"""Real coordinator and owner lifecycle for grounded Monday drafts."""

from __future__ import annotations

import json
import sys
from concurrent.futures import ThreadPoolExecutor
from dataclasses import replace
from datetime import datetime
from pathlib import Path
from threading import Lock
from collections.abc import Mapping

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

from checkin_cli import load_customer_registry
from checkin_cli.weekly_operations_grounding import build_grounded_weekly_source
from checkin_cli.weekly_operations_knowledge import WeeklyEmphasis, WeeklyPrincipleId

from gateway.platforms.nutrition_coaching import CallbackInput, IncomingAddress, NutritionCoachingCoordinator
from tests.gateway._weekly_operations_authority_support import (
    test_registry_identity_binding_digest as registry_identity_binding_digest,
    test_registry_identity_document as registry_identity_document,
)
from gateway.platforms.nutrition_weekly_operations_authority import WeeklyOperationsRuntimeContext, parse_weekly_operations_authority
from gateway.platforms.nutrition_weekly_operations_config import parse_weekly_operations_config
from gateway.platforms.nutrition_weekly_owner_draft import WeeklyOwnerDraftRequest
from gateway.platforms.nutrition_weekly_owner_model import JsonValue
from gateway.platforms.nutrition_weekly_owner_contract import WeeklyDraftLifecycle
from tests.gateway._weekly_owner_summary_support import bound_summary_at


def _coordinator(tmp_path: Path) -> NutritionCoachingCoordinator:
    weeks = [{
        "week": week, "calories_kcal": 2300, "protein_g": 150,
        "carbohydrate_g": 280, "fat_g": 65, "meal_structure": ["아침", "점심", "저녁"],
    } for week in range(1, 13)]
    registry_path = tmp_path / "registry.json"
    _ = registry_path.write_text(json.dumps({
        "version": 1,
        "owner": {"user_id": "coach", "chat_id": "owner-dm", "topic_id": "owner"},
        "customers": [{
            "customer_key": "client_001", "display_name": "고객 001", "enabled": True,
            "telegram": {"user_id": "client", "chat_id": "customer-chat", "topic_id": "customer-topic"},
            "ai_processing_consent": {"granted": True, "recorded_on": "2026-07-01", "notice_version": "privacy-v1"},
            "schedule": {"daily_time": "08:00", "weekly_weekday": 0, "monthly_day": 1},
            "plan": {"starts_on": "2026-07-01", "focus": "nutrition_90_training_10", "weeks": weeks},
        }],
    }, ensure_ascii=False), encoding="utf-8")
    coordinator = NutritionCoachingCoordinator(tmp_path, load_customer_registry(registry_path, tmp_path))
    address = IncomingAddress("client", "customer-chat", "customer-topic")
    opening = coordinator.open_launcher("client_001")
    assert opening.callback_data is not None
    _ = coordinator.bind_launcher("client_001", opening.callback_data, "44")
    _ = coordinator.handle_callback(CallbackInput(opening.callback_data, address, "44"))
    resolved = coordinator.resolve(address)
    assert resolved is not None
    bridge = resolved.bridge
    actions = ("value", "value", "value", "value", "value", "value", "select", "select", "select", "value", "value", "select")
    answers = ("70", "2300", "150 280 65", "계획대로 3식", "2.5", "7", "4", "normal", "4", "식욕 3/5, 스트레스 2/5", "하체 70분", "skip")
    reply = bridge.apply_model_action(actions[0], answers[0])
    for action, value in zip(actions[1:], answers[1:], strict=True):
        reply = bridge.apply_model_action(action, value)
    assert reply.prompt is not None
    save = next(callback for label, callback in reply.prompt.buttons if label == "저장")
    assert coordinator.handle_callback(CallbackInput(save, address, "44")).completion is not None
    return coordinator


def _request(coordinator: NutritionCoachingCoordinator):
    config = parse_weekly_operations_config({"nutrition_coaching": {
        "operator_review": {"user_id": "operator", "chat_id": "review", "topic_id": 59},
        "weekly_operations": {"enabled": True, "reminder_time": "20:00:00", "missed_cutoff_time": "23:00:00", "weekly_weekday": 0, "feature_epoch": "weekly-operations-v1", "registry_identity_binding_digest": registry_identity_binding_digest()},
    }})
    runtime = WeeklyOperationsRuntimeContext(
        "a" * 64, "client_001", "coach", "owner-dm", 7, "b" * 64, True,
        "weekly-operations-v1", datetime.fromisoformat("2026-08-24T08:00:00+09:00"),
    )
    receipt = parse_weekly_operations_authority({
        "schema": "nutricoach-weekly-operations-authority-v2",
        "candidate_digest": runtime.candidate_digest, "config_digest": config.digest,
        "enabled_customer_keys": [runtime.customer_key],
        "owner": {"user_id": "coach", "chat_id": "owner-dm", "version": 7},
        "consent_digest": runtime.consent_digest,
        "registry_identity": registry_identity_document(),
        "issued_at": "2026-08-24T00:00:00+09:00", "expires_at": "2026-08-25T00:00:00+09:00",
        "feature_epoch": runtime.feature_epoch,
    })
    bound = bound_summary_at(
        coordinator.weekly_owner_storage_authority().acquisition_path / "summary-authority",
    )
    return WeeklyOwnerDraftRequest(
        "client_001", coordinator.weekly_owner_key(), config, receipt, runtime,
        bound, coordinator.weekly_owner_storage_authority().binding_digest,
        lambda: bound,
    )


class ValidModel:
    def __init__(self) -> None:
        self.calls: int = 0
        self._lock: Lock = Lock()

    def generate(self, request: Mapping[str, JsonValue]) -> Mapping[str, JsonValue]:
        with self._lock:
            self.calls += 1
        return {
            "grounding_digest": request["grounding_digest"],
            "principle_ids": ["recorded_trend", "owner_decides"],
            "emphasis": "summary_first",
        }


def test_concurrent_restart_owner_edit_and_approval_use_one_generation(tmp_path: Path) -> None:
    coordinator = _coordinator(tmp_path)
    lifecycle: WeeklyDraftLifecycle = coordinator
    assert lifecycle.weekly_owner_key() == coordinator.weekly_owner_key()
    request = _request(coordinator)
    model = ValidModel()

    with ThreadPoolExecutor(max_workers=10) as pool:
        def create_one(_index: int):
            return coordinator.create_grounded_weekly_owner_draft(request, model)

        results = tuple(pool.map(
            create_one,
            range(10),
        ))
    draft_ids = {result.draft.draft_id for result in results if result.draft is not None}
    draft_id = next(iter(draft_ids))
    assert isinstance(draft_id, str)
    restarted = NutritionCoachingCoordinator(
        tmp_path, load_customer_registry(tmp_path / "registry.json", tmp_path),
    )
    replay = restarted.create_grounded_weekly_owner_draft(
        replace(request, owner_key=restarted.weekly_owner_key()), model,
    )

    assert model.calls == 1
    assert draft_ids == {draft_id}
    assert replay.provider_calls == 0
    assert coordinator.prepare_send_draft(draft_id, coordinator.owner).error == "draft_not_approved"
    created = coordinator.draft(draft_id, coordinator.owner)
    edited = coordinator.edit_draft(
        draft_id, coordinator.owner, f"{created.text}\n소유자 수정",
        expected_generation=created.generation,
        expected_record_digest=created.generation_record_digest,
        expected_checkin_revision=created.generation_checkin_revision,
        expected_draft_revision=created.generation_draft_revision,
    )
    assert edited.accepted is True
    assert isinstance(edited.draft_id, str)
    approved = coordinator.approve_draft(
        edited.draft_id, coordinator.owner,
        expected_generation=edited.generation,
        expected_record_digest=edited.generation_record_digest,
        expected_checkin_revision=edited.generation_checkin_revision,
        expected_draft_revision=edited.generation_draft_revision,
    )
    assert approved.accepted is True


def test_direct_tampered_source_fails_before_draft_mutation(tmp_path: Path) -> None:
    coordinator = _coordinator(tmp_path)
    request = _request(coordinator)
    drafts_path = tmp_path / "data" / "owner-actions" / "drafts.json"
    before = drafts_path.read_bytes() if drafts_path.exists() else None
    source = build_grounded_weekly_source(
        request.facts, (WeeklyPrincipleId.OWNER_DECIDES,), WeeklyEmphasis.SUMMARY_FIRST,
    )
    unreserved = coordinator.create_weekly_review_draft("client_001", coordinator.owner, source)
    assert (unreserved.accepted, unreserved.error) == (False, "weekly_review_source_invalid")
    tampered_source = replace(source, source_digest="f" * 64)
    direct = coordinator.create_weekly_review_draft("client_001", coordinator.owner, tampered_source)
    assert (direct.accepted, direct.error) == (False, "weekly_review_source_invalid")
    assert (drafts_path.read_bytes() if drafts_path.exists() else None) == before
    object.__setattr__(request.bound_summary, "summary_digest", "f" * 64)
    denied = coordinator.create_grounded_weekly_owner_draft(request, ValidModel())
    assert (denied.accepted, denied.provider_calls) == (False, 0)
    assert (drafts_path.read_bytes() if drafts_path.exists() else None) == before
