"""Strict, privacy-minimizing value models for nutrition onboarding."""

from __future__ import annotations

import re
import unicodedata
from datetime import date
from decimal import Decimal
from enum import StrEnum
from typing import Any, Self

from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator, model_validator


NUTRITION_ONBOARDING_API_VERSION = "2.0"


class OnboardingSessionStatus(StrEnum):
    IN_PROGRESS = "in_progress"
    COMPLETED = "completed"
    CANCELLED = "cancelled"


class OnboardingState(StrEnum):
    COLLECTING = "collecting"
    CUSTOMER_ATTESTATION = "customer_attestation"
    RECONCILING = "reconciling"
    SAFETY_HOLD = "safety_hold"
    OWNER_REVIEW = "owner_review"
    FINALIZING = "finalizing"
    READY = "ready"
    REJECTED = "rejected"
    CANCELLED = "cancelled"
    EXPIRED = "expired"
    CONSENT_REVOKED = "consent_revoked"


class ReviewDecision(StrEnum):
    PENDING = "pending"
    APPROVED = "approved"
    REJECTED = "rejected"


class PublicationStatus(StrEnum):
    UNPUBLISHED = "unpublished"
    PUBLISHED = "published"
    WITHDRAWN = "withdrawn"


class EquationSexBasis(StrEnum):
    MALE = "male"
    FEMALE = "female"
    DECLINE = "decline"


class ActivityCategory(StrEnum):
    SEDENTARY = "sedentary"
    LIGHT = "light"
    MODERATE = "moderate"
    VERY_ACTIVE = "very_active"
    EXTRA_ACTIVE = "extra_active"


class GoalType(StrEnum):
    LOSS = "loss"
    MAINTAIN = "maintain"
    GAIN = "gain"


class StructuredItemStatus(StrEnum):
    NONE = "none"
    PROVIDED = "provided"


def _normalize_text(value: str) -> str:
    return re.sub(r"\s+", " ", unicodedata.normalize("NFC", value)).strip()


_EQUATION_SEX_BASIS_ANSWERS = {
    "male": "male",
    "female": "female",
    "decline": "decline",
    "남성": "male",
    "남성입니다": "male",
    "여성": "female",
    "여성입니다": "female",
    "선택 안 함": "decline",
    "선택 안 합니다": "decline",
    "선택하지 않겠습니다": "decline",
    "네 남성입니다": "male",
    "네, 남성입니다": "male",
    "넵 남성입니다": "male",
    "넵, 남성입니다": "male",
    "예 남성입니다": "male",
    "예, 남성입니다": "male",
    "네 여성입니다": "female",
    "네, 여성입니다": "female",
    "넵 여성입니다": "female",
    "넵, 여성입니다": "female",
    "예 여성입니다": "female",
    "예, 여성입니다": "female",
    "yes male": "male",
    "yes, male": "male",
    "yep male": "male",
    "yep, male": "male",
    "yes female": "female",
    "yes, female": "female",
    "yep female": "female",
    "yep, female": "female",
    "i prefer not to say": "decline",
    "prefer not to say": "decline",
}


def canonical_equation_sex_basis(value: object) -> str | None:
    """Return an enum value only for an exact supported UI answer."""
    if not isinstance(value, str):
        return None
    return _EQUATION_SEX_BASIS_ANSWERS.get(_normalize_text(value).casefold())


def _canonical_decimal(value: Decimal) -> str:
    rendered = format(value.normalize(), "f")
    if "." in rendered:
        rendered = rendered.rstrip("0").rstrip(".")
    return rendered or "0"


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


class StructuredItems(StrictFrozenModel):
    status: StructuredItemStatus
    items: tuple[str, ...]

    @field_validator("items", mode="before")
    @classmethod
    def normalize_items(cls, value: Any) -> tuple[str, ...]:
        if not isinstance(value, (list, tuple)):
            raise ValueError("items must be a sequence")
        result: list[str] = []
        seen: set[str] = set()
        for raw in value:
            if not isinstance(raw, str):
                raise ValueError("structured items must be text")
            item = _normalize_text(raw)
            if not item or len(item) > 200:
                raise ValueError("structured item text is out of bounds")
            key = item.casefold()
            if key not in seen:
                seen.add(key)
                result.append(item)
        if len(result) > 40:
            raise ValueError("too many structured items")
        return tuple(result)

    @model_validator(mode="after")
    def validate_status(self) -> Self:
        if self.status is StructuredItemStatus.NONE and self.items:
            raise ValueError("none status cannot contain items")
        if self.status is StructuredItemStatus.PROVIDED and not self.items:
            raise ValueError("provided status requires items")
        return self


