from __future__ import annotations

import hashlib
import importlib
import json
from datetime import date, datetime
from pathlib import Path
from typing import Any
from zoneinfo import ZoneInfo

from gateway.platforms.nutrition_onboarding_reconciliation import (
    NutritionOnboardingReconciler,
)
from gateway.platforms.telegram_nutrition_onboarding import (
    NUTRITION_ONBOARDING_API_VERSION,
    OPTIONAL_QUESTION_DEFAULTS,
    encode_callback_hash,
)
from gateway.platforms.telegram_nutrition_onboarding_authority import (
    consent_receipt_digest,
    feature_epoch_digest,
)
from gateway.platforms.telegram_nutrition_onboarding_runtime_callback import (
    TelegramNutritionOnboardingRuntimeCallbackMixin,
)
from gateway.platforms.telegram_nutrition_onboarding_runtime_authority import (
    TelegramNutritionOnboardingRuntimeAuthorityMixin,
)
from gateway.platforms.telegram_nutrition_onboarding_runtime_collection import (
    TelegramNutritionOnboardingRuntimeCollectionMixin,
    is_consent_withdrawal,
)
from gateway.platforms.telegram_nutrition_onboarding_runtime_errors import (
    NutritionOnboardingRuntimeError,
)
from gateway.platforms.telegram_nutrition_onboarding_runtime_publication import (
    TelegramNutritionOnboardingRuntimePublicationMixin,
    publication_generation,
)
from gateway.platforms.telegram_nutrition_onboarding_publication_outbox import (
    GatewayOnboardingPublicationOutbox,
)
from gateway.platforms.telegram_customer_bootstrap import (
    BootstrapState,
)


