"""§19 redaction boundary для чата (#957, Step 2/3) — ЕДИНСТВЕННАЯ точка сборки
исходящего ``SafePayload``.

═══════════════════════════════════════════════════════════════════════════════
КОНТРАКТ (читать ОБЯЗАТЕЛЬНО перед изменением) — allowlist-first, §19 data-residency
═══════════════════════════════════════════════════════════════════════════════

Провайдер LLM ВНЕШНИЙ (OpenAI) → всё, что попадёт в этот ``SafePayload``, ПОКИНЕТ РФ.
Поэтому сюда разрешено передавать ТОЛЬКО курируемые агрегаты секций отчёта
(``SiteFinderReport.as_dict()``), которые вызывающий уже отобрал секционными
tool'ами (см. tools.py). ЭТО ЕДИНСТВЕННОЕ место, через которое данные чата уходят
во внешнюю модель — поэтому и аллоулист здесь.

РАЗРЕШЕНО в ``section_data`` / ``fields``:
  • срезы секций отчёта из tools.py (exec_summary / product_tz / future_market /
    scoring / confidence / scenarios) — это посчитанные advisory-агрегаты.
  • ``parcel_context`` — КУРИРУЕМЫЙ срез analyze-рана (retrieval.get_parcel_context_for_chat):
    публичные градоданные участка (адрес, площадь, категория, ВРИ, тер.зона ПЗЗ + лимиты
    застройки, ЗОУИТ-сводка + названия зон). Строится по фиксированному whitelist'у из
    скаляров и списка строк — БЕЗ геометрии/координат, без сырых под-словарей, без PII.

ЗАПРЕЩЕНО (НИКОГДА не должно сюда дойти):
  • сырой ``analyze``-blob целиком / сырые строки БД (rosreestr_deals, parcels, …);
  • геометрия/координаты участка или ЗОУИТ-пересечений;
  • свободный insight-текст / лиды / любой контент с PII;
  • любой источник, помеченный confidential.

Вторичная защита (belt-and-suspenders): ``complete`` всё равно прогонит ``text`` и
строковые значения ``fields`` через regex-scrub PII (redaction.py). Но это НЕ замена
аллоулиста — основная гарантия в том, ЧТО сюда передаёт вызывающий.

``is_confidential``: прокидывается в ``SafePayload`` как есть. Для MVP отчёт — это
не-чувствительные агрегаты → False. Но параметр СУЩЕСТВУЕТ и ЧЕСТНО соблюдается: если
когда-нибудь источник окажется confidential, ``complete`` для внешнего провайдера
поднимет ``RedactionRefusedError`` → чат деградирует в детерминированный fallback
(hard-block, данные наружу НЕ уходят).
"""

from __future__ import annotations

import logging
from typing import Any

from app.services.llm import SafePayload

logger = logging.getLogger(__name__)


def build_chat_payload(
    question: str,
    section_data: dict[str, Any],
    *,
    is_confidential: bool = False,
) -> SafePayload:
    """Собрать исходящий ``SafePayload`` чата из вопроса + КУРИРУЕМЫХ срезов секций.

    ЕДИНСТВЕННАЯ точка сборки исходящей нагрузки чата (§19-гейт). Вызывающий ОБЯЗАН
    передавать в ``section_data`` ТОЛЬКО агрегаты секций отчёта (срезы из tools.py) —
    НИКОГДА сырой ``analyze``-blob, сырые строки БД или insight-текст (см. модульный
    docstring). Эта функция ничего не «достаёт» и не ходит в БД — она лишь упаковывает
    то, что ей дали, в ``SafePayload`` (который ``complete`` затем проскрабит/заблокирует).

    Args:
        question: вопрос пользователя (RU). Уйдёт в ``text`` и будет проскраблен от PII
            внутри ``complete`` перед отправкой (вторичная защита).
        section_data: накопленные срезы секций отчёта (имя tool'а → под-dict секции).
            Аллоулист: только вывод секционных executors из tools.py.
        is_confidential: флаг конфиденциальности источника. MVP: отчёт = агрегаты → False.
            True ⇒ ``complete`` для внешнего провайдера поднимет RedactionRefusedError
            (hard-block) → вызывающий деградирует в детерминированный fallback.

    Returns:
        ``SafePayload`` с ``text``=вопрос, ``fields``=section_data, флагом confidential.
    """
    # НЕ логируем содержимое вопроса/секций (PII-гигиена) — только факт и размер.
    logger.debug(
        "chat: building safe payload (sections=%d, confidential=%s)",
        len(section_data),
        is_confidential,
    )
    return SafePayload(
        text=question,
        # dict(...) — копия: payload не должен держать ссылку на изменяемый аккумулятор
        # вызывающего (loop дополняет section_data между итерациями).
        fields=dict(section_data),
        is_confidential=is_confidential,
    )


__all__ = ["build_chat_payload"]
