from __future__ import annotations

import hashlib
import json
from typing import Any, cast

from gateway.platforms.telegram_nutrition_onboarding import (
    encode_callback_hash,
)
from gateway.platforms.telegram_nutrition_onboarding_publication_outbox import (
    GatewayOnboardingPublicationOutbox,
    GatewayPublicationReceipt,
)


class PublicationReceiptPersistenceError(RuntimeError):
    """A sent Telegram card lacks the durable evidence required for replay."""


def operator_attention_payloads_equivalent(
    left: dict[str, Any],
    right: dict[str, Any],
) -> bool:
    left_attention = left.get("operator_attention_identity")
    right_attention = right.get("operator_attention_identity")
    left_epoch = left.get("operator_delivery_epoch")
    right_epoch = right.get("operator_delivery_epoch")
    if (
        not isinstance(left_attention, str)
        or left_attention != right_attention
        or not isinstance(left_epoch, str)
        or left_epoch != right_epoch
    ):
        return False
    normalized_left = dict(left)
    normalized_right = dict(right)
    normalized_left.pop("operator_recovery_request", None)
    normalized_right.pop("operator_recovery_request", None)
    return normalized_left == normalized_right


def publication_generation(
    status: Any,
    *,
    current: Any | None = None,
    payload: dict[str, Any] | None = None,
) -> int:
    candidate = status.answer_count + {
        "customer_attestation": 1,
        "owner_review": 3,
        "finalizing": 4,
        "ready": 4,
    }.get(status.state.value, 0)
    if current is None:
        return candidate
    if payload is not None and current.payload == payload:
        return current.generation
    return max(candidate, current.generation + 1)