class TelegramNutritionOnboardingRuntime(
    TelegramNutritionOnboardingRuntimeAuthorityMixin,
    TelegramNutritionOnboardingRuntimePublicationMixin,
    TelegramNutritionOnboardingRuntimeCollectionMixin,
    TelegramNutritionOnboardingRuntimeCallbackMixin,
):
    def __init__(
        self,
        *,
        adapter: Any,
        profile_root: Path,
        bootstrap_transport: Any,
        reconciler: NutritionOnboardingReconciler | None = None,
    ) -> None:
        self.adapter = adapter
        self.profile_root = Path(profile_root).resolve()
        self.bootstrap = bootstrap_transport
        self.reconciler = reconciler or NutritionOnboardingReconciler()
        self._publication_outbox = GatewayOnboardingPublicationOutbox(
            self.profile_root
        )
        self.domain = importlib.import_module(
            "checkin_cli.nutrition_onboarding"
        )
        source_path = getattr(self.domain, "__file__", None)
        if not isinstance(source_path, str):
            raise NutritionOnboardingRuntimeError(
                "nutrition onboarding module source is unavailable"
            )
        source = Path(source_path).resolve()
        expected = (
            self.profile_root / "workspace" / "checkin_cli" / "checkin_cli"
        ).resolve()
        if not source.is_relative_to(expected):
            raise NutritionOnboardingRuntimeError(
                "nutrition onboarding module is outside configured profile"
            )
        if (
            getattr(
                self.domain,
                "NUTRITION_ONBOARDING_API_VERSION",
                None,
            )
            != NUTRITION_ONBOARDING_API_VERSION
        ):
            raise NutritionOnboardingRuntimeError(
                "unsupported nutrition onboarding API version"
            )

    async def start_after_consent(
        self,
        *,
        session: Any,
        query: Any,
        message: Any,
    ) -> None:
        current = self.bootstrap.store.get(session.session_id)
        if current.state is not BootstrapState.AWAITING_ACTIVATION:
            return
        recorded_at = getattr(message, "date", None)
        if not isinstance(recorded_at, datetime):
            await query.answer(text="동의 시각을 확인하지 못했습니다.")
            return
        try:
            self.domain.record_current_registry_consent(
                self.profile_root,
                current.customer_key,
                recorded_on=recorded_at.astimezone(
                    ZoneInfo("Asia/Seoul")
                ).date(),
            )
        except (OSError, TypeError, ValueError):
            await query.answer(
                text="현재 동의를 안전하게 저장하지 못했습니다."
            )
            return
        try:
            authority = self._current_authority(current)
        except (OSError, ValueError):
            await query.answer(
                text="온보딩 권한 또는 동의가 변경됐습니다."
            )
            return
        evidence = self._evidence(
            actor_id=getattr(
                getattr(query, "from_user", None),
                "id",
                None,
            ),
            message=message,
            update_id=getattr(message, "message_id", None),
        )
        service = self._service(current.customer_key)
        status = service.start_or_resume(
            authority=authority,
            evidence=evidence,
            consent_receipt_digest=consent_receipt_digest(
                session=session,
                query=query,
                message=message,
            ),
        )
        await self._publish(current, service, status)

    def _service(self, customer_key: str) -> Any:
        return self.domain.NutritionOnboardingService(
            profile_root=self.profile_root,
            customer_key=customer_key,
            enforce_reconciliation=True,
        )

    def _finalize_owner_approval(
        self,
        *,
        session: Any,
        service: Any,
        authority: Any,
    ) -> Any:
        return service.finalize(
            starts_on=date.fromisoformat(session.customer_draft.starts_on),
            issued_at_kst=datetime.now(ZoneInfo("Asia/Seoul")),
            privacy_consent_digest=None,
            feature_epoch_digest=feature_epoch_digest(
                profile_root=self.profile_root,
                customer_key=session.customer_key,
            ),
            authority=authority,
        )

    async def recover_waiting_session(self, session: Any) -> bool:
        """Restore one current customer card only from durable onboarding state."""
        current = self.bootstrap.store.get(session.session_id)
        if current.state is not BootstrapState.AWAITING_ACTIVATION:
            return False
        service = self._service(current.customer_key)
        await self._recover_publication_receipts(service, session=current)
        try:
            status = service.status()
            if status.state.value == "finalizing":
                status = self._finalize_owner_approval(
                    session=current,
                    service=service,
                    authority=self._current_authority(current),
                )
        except (OSError, TypeError, ValueError):
            return False
        await self._publish(current, service, status)
        if status.state.value == "ready":
            self._reconcile_owner_callback_receipt(current, service)
        return True

    def _preserve_owner_callback_receipt(
        self,
        *,
        session: Any,
        service: Any,
        publication: Any,
        authority: Any,
        evidence: Any,
        callback_data: str,
    ) -> None:
        """Persist the authenticated Approve event before profile finalization."""
        outbox = self._publication_outbox
        document = self.domain.read_private_json(service.session_path)
        consumed = document.get("consumed_updates")
        if not isinstance(consumed, list):
            raise ValueError("owner callback consumed-update authority is invalid")
        consumed_updates_before: list[int] = []
        for item in consumed:
            if type(item) is not int or item < 0:
                raise ValueError("owner callback consumed-update authority is invalid")
            consumed_updates_before.append(item)
        if len(set(consumed_updates_before)) != len(consumed_updates_before):
            raise ValueError("owner callback consumed-update authority is invalid")
        route = self._route(session, "owner")
        actor_user_id = getattr(evidence, "actor_user_id", None)
        update_id = getattr(evidence, "update_id", None)
        message_id = getattr(evidence, "message_id", None)
        evidence_route = (
            str(getattr(evidence, "chat_id", "")),
            str(getattr(evidence, "topic_id", "")),
        )
        owner_user_id = getattr(authority, "owner_user_id", None)
        owner_chat_id = getattr(authority, "owner_chat_id", None)
        owner_topic_id = getattr(authority, "owner_topic_id", None)
        if (
            type(actor_user_id) is not int
            or type(update_id) is not int
            or type(message_id) is not int
            or type(owner_user_id) is not int
            or type(owner_chat_id) is not int
            or type(owner_topic_id) is not int
            or actor_user_id != owner_user_id
            or evidence_route != route
            or message_id != getattr(publication, "message_id", None)
        ):
            raise ValueError("owner callback event authority does not match")
        authority_values = (owner_user_id, owner_chat_id, owner_topic_id)
        outbox.record_owner_callback(
            session_id=str(session.session_id),
            customer_key=str(session.customer_key),
            action="Approve",
            actor_user_id=actor_user_id,
            authority=authority_values,
            route=route,
            message_id=message_id,
            update_id=update_id,
            consumed_updates_before=tuple(consumed_updates_before),
            callback_data=callback_data,
            publication_generation=publication.generation,
        )

    def _reconcile_owner_callback_receipt(
        self,
        session: Any,
        service: Any,
    ) -> bool:
        """Authenticate and receipt one owner commit whose UI recovered later."""
        outbox = getattr(self, "_publication_outbox", None)
        ready_path = getattr(service, "ready_path", None)
        baseline_path = ready_path.parent / "baseline-v1.json" if isinstance(ready_path, Path) else None
        callback = getattr(
            getattr(self, "adapter", None),
            "_reconcile_telegram_business_commit",
            None,
        )
        domain = getattr(self, "domain", None)
        read_private_json = getattr(domain, "read_private_json", None)
        if (
            not isinstance(outbox, GatewayOnboardingPublicationOutbox)
            or not isinstance(ready_path, Path)
            or not isinstance(baseline_path, Path)
            or not callable(callback)
            or not callable(read_private_json)
        ):
            return False
        try:
            document = read_private_json(ready_path)
            baseline = read_private_json(baseline_path)
            consumed = document.get("consumed_updates")
            event = outbox.owner_callback(str(session.session_id))
            if (
                document.get("state") != "ready"
                or event is None
                or not isinstance(consumed, list)
                or tuple(consumed) != (*event.consumed_updates_before, event.update_id)
            ):
                return False
            update_id = event.update_id
            baseline_digest = document.get("baseline_digest")
            owner_review_receipt = baseline.get("owner_review_receipt")
            if (
                not isinstance(baseline_digest, str)
                or len(baseline_digest) != 64
                or baseline.get("digest") != baseline_digest
                or not isinstance(owner_review_receipt, str)
                or len(owner_review_receipt) != 64
            ):
                return False
            authority = self._current_authority(session)
            current = service.store.load_session(str(session.session_id))
            records = outbox.records()
            ready_matches = tuple(
                record
                for record in records
                if record.session_id == str(session.session_id)
                and record.role == "owner"
                and record.state == "COMMITTED"
                and record.payload.get("state") == "ready"
                and record.generation == getattr(current, "generation", None)
                and record.message_id == getattr(current, "message_id", None)
                and record.payload == getattr(current, "payload", None)
                and isinstance(record.receipt_integrity, str)
                and self._receipt_route_is_current(record, session)
            )
            owner_matches = tuple(
                record
                for record in records
                if record.session_id == str(session.session_id)
                and record.role == "owner"
                and record.state == "COMMITTED"
                and record.payload.get("state") == "owner_review"
                and record.generation == event.publication_generation
                and record.message_id == event.message_id
                and record.route == event.route
                and record.dispatch_identity == event.publication_dispatch_identity
                and record.receipt_integrity == event.publication_receipt_integrity
                and self._receipt_route_is_current(record, session)
            )
            authority_values = (
                authority.owner_user_id,
                authority.owner_chat_id,
                authority.owner_topic_id,
            )
            expected_callback_data = encode_callback_hash(
                action="owner_ok",
                generation=event.publication_generation,
                sid_hash=str(session.sid_hash),
            )
            if (
                len(ready_matches) != 1
                or len(owner_matches) != 1
                or event.customer_key != str(session.customer_key)
                or event.action != "Approve"
                or event.actor_user_id != authority.owner_user_id
                or event.authority != authority_values
                or event.route != tuple(str(item) for item in self._route(session, "owner"))
                or event.callback_data != expected_callback_data
            ):
                return False
            ready_receipt = ready_matches[0]
            owner_receipt = owner_matches[0]
            provenance_digest = hashlib.sha256(
                json.dumps(
                    {
                        "owner_review_receipt": owner_review_receipt,
                        "owner_callback_event_integrity": event.event_integrity,
                        "owner_publication_integrity": owner_receipt.receipt_integrity,
                        "ready_publication_integrity": ready_receipt.receipt_integrity,
                        "session_digest": hashlib.sha256(
                            str(session.session_id).encode()
                        ).hexdigest(),
                        "update_id": update_id,
                    },
                    sort_keys=True,
                    separators=(",", ":"),
                ).encode()
            ).hexdigest()
            callback(
                update_id=update_id,
                provenance_digest=provenance_digest,
                actor_id=event.actor_user_id,
                chat_id=int(event.route[0]),
                topic_id=int(event.route[1]),
                message_id=event.message_id,
                callback_data=event.callback_data,
            )
        except (AttributeError, OSError, TypeError, ValueError):
            return False
        return True

    def replayed_status_if_consumed(
        self,
        service: object,
        update_id: int | None,
    ) -> object | None:
        """Return durable status only when this exact update already committed."""
        if type(update_id) is not int or update_id < 0:
            return None
        read_private_json = getattr(
            getattr(self, "domain", None),
            "read_private_json",
            None,
        )
        status = getattr(service, "status", None)
        if not callable(read_private_json) or not callable(status):
            return None
        for path in (
            getattr(service, "session_path", None),
            getattr(service, "ready_path", None),
        ):
            if not isinstance(path, Path) or not path.exists():
                continue
            document = read_private_json(path)
            consumed_updates = document.get("consumed_updates")
            if isinstance(consumed_updates, list) and update_id in consumed_updates:
                return status()
        return None

    def _rewind_collection(
        self,
        *,
        service: Any,
        authority: Any,
        evidence: Any,
        target_cursor: int,
    ) -> Any:
        fields = self.domain.QUESTION_FIELDS
        if target_cursor < 0 or target_cursor >= len(fields):
            raise NutritionOnboardingRuntimeError(
                "invalid onboarding navigation target"
            )
        document = self.domain.load_mutable_session(
            session_path=service.session_path,
            customer_key=service.customer_key,
            authority=authority,
            evidence=evidence,
            role="customer",
        )
        raw_answers = document.get("answers")
        if not isinstance(raw_answers, dict):
            raise NutritionOnboardingRuntimeError(
                "invalid onboarding answers"
            )
        answers: dict[str, object] = {}
        for key, value in raw_answers.items():
            if not isinstance(key, str):
                raise NutritionOnboardingRuntimeError(
                    "invalid onboarding answers"
                )
            answers[key] = value
        for field in fields[target_cursor:]:
            if not isinstance(field, str):
                raise NutritionOnboardingRuntimeError(
                    "invalid onboarding navigation target"
                )
            answers.pop(field, None)
        document["answers"] = answers
        document.update(
            state=self.domain.OnboardingState.COLLECTING.value,
            cursor=target_cursor,
        )
        document.pop("reconciliation", None)
        self.domain.save_session(
            session_path=service.session_path,
            document=document,
            evidence=evidence,
        )
        return self.domain.build_status(
            customer_key=service.customer_key,
            document=document,
        )

    async def _continue_later(
        self,
        session: Any,
        service: Any,
        status: Any,
    ) -> None:
        await self._publish_paused(session, service, status)

    async def _resume(self, session: Any, service: Any) -> None:
        await self._publish(session, service, service.status())
