"""Read-only «секционные» tool'ы чата (#957, Step 2) — срезы УЖЕ-загруженного отчёта.

LLM в tool-loop'е (см. orchestrator.py) просит секции отчёта через function-calling.
Здесь — две вещи и НИЧЕГО больше:

  1. OpenAI tool-спеки (JSON-schema) шести read-only секционных tool'ов
     (`get_exec_summary` / `get_product_recommendation` / `get_forecast` /
     `get_risks` / `get_scenarios` / `get_parcel_info`). Без параметров — каждый отдаёт
     фиксированную секцию(и) отчёта (модель не управляет вычислениями, только запрашивает
     данные).
  2. ЧИСТЫЕ executors: режут УЖЕ-ЗАГРУЖЕННЫЙ in-memory `report_dict`
     (`SiteFinderReport.as_dict()`, 8 секций). НИКАКОЙ БД, НИКАКОГО пере-расчёта,
     НИКАКОЙ движковой математики — только срез готового dict'а.

КРИТИЧНО (§16 grounding): числа берутся ВЕРБАТИМ из отчёта. Tool'ы не считают и не
выдумывают — они достают под-dict секции как есть. Модель потом только оборачивает
эти числа в RU-прозу (compose), не изобретая своих.

ROBUST: отчёт может быть тонким/частичным (каждая секция Optional). Executors НИКОГДА
не бросают KeyError — отсутствует секция → `{}` или маленький маркер «недоступно».
"""

from __future__ import annotations

from collections.abc import Callable
from typing import Any

# Маркер «секции нет в отчёте» (graceful, не KeyError). Модель по нему скажет, что
# данных нет, а не выдумает их (см. system-prompt в prompts.py).
_NOT_AVAILABLE: dict[str, Any] = {"available": False}


def _section(report: dict[str, Any], key: str) -> dict[str, Any]:
    """Достать секцию-dict по ключу или вернуть маркер «недоступно». PURE, без KeyError."""
    value = report.get(key)
    if isinstance(value, dict) and value:
        return value
    return dict(_NOT_AVAILABLE)


# ── Executors (PURE срезы готового report_dict; НЕТ БД/recompute/engine-math) ────


def get_exec_summary(report: dict[str, Any]) -> dict[str, Any]:
    """§13.1 exec_summary — вердикт-заголовок + ключевые числа + общая уверенность."""
    return _section(report, "exec_summary")


def get_product_recommendation(report: dict[str, Any]) -> dict[str, Any]:
    """§13.4 product_tz — рекомендация продукта (класс / квартирография / USP / §16)."""
    return _section(report, "product_tz")


def get_forecast(report: dict[str, Any]) -> dict[str, Any]:
    """§13.3 future_market — прогноз спроса/предложения по горизонтам + future-supply."""
    return _section(report, "future_market")


def get_risks(report: dict[str, Any]) -> dict[str, Any]:
    """Риски — §13.6 scoring (спец-индексы §25) + §13.7 confidence (уровень/факторы).

    Объединяет ДВЕ секции в один срез: риски в отчёте размазаны по scoring
    (special_indices, вкл. каннибализацию) и confidence (уровень + тянущие факторы).
    Каждая под-секция graceful: отсутствует → маркер «недоступно», без KeyError.
    """
    return {
        "scoring": _section(report, "scoring"),
        "confidence": _section(report, "confidence"),
    }


def get_scenarios(report: dict[str, Any]) -> dict[str, Any]:
    """§13.5 scenarios — сводка conservative/base/aggressive + summary."""
    return _section(report, "scenarios")


def get_parcel_info(report: dict[str, Any]) -> dict[str, Any]:
    """§1 parcel_context — паспорт участка + градрегламент ПЗЗ + ЗОУИТ.

    Курируемый срез analyze-рана (тер.зона ПЗЗ, ВРИ, лимиты застройки, ЗОУИТ), влитый
    эндпоинтом под ключ "parcel_context" (retrieval.get_parcel_context_for_chat). Секции
    в отчёте нет (analyze-рана не было) → маркер «недоступно», как у остальных tool'ов.
    """
    return _section(report, "parcel_context")