class TelegramNutritionOnboardingRuntimePublicationTransportMixin:
    adapter: Any

    def _route(self, session: Any, role: str) -> tuple[str, str]:
        raise NotImplementedError

    async def _recover_publication_receipts(
        self,
        service: Any,
        *,
        session: Any | None = None,
        expected: GatewayPublicationReceipt | None = None,
    ) -> int:
        """Commit only receipted publications matching current durable authority."""
        outbox = getattr(self, "_publication_outbox", None)
        if not isinstance(outbox, GatewayOnboardingPublicationOutbox):
            return 0
        outbox.reconcile_emergency_receipts()
        recovered = 0
        for receipt in outbox.receipted():
            if (
                receipt.message_id is None
                or expected is not None
                and not self._same_publication_authority(receipt, expected)
                or not self._receipt_route_is_current(receipt, session)
            ):
                continue
            current = self._current_publication(service, receipt.session_id)
            if not self._current_matches_receipt(current, receipt):
                continue
            if getattr(current, "state", None) == "COMMITTED":
                if getattr(current, "message_id", None) != receipt.message_id:
                    continue
            else:
                try:
                    service.store.mark_committed(
                        session_id=receipt.session_id,
                        generation=receipt.generation,
                        message_id=receipt.message_id,
                    )
                except ValueError:
                    current = self._current_publication(service, receipt.session_id)
                    if (
                        not self._current_matches_receipt(current, receipt)
                        or getattr(current, "state", None) != "COMMITTED"
                        or getattr(current, "message_id", None) != receipt.message_id
                    ):
                        continue
            outbox.mark_committed(
                session_id=receipt.session_id,
                generation=receipt.generation,
                payload=receipt.payload,
                route=receipt.route,
                role=receipt.role,
                render_identity=receipt.render_identity,
                message_id=receipt.message_id,
            )
            recovered += 1
        return recovered

    def _receipt_route_is_current(
        self,
        receipt: GatewayPublicationReceipt,
        session: Any | None,
    ) -> bool:
        current_session = session
        if current_session is None:
            bootstrap = getattr(self, "bootstrap", None)
            store = getattr(bootstrap, "store", None)
            get = getattr(store, "get", None)
            if callable(get):
                try:
                    current_session = get(receipt.session_id)
                except (OSError, ValueError):
                    return False
            elif bootstrap is not None:
                return False
        if current_session is None:
            return True
        try:
            return self._route(current_session, receipt.role) == receipt.route
        except (OSError, TypeError, ValueError, AttributeError):
            return False

    @staticmethod
    def _same_publication_authority(
        left: GatewayPublicationReceipt,
        right: GatewayPublicationReceipt,
    ) -> bool:
        return (
            left.session_id == right.session_id
            and left.generation == right.generation
            and left.payload == right.payload
            and left.route == right.route
            and left.role == right.role
            and left.render_identity == right.render_identity
            and left.payload_digest == right.payload_digest
            and left.dispatch_identity == right.dispatch_identity
        )

    @staticmethod
    def _current_matches_receipt(
        current: Any | None,
        receipt: GatewayPublicationReceipt,
    ) -> bool:
        return bool(
            current is not None
            and getattr(current, "generation", None) == receipt.generation
            and getattr(current, "payload", None) == receipt.payload
            and getattr(current, "state", None) in {"PREPARED", "COMMITTED"}
        )

    def _publication_callback_is_current(
        self,
        *,
        session: Any,
        publication: Any,
        role: str,
        message: Any,
    ) -> bool:
        """Require a current signed receipt on the callback's exact route."""
        outbox = getattr(self, "_publication_outbox", None)
        if not isinstance(outbox, GatewayOnboardingPublicationOutbox):
            # Runtime construction always installs the outbox; only
            # transport-less unit seams omit it.
            return True
        chat_id = getattr(getattr(message, "chat", None), "id", None)
        if chat_id is None:
            chat_id = getattr(message, "chat_id", None)
        topic_id = getattr(message, "message_thread_id", None)
        if topic_id is None:
            topic_id = 0
        message_id = getattr(message, "message_id", None)
        if (
            type(chat_id) is not int
            or type(topic_id) is not int
            or type(message_id) is not int
            or message_id <= 0
        ):
            return False
        try:
            route = self._route(session, role)
            if route != (str(chat_id), str(topic_id)):
                return False
            matches = tuple(
                receipt
                for receipt in outbox.records()
                if (
                    receipt.session_id == str(session.session_id)
                    and receipt.generation == publication.generation
                    and receipt.payload == publication.payload
                    and receipt.route == route
                    and receipt.role == role
                    and receipt.message_id == message_id
                    and receipt.state in {"RECEIPTED", "COMMITTED"}
                )
            )
        except (OSError, ValueError, TypeError, AttributeError):
            return False
        return len(matches) == 1

    @staticmethod
    def _current_publication(
        service: Any,
        session_id: str,
    ) -> Any | None:
        try:
            return service.store.load_session(session_id)
        except ValueError:
            return None

    @staticmethod
    def _render_identity(
        *,
        session: Any,
        generation: int,
        route: tuple[str, str],
        text: str,
        role: str,
        payload: dict[str, Any],
        actions: list[tuple[str, str]],
        force_reply: bool,
        reply_anchor_message_id: int | None,
    ) -> str:
        try:
            canonical = json.dumps(
                {
                    "session_id": str(session.session_id),
                    "sid_hash": str(session.sid_hash),
                    "generation": generation,
                    "route": list(route),
                    "text": text,
                    "role": role,
                    "payload": payload,
                    "actions": [list(action) for action in actions],
                    "force_reply": force_reply,
                    "reply_anchor_message_id": reply_anchor_message_id,
                },
                ensure_ascii=False,
                sort_keys=True,
                separators=(",", ":"),
                allow_nan=False,
            )
        except (AttributeError, TypeError, ValueError) as exc:
            raise PublicationReceiptPersistenceError(
                "onboarding publication render identity is unavailable"
            ) from exc
        return hashlib.sha256(canonical.encode("utf-8")).hexdigest()

    @staticmethod
    def _message_id(sent: Any) -> int:
        message_id = getattr(sent, "message_id", None)
        if type(message_id) is not int or isinstance(message_id, bool) or message_id <= 0:
            raise PublicationReceiptPersistenceError(
                "Telegram publication returned no authoritative message ID"
            )
        return message_id

    @staticmethod
    def _mark_unknown(service: Any, session_id: str, generation: int) -> None:
        try:
            service.store.mark_uncertain(
                session_id=session_id,
                generation=generation,
            )
        except ValueError:
            pass

    async def _send_publication(
        self,
        *,
        session: Any,
        service: Any,
        status: Any,
        text: str,
        role: str,
        payload: dict[str, Any],
        actions: list[tuple[str, str]],
        force_reply: bool,
        reply_anchor_message_id: int | None = None,
    ) -> None:
        current = self._current_publication(service, str(session.session_id))
        current_payload = getattr(current, "payload", None)
        if (
            role == "owner"
            and current is not None
            and isinstance(current_payload, dict)
            and "operator_recovery_request" not in payload
            and operator_attention_payloads_equivalent(
                cast(dict[str, Any], current_payload),
                payload,
            )
        ):
            return
        route = self._route(session, role)
        generation = publication_generation(
            status,
            current=current,
            payload=payload,
        )
        render_identity = self._render_identity(
            session=session,
            generation=generation,
            route=route,
            text=text,
            role=role,
            payload=payload,
            actions=actions,
            force_reply=force_reply,
            reply_anchor_message_id=reply_anchor_message_id,
        )
        prepared_from_existing_record = False
        try:
            prepared = service.store.mark_prepared(
                session_id=session.session_id,
                generation=generation,
                payload=payload,
            )
        except ValueError:
            prepared_from_existing_record = True
            prepared = self._current_publication(service, str(session.session_id))
            if (
                prepared is None
                or getattr(prepared, "generation", None) != generation
                or getattr(prepared, "payload", None) != payload
                or getattr(prepared, "state", None) != "PREPARED"
            ):
                return
        if prepared.state in {"COMMITTED", "UNCERTAIN"}:
            return
        outbox = getattr(self, "_publication_outbox", None)
        receipt: GatewayPublicationReceipt | None = None
        dispatch = True
        if isinstance(outbox, GatewayOnboardingPublicationOutbox):
            outbox.reconcile_emergency_receipts()
            if prepared_from_existing_record:
                receipt = outbox.get(
                    session_id=session.session_id,
                    generation=generation,
                    payload=payload,
                    route=route,
                    role=role,
                    render_identity=render_identity,
                )
                if receipt is None:
                    self._mark_unknown(service, session.session_id, generation)
                    return
                dispatch = False
            else:
                receipt, dispatch = outbox.claim(
                    session_id=session.session_id,
                    generation=generation,
                    payload=payload,
                    route=route,
                    role=role,
                    render_identity=render_identity,
                )
            if not dispatch:
                if receipt.state == "RECEIPTED":
                    await self._recover_publication_receipts(
                        service,
                        session=session,
                        expected=receipt,
                    )
                elif receipt.state == "DISPATCHING":
                    self._mark_unknown(service, session.session_id, generation)
                return
        kwargs: dict[str, Any] = {"text": text}
        if force_reply:
            from telegram import ForceReply, ReplyParameters

            kwargs["reply_markup"] = ForceReply(
                selective=reply_anchor_message_id is not None,
                input_field_placeholder="답변을 입력하세요",
            )
            if reply_anchor_message_id is not None:
                kwargs["reply_parameters"] = ReplyParameters(
                    message_id=int(reply_anchor_message_id),
                )
        elif actions:
            from telegram import InlineKeyboardButton, InlineKeyboardMarkup

            kwargs["reply_markup"] = InlineKeyboardMarkup(
                [
                    [
                        InlineKeyboardButton(
                            label,
                            callback_data=encode_callback_hash(
                                action=action,
                                generation=generation,
                                sid_hash=session.sid_hash,
                            ),
                        )
                        for action, label in actions
                    ]
                ]
            )
        try:
            sent = await self.adapter._send_nutrition_topic(
                chat_id=route[0],
                topic_id=route[1],
                **kwargs,
            )
        except (OSError, RuntimeError, TypeError, ValueError):
            self._mark_unknown(service, session.session_id, generation)
            raise
        try:
            message_id = self._message_id(sent)
        except PublicationReceiptPersistenceError:
            self._mark_unknown(service, session.session_id, generation)
            raise
        if isinstance(outbox, GatewayOnboardingPublicationOutbox):
            if receipt is None:
                raise PublicationReceiptPersistenceError(
                    "onboarding publication outbox is unavailable"
                )
            try:
                outbox.record_receipt(
                    session_id=session.session_id,
                    generation=generation,
                    chat_id=route[0],
                    topic_id=route[1],
                    message_id=message_id,
                )
            except (OSError, ValueError) as primary_error:
                try:
                    outbox.record_emergency_receipt(
                        session_id=session.session_id,
                        generation=generation,
                        chat_id=route[0],
                        topic_id=route[1],
                        message_id=message_id,
                    )
                except (OSError, ValueError) as emergency_error:
                    self._mark_unknown(service, session.session_id, generation)
                    raise PublicationReceiptPersistenceError(
                        "primary and emergency publication receipt persistence failed"
                    ) from emergency_error
                try:
                    outbox.reconcile_emergency_receipts()
                except (OSError, ValueError) as merge_error:
                    raise PublicationReceiptPersistenceError(
                        "emergency publication receipt persisted but primary reconciliation failed"
                    ) from merge_error
        service.store.mark_committed(
            session_id=session.session_id,
            generation=generation,
            message_id=message_id,
        )
        if isinstance(outbox, GatewayOnboardingPublicationOutbox):
            outbox.mark_committed(
                session_id=session.session_id,
                generation=generation,
                payload=payload,
                route=route,
                role=role,
                render_identity=render_identity,
                message_id=message_id,
            )
