"""Thin Telegram transport contracts for nutrition onboarding."""

from __future__ import annotations

import base64
import hashlib
import logging
import re
from enum import StrEnum
from typing import Self

from pydantic import BaseModel, ConfigDict, Field, model_validator


NUTRITION_ONBOARDING_API_VERSION = "2.0"
_INGRESS_LOGGER = logging.getLogger(__name__)
_INGRESS_STAGES = frozenset(
    {
        "ingress",
        "authority",
        "route",
        "validation",
        "persistence",
        "publication",
        "receipt",
    }
)
_REASON_CODE = re.compile(r"^[a-z][a-z0-9_]{0,63}$")


OPTIONAL_QUESTION_DEFAULTS: dict[str, object] = {
    "activity_rationale": "",
    "cooking_access": "",
    "budget_band": "",
    "schedule_constraints": "",
}
_ACTION = re.compile(r"^[a-z][a-z0-9_-]{0,11}$")


class TransportModel(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)


class CallbackToken(TransportModel):
    action: str
    generation: int = Field(ge=0)
    session_hash: str = Field(pattern=r"^[A-Za-z0-9_-]{43}$")


def encode_callback(*, action: str, generation: int, session_id: str) -> str:
    digest = hashlib.sha256(session_id.encode("utf-8")).hexdigest()
    return encode_callback_hash(
        action=action,
        generation=generation,
        sid_hash=digest,
    )


def encode_callback_hash(*, action: str, generation: int, sid_hash: str) -> str:
    if not _ACTION.fullmatch(action):
        raise ValueError("invalid onboarding callback action")
    if generation < 0:
        raise ValueError("invalid onboarding callback generation")
    if not re.fullmatch(r"[0-9a-f]{64}", sid_hash):
        raise ValueError("invalid onboarding callback session hash")
    digest = bytes.fromhex(sid_hash)
    session_hash = base64.urlsafe_b64encode(digest).decode("ascii").rstrip("=")
    callback = f"non2:{action}:{generation}:{session_hash}"
    if len(callback.encode("utf-8")) > 64:
        raise ValueError("onboarding callback exceeds Telegram limit")
    return callback


def decode_callback(value: str) -> CallbackToken:
    parts = value.split(":")
    if len(parts) == 4 and parts[0] == "non1":
        raise ValueError("historical onboarding callback is expired")
    if len(parts) != 4 or parts[0] != "non2" or not _ACTION.fullmatch(parts[1]):
        raise ValueError("invalid onboarding callback")
    try:
        generation = int(parts[2])
    except ValueError as exc:
        raise ValueError("invalid onboarding callback generation") from exc
    return CallbackToken(
        action=parts[1],
        generation=generation,
        session_hash=parts[3],
    )


def log_ingress_stage(
    stage: str,
    update_id: object,
    *,
    reason_code: str | None = None,
    logger: logging.Logger = _INGRESS_LOGGER,
) -> None:
    """Emit the bounded, content-free audit record for nutrition ingress."""
    if stage not in _INGRESS_STAGES:
        raise ValueError("invalid nutrition ingress stage")
    safe_update_id = (
        str(update_id)
        if type(update_id) is int and update_id >= 0
        else "unknown"
    )
    if reason_code is None:
        logger.info(
            "nutrition_ingress stage=%s update_id=%s",
            stage,
            safe_update_id,
        )
        return
    if not _REASON_CODE.fullmatch(reason_code):
        raise ValueError("invalid nutrition ingress reason code")
    logger.info(
        "nutrition_ingress stage=%s update_id=%s reason_code=%s",
        stage,
        safe_update_id,
        reason_code,
    )


class IngressEvidence(TransportModel):
    actor_user_id: int
    chat_id: int
    topic_id: int
    message_id: int
    generation: int = Field(ge=0)
    action: str
    member_present: bool


class IngressDecision(TransportModel):
    consumed: bool
    mutate: bool
    fallthrough: bool = False


def authorize_ingress(
    *,
    expected: IngressEvidence,
    actual: IngressEvidence,
) -> IngressDecision:
    exact = expected == actual and actual.member_present
    return IngressDecision(consumed=True, mutate=exact, fallthrough=False)


def consume_unknown_callback(_: str) -> IngressDecision:
    return IngressDecision(consumed=True, mutate=False, fallthrough=False)


class PublicationState(StrEnum):
    PREPARED = "PREPARED"
    COMMITTED = "COMMITTED"
    UNCERTAIN = "UNCERTAIN"


class PublicationAttempt(TransportModel):
    generation: int = Field(ge=0)
    body_digest: str = Field(pattern=r"^[0-9a-f]{64}$")
    button_digest: str = Field(pattern=r"^[0-9a-f]{64}$")
    state: PublicationState
    message_id: int | None = None

    @classmethod
    def prepare(
        cls,
        *,
        generation: int,
        body_digest: str,
        button_digest: str,
    ) -> Self:
        return cls(
            generation=generation,
            body_digest=body_digest,
            button_digest=button_digest,
            state=PublicationState.PREPARED,
        )

    @model_validator(mode="after")
    def validate_message_id(self) -> Self:
        if self.state is PublicationState.COMMITTED and self.message_id is None:
            raise ValueError("committed publication requires message ID")
        if self.state is not PublicationState.COMMITTED and self.message_id is not None:
            raise ValueError("only committed publication may contain message ID")
        return self

    @property
    def requires_send(self) -> bool:
        return self.state is PublicationState.PREPARED

    def commit(self, *, message_id: int) -> Self:
        if self.state is not PublicationState.PREPARED:
            raise ValueError("only prepared publication can commit")
        return self.model_copy(
            update={"state": PublicationState.COMMITTED, "message_id": message_id}
        )

    def mark_uncertain(self) -> Self:
        if self.state is not PublicationState.PREPARED:
            raise ValueError("only prepared publication can become uncertain")
        return self.model_copy(update={"state": PublicationState.UNCERTAIN})

    def retry(self, *, no_side_effect_evidence: bool = False) -> Self:
        if self.state is not PublicationState.UNCERTAIN or not no_side_effect_evidence:
            raise ValueError("retry requires positive no-side-effect evidence")
        return self.model_copy(update={"state": PublicationState.PREPARED})
