"""Typed state and outcomes for the profile-local check-in wizard."""

from __future__ import annotations

from dataclasses import dataclass
from enum import StrEnum
from typing import ClassVar

from pydantic import BaseModel, ConfigDict, Field, model_validator

from checkin_cli.models import SafetyReason


ADAPTIVE_FOLLOW_UP_IDS: tuple[str, ...] = (
    "Q-SLEEP-CAUSE", "Q-SLEEP-ADJUST",
    "Q-COND-SYMPTOM",
    "Q-COND-INTENSITY",
    "Q-PERF-REASON",
    "Q-PERF-NEXT",
)


class WizardFlow(StrEnum):
    """The independently finalizable wizard flows."""

    MORNING = "morning"
    WORKOUT = "workout"
    NUTRITION = "nutrition_daily"
    TRAINER_SESSION = "trainer_session"
    SCHEDULE_REFERENCE = "schedule_reference"


NUTRITION_UNKNOWN_STEPS: tuple[str, ...] = (
    "bodyweight",
    "calories",
    "macros",
    "meals",
    "water",
    "sleep_duration",
    "sleep_quality",
    "digestion",
    "condition",
    "appetite_stress",
    "training_summary",
)
NUTRITION_COMPLETION_STEPS: tuple[str, ...] = (
    *NUTRITION_UNKNOWN_STEPS,
    "optional_note",
)


class WizardBranch(StrEnum):
    """Deterministic customer or trainer outcome branch."""

    NORMAL = "normal"
    ANOMALY = "anomaly"
    CHANGE = "change"
    SAFETY_HOLD = "safety_hold"


class WizardStatus(StrEnum):
    """Observable result states for a wizard transition."""

    ADVANCED = "advanced"
    SAVED = "saved"
    INVALID = "invalid"
    REJECTED = "rejected"
    SAFETY_STOP = "safety_stop"
    DEFERRED = "deferred"


@dataclass(frozen=True, slots=True)
class WizardContext:
    """The exact Telegram identity boundary supplied by the adapter."""

    owner_id: str
    topic_id: str
    customer_key: str | None = None


class WizardSession(BaseModel):
    """Durable private draft state; it never participates in trend views."""

    model_config: ClassVar[ConfigDict] = ConfigDict(frozen=True)

    session_id: str = Field(pattern=r"^[a-f0-9]{32}$")
    flow: WizardFlow
    owner_id: str = Field(min_length=1, max_length=256)
    customer_key: str | None = Field(default=None, min_length=1, max_length=64)
    topic_id: str = Field(min_length=1, max_length=256)
    kst_day: str = Field(pattern=r"^\d{4}-\d{2}-\d{2}$")
    version: int = Field(ge=0)
    step: str = Field(min_length=1, max_length=80)
    answers: dict[str, str] = Field(default_factory=dict)
    state_schema_version: int = Field(default=1, ge=1, le=2)
    unknown_steps: tuple[str, ...] = ()
    step_history: tuple[str, ...] = ()
    safety_signals: tuple[str, ...] = ()
    safety_reasons: tuple[SafetyReason, ...] = ()
    branch: WizardBranch | None = None
    follow_up_ids: tuple[str, ...] = ()
    supersedes: str | None = Field(default=None, max_length=128)
    finalized_event_id: str | None = Field(default=None, max_length=128)
    macro_order: str = Field(
        default="protein_carbohydrate_fat",
        pattern=r"^(protein_carbohydrate_fat|carbohydrate_protein_fat)$",
    )

    @model_validator(mode="after")
    def validate_unknown_state(self) -> WizardSession:
        """Keep nutrition cursor, known answers, and explicit unknowns coherent."""
        markers = self.unknown_steps
        marker_set = set(markers)
        if len(marker_set) != len(markers):
            raise ValueError("unknown_steps must be unique")
        if self.flow is not WizardFlow.NUTRITION:
            if markers:
                raise ValueError("unknown_steps are valid only for nutrition_daily")
            return self
        if markers:
            unsupported = marker_set.difference(NUTRITION_UNKNOWN_STEPS)
            if unsupported:
                raise ValueError("unknown_steps must contain only unknown-capable nutrition fields")
            canonical_order = tuple(
                step for step in NUTRITION_UNKNOWN_STEPS if step in marker_set
            )
            if markers != canonical_order:
                raise ValueError("unknown_steps must use nutrition step order")
            if self.state_schema_version < 2:
                raise ValueError("unknown_steps require state_schema_version=2")
            if marker_set.intersection(self.answers):
                raise ValueError("nutrition fields cannot be both known and unknown")
        if self.state_schema_version < 2:
            return self
        if self.supersedes is not None or self.finalized_event_id is not None or self.safety_signals:
            return self
        history = self.step_history
        history_set = set(history)
        if len(history_set) != len(history):
            raise ValueError("step_history must be unique")
        nutrition_history = tuple(
            step for step in history if step in NUTRITION_COMPLETION_STEPS
        )
        if nutrition_history != NUTRITION_COMPLETION_STEPS[:len(nutrition_history)]:
            raise ValueError("nutrition step_history must use completion order")
        represented = set(self.answers).union(marker_set)
        if history_set.difference(represented):
            raise ValueError("completed history must be represented by known or unknown state")
        current: set[str] = (
            {self.step} if self.step in NUTRITION_COMPLETION_STEPS else set()
        )
        nutrition_state = represented.intersection(NUTRITION_COMPLETION_STEPS)
        if nutrition_state.difference(history_set.union(current)):
            raise ValueError("nutrition state cannot refer to a future or unvisited field")
        return self

    @property
    def safe_hold_reasons(self) -> tuple[SafetyReason, ...]:
        """Expose the canonical typed safety reasons under the adapter name."""
        return self.safety_reasons


@dataclass(frozen=True, slots=True)
class WizardResult:
    """Safe adapter-facing result without health values or callback payloads."""

    status: WizardStatus
    session_id: str
    version: int
    step: str
    message: str
    branch: WizardBranch | None = None
    follow_up_ids: tuple[str, ...] = ()
    safety_signals: tuple[str, ...] = ()
    safety_reasons: tuple[SafetyReason, ...] = ()
    position: int = 0
    total: int = 0
    can_previous: bool = False
    can_skip: bool = False

    @property
    def safe_hold_reasons(self) -> tuple[SafetyReason, ...]:
        """Expose the canonical typed safety reasons under the adapter name."""
        return self.safety_reasons
