"""Pydantic v2 контракты чата по §22-форсайту участка (#957, Step 1 — БЕЗ LLM).

ДЕТЕРМИНИРОВАННЫЙ слой: эндпоинт `POST /api/v1/chat/ask` читает УЖЕ-ПЕРСИСТЕНТНЫЙ
SiteFinderReport участка (`as_dict()`, 8 секций) и отдаёт шаблонный RU-ответ. LLM-
композиция — Step 2 (#957), поэтому `llm_used` ВСЕГДА False, а `history` принимается,
но в Step 1 НЕ используется (форвардится в LLM только на Step 2; длину кэпируем здесь,
чтобы контракт был стабилен и не раздувался).

advisory: зеркалит `report.advisory` — отчёт советующий, не основание для инвест-решения,
поэтому каждый ответ несёт advisory-оговорку (см. services/chat/intents.py).
"""

from __future__ import annotations

from enum import StrEnum
from typing import Literal

from pydantic import BaseModel, ConfigDict, Field, field_validator

# История диалога принимается, но в Step 1 НЕ используется (LLM-контекст — Step 2).
# Кэпируем длину, чтобы payload не раздувался до подключения LLM.
_HISTORY_MAX_TURNS = 20


class ChatIntent(StrEnum):
    """Поддерживаемые типы вопросов по отчёту (детерминированный роутинг §22-чата).

    StrEnum → значение члена и есть строка (сериализуется как plain-строка в JSON-
    ответе, сравнимо со строкой). `unknown` — graceful-фолбэк: вопрос не распознан,
    отдаём меню поддерживаемых тем.
    """

    summary = "summary"  # что строить здесь / общий вердикт (exec_summary)
    what_to_build = "what_to_build"  # рекомендация продукта (product_tz)
    why_forecast = "why_forecast"  # почему такой прогноз (future_market + reasons)
    risks = "risks"  # риски / спец-индексы §25 + уверенность
    scenarios = "scenarios"  # разброс сценариев (conservative/base/aggressive)
    unknown = "unknown"  # вопрос не распознан → меню тем


class ChatTurn(BaseModel):
    """Один ход диалога (роль + текст). Принимается, но в Step 1 НЕ используется.

    Форвардится в LLM-контекст только на Step 2 (#957). frozen — это входной факт.
    """

    model_config = ConfigDict(frozen=True)

    role: str = Field(description="Роль хода: 'user' | 'assistant'")
    content: str = Field(description="Текст хода диалога")


class GroundedIn(BaseModel):
    """Привязка ответа к источнику — какой ран/схема/секции легли в основу ответа.

    Делает ответ explainable: фронт может показать «основано на ране N, секции X, Y».
    None в ответе, когда отчёта ещё нет (report_status='pending').
    """

    model_config = ConfigDict(frozen=True)

    run_id: int = Field(description="ID персистентного analysis_run, давшего отчёт")
    schema_version: str = Field(description="Версия схемы отчёта (контракт as_dict)")
    sections: list[str] = Field(
        default_factory=list,
        description="Секции отчёта, использованные для ответа (explainability)",
    )


class ChatAskRequest(BaseModel):
    """Запрос вопроса по §22-форсайту участка.

    `cad_num` + `message` обязательны. `intent` — опциональный явный intent (если задан,
    роутер его уважает и не угадывает по тексту). `history` — опциональная история
    диалога: принимается для совместимости контракта, но в Step 1 НЕ используется
    (LLM-контекст — Step 2); длину кэпируем (последние _HISTORY_MAX_TURNS ходов).
    """

    cad_num: str = Field(min_length=1, description="Кадастровый номер участка")
    message: str = Field(min_length=1, description="Вопрос пользователя (RU, free-form)")
    intent: ChatIntent | None = Field(
        default=None,
        description="Явный intent — если задан, роутер его уважает (не угадывает по тексту)",
    )
    history: list[ChatTurn] | None = Field(
        default=None,
        description="История диалога (Step 1: принимается, НЕ используется; LLM-контекст — Step 2)",
    )

    @field_validator("history")
    @classmethod
    def _cap_history(cls, v: list[ChatTurn] | None) -> list[ChatTurn] | None:
        """Graceful-усечение: кэпируем хвост до последних _HISTORY_MAX_TURNS ходов.

        НЕ отклоняем длинный диалог 422 (контракт обещает усечение, не отказ) —
        оставляем самые свежие ходы, отбрасывая старые с головы.
        """
        if v is not None and len(v) > _HISTORY_MAX_TURNS:
            return v[-_HISTORY_MAX_TURNS:]
        return v


class ChatAskResponse(BaseModel):
    """Ответ чата по отчёту участка (детерминированный, шаблонный RU-текст).

    `answer` — готовый RU-ответ с числами ВЕРБАТИМ из отчёта (никогда не фабрикуем) +
    advisory-оговорка. `grounded_in` — привязка к рану/секциям (None если pending).
    `llm_used` ВСЕГДА False в Step 1. `fallback_reason` — почему ответ деградировал
    (отчёта нет / intent не распознан / секция отсутствует). `advisory` зеркалит
    report.advisory (пока всегда True). `report_status` — 'ready' (отчёт прочитан) или
    'pending' (отчёта ещё нет — запустите анализ участка).
    """

    answer: str = Field(description="Готовый RU-ответ (числа из отчёта + advisory-оговорка)")
    grounded_in: GroundedIn | None = Field(
        default=None,
        description="Привязка к рану/схеме/секциям (None если отчёта ещё нет)",
    )
    llm_used: bool = Field(
        default=False,
        description="Использовался ли LLM (Step 1 — ВСЕГДА False; LLM — Step 2)",
    )
    fallback_reason: str | None = Field(
        default=None,
        description="Почему ответ деградировал (нет отчёта / intent unknown / нет секции)",
    )
    advisory: bool = Field(
        default=True,
        description="Отчёт советующий (зеркало report.advisory — пока всегда True)",
    )
    report_status: Literal["ready", "pending"] = Field(
        description="Статус отчёта: 'ready' (прочитан) | 'pending' (ещё нет)",
    )