# ── Реестр имя→executor + имя→секции отчёта (для provenance grounded_in.sections) ─
# ЕДИНЫЙ источник истины: и спеки, и orchestrator берут имена/маппинг отсюда.

# Имя tool'а → (executor, какие секции отчёта он реально трогает). sections нужны для
# честного grounded_in.sections: что СПРАШИВАЛ LLM = на чём заземлён ответ.
_TOOLS: dict[str, tuple[Callable[[dict[str, Any]], dict[str, Any]], tuple[str, ...]]] = {
    "get_exec_summary": (get_exec_summary, ("exec_summary",)),
    "get_product_recommendation": (get_product_recommendation, ("product_tz",)),
    "get_forecast": (get_forecast, ("future_market",)),
    "get_risks": (get_risks, ("scoring", "confidence")),
    "get_scenarios": (get_scenarios, ("scenarios",)),
    "get_parcel_info": (get_parcel_info, ("parcel_context",)),
}

# RU-описания tool'ов для модели (что внутри секции — чтобы LLM выбирал верный tool).
_TOOL_DESCRIPTIONS: dict[str, str] = {
    "get_exec_summary": (
        "Краткое резюме отчёта: вердикт «что строить», ключевые числа, общая уверенность."
    ),
    "get_product_recommendation": (
        "Рекомендация продукта: класс объекта, квартирография (mix), коммерция, "
        "USP и §16-обоснования."
    ),
    "get_forecast": (
        "Прогноз будущего рынка: спрос/предложение по горизонтам, дефицит/затоварка, "
        "давление будущего предложения."
    ),
    "get_risks": (
        "Риски участка: специальные индексы §25 (включая каннибализацию портфеля) "
        "и уровень/факторы уверенности отчёта."
    ),
    "get_scenarios": ("Сценарии развития: разброс по консервативному / базовому / агрессивному."),
    "get_parcel_info": (
        "Паспорт участка и градостроительный регламент: адрес, площадь, категория земель, "
        "ВРИ, территориальная зона ПЗЗ (код и название), лимиты застройки, "
        "ЗОУИТ-обременения."
    ),
}


def _spec(name: str) -> dict[str, Any]:
    """Собрать одну OpenAI function-tool спеку (без параметров — секция фиксирована)."""
    return {
        "type": "function",
        "function": {
            "name": name,
            "description": _TOOL_DESCRIPTIONS[name],
            # Без аргументов: tool отдаёт фиксированную секцию, модель не управляет
            # вычислениями. additionalProperties=False — строгая пустая схема.
            "parameters": {
                "type": "object",
                "properties": {},
                "additionalProperties": False,
            },
        },
    }


def tool_specs() -> list[dict[str, Any]]:
    """Все 6 секционных tool-спек (JSON-schema) для передачи в ``complete(tools=...)``."""
    return [_spec(name) for name in _TOOLS]


def is_known_tool(name: str) -> bool:
    """Известен ли tool по имени (защита от галлюцинации имени моделью)."""
    return name in _TOOLS


def execute_tool(name: str, report: dict[str, Any]) -> dict[str, Any]:
    """Выполнить секционный executor по имени против УЖЕ-загруженного report_dict.

    PURE-срез: НИКАКОЙ БД/recompute/engine-math. Неизвестное имя → маркер «недоступно»
    (модель не уронит loop, если выдумает имя tool'а). KeyError невозможен (executors
    graceful).
    """
    entry = _TOOLS.get(name)
    if entry is None:
        return dict(_NOT_AVAILABLE)
    executor, _ = entry
    return executor(report)


def sections_for_tool(name: str) -> tuple[str, ...]:
    """Какие секции отчёта трогает tool (для grounded_in.sections). Неизвестный → ()."""
    entry = _TOOLS.get(name)
    return entry[1] if entry is not None else ()


__all__ = [
    "execute_tool",
    "get_exec_summary",
    "get_forecast",
    "get_parcel_info",
    "get_product_recommendation",
    "get_risks",
    "get_scenarios",
    "is_known_tool",
    "sections_for_tool",
    "tool_specs",
]