class NutritionOnboardingBaseline(StrictFrozenModel):
    schema_version: str = Field(pattern=r"^1\.0$")
    customer_key: str = Field(min_length=1, max_length=80)
    adult_age: int = Field(ge=18, le=120)
    equation_sex_basis: EquationSexBasis
    height_cm: Decimal = Field(ge=Decimal("120"), le=Decimal("250"))
    weight_kg: Decimal = Field(ge=Decimal("35"), le=Decimal("300"))
    activity_category: ActivityCategory
    activity_rationale: str = ""
    goal_type: GoalType
    target_weight_kg: Decimal | None = Field(default=None, ge=Decimal("35"), le=Decimal("300"))
    target_date: date | None = None
    dietary_preferences: StructuredItems
    disliked_foods: StructuredItems
    allergies: StructuredItems
    intolerances: StructuredItems
    religious_ethical_exclusions: StructuredItems
    conditions: StructuredItems
    medications: StructuredItems
    pregnancy_breastfeeding: bool | None = None
    eating_disorder_risk: bool | None = None
    cooking_access: str = ""
    budget_band: str = ""
    meal_count: int = Field(default=3, ge=2, le=6)
    schedule_constraints: str = ""
    session_status: OnboardingSessionStatus
    review_decision: ReviewDecision
    publication_status: PublicationStatus
    source_value_digest: str | None = Field(default=None, pattern=r"^[0-9a-f]{64}$")
    reference_date: date | None = None

    @field_validator(
        "activity_rationale",
        "cooking_access",
        "budget_band",
        "schedule_constraints",
        mode="before",
    )
    @classmethod
    def normalize_free_text(cls, value: Any) -> str:
        if value is None:
            return ""
        if not isinstance(value, str):
            raise ValueError("free-text values must be strings")
        normalized = _normalize_text(value)
        if len(normalized) > 500:
            raise ValueError("free-text value is too long")
        return normalized

    @model_validator(mode="after")
    def validate_goal(self) -> Self:
        has_target = self.target_weight_kg is not None or self.target_date is not None
        if self.goal_type is GoalType.MAINTAIN and has_target:
            raise ValueError("maintain goal cannot specify a target")
        if self.goal_type is not GoalType.MAINTAIN and (
            self.target_weight_kg is None or self.target_date is None
        ):
            raise ValueError("target weight and date are required")
        return self

    @field_serializer("height_cm", "weight_kg", "target_weight_kg")
    def serialize_decimals(self, value: Decimal | None) -> str | None:
        return None if value is None else _canonical_decimal(value)


def derive_adult_age(date_of_birth: date, *, as_of: date) -> int:
    age = as_of.year - date_of_birth.year - (
        (as_of.month, as_of.day) < (date_of_birth.month, date_of_birth.day)
    )
    if not 18 <= age <= 120:
        raise ValueError("adult age must be between 18 and 120")
    return age


def build_onboarding_baseline(
    raw_answers: dict[str, object],
    *,
    as_of: date,
) -> NutritionOnboardingBaseline:
    import hashlib
    import json

    payload = dict(raw_answers)
    raw_dob = payload.pop("date_of_birth", None)
    if not isinstance(raw_dob, str):
        raise ValueError("date_of_birth is required")
    date_of_birth = date.fromisoformat(raw_dob)
    payload["adult_age"] = derive_adult_age(date_of_birth, as_of=as_of)
    payload["reference_date"] = as_of
    payload["source_value_digest"] = hashlib.sha256(
        json.dumps(raw_answers, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()
    ).hexdigest()
    return NutritionOnboardingBaseline.model_validate(payload)
