"""Чат по §22-форсайту участка — `POST /api/v1/chat/ask` (#957, Step 1 + Step 2 LLM).

Stateless-эндпоинт: читает УЖЕ-ПЕРСИСТЕНТНЫЙ SiteFinderReport участка и отдаёт RU-ответ.
Две ветки по `settings.llm_enabled`:
  • False (дефолт) → ДЕТЕРМИНИРОВАННЫЙ Step-1 путь (route_intent → render_answer),
    llm_used=False. В проде до настройки секретов сеть НЕ дёргается.
  • True → Step-2 LLM tool-loop (orchestrate_chat) поверх того же отчёта; при ЛЮБОМ
    сбое LLM оркестратор сам деградирует в Step-1 ответ (llm_used=False + fallback_reason).

Поток:
  get_report_for_chat → отчёта нет (None) → report_status='pending' + детерминированный
  RU-ответ «запустите анализ участка» (READ-ONLY: НЕ enqueue'им расчёт); иначе ветка по
  llm_enabled → ChatAskResponse(report_status='ready', grounded_in).

Sync↔async мост: ядро LLM (`complete`) синхронное (httpx.Client) — async-хендлер мостит
через `run_in_threadpool` (НЕ делаем async Celery/блокирующий вызов в event-loop'е).

RBAC: смонтировано под /api/v1/chat (НЕ /admin) → middleware rbac_guard (app/main.py)
АВТОМАТИЧЕСКИ требует аутентифицированного известного юзера (X-Authenticated-User из
Caddy). Доп. guard-код тут НЕ нужен. Сессия БД — синхронная (`Depends(get_db)`, как
get_parcel_forecast); хендлер async def (FastAPI house-style).
"""

from __future__ import annotations

import logging
from typing import Annotated, Any

from fastapi import APIRouter, Depends
from fastapi.concurrency import run_in_threadpool
from sqlalchemy.orm import Session

from app.core.config import settings
from app.core.db import get_db
from app.schemas.chat import ChatAskRequest, ChatAskResponse, ChatIntent, GroundedIn
from app.services.chat.intents import render_answer, route_intent
from app.services.chat.orchestrator import orchestrate_chat
from app.services.chat.retrieval import (
    _FORECAST_SCHEMA_VERSION,
    get_parcel_context_for_chat,
    get_report_for_chat,
)

logger = logging.getLogger(__name__)

router = APIRouter()

# Детерминированный RU-ответ, когда §22-отчёта ещё нет (READ-ONLY: чат не триггерит
# расчёт — его запускает POST /analyze).
_PENDING_ANSWER = (
    "Отчёт по этому участку ещё не готов. Запустите анализ участка — после расчёта "
    "форсайта я смогу ответить на вопросы по нему."
)


@router.post("/ask", response_model=ChatAskResponse)
async def ask(
    payload: ChatAskRequest,
    db: Annotated[Session, Depends(get_db)],
) -> ChatAskResponse:
    """Ответить на вопрос по §22-форсайту участка.

    Отчёта нет → 200 + report_status='pending' + RU-подсказка «запустите анализ»
    (READ-ONLY, ничего не считаем). Иначе ветка по `settings.llm_enabled`:
      • False → детерминированный Step-1 ответ (intent → render_answer), llm_used=False;
      • True → LLM tool-loop (orchestrate_chat через run_in_threadpool); при сбое LLM
        оркестратор сам отдаёт детерминированный ответ (llm_used=False + fallback_reason).
    """
    try:
        # Sync SQLAlchemy чтение (тянет крупный JSONB §22-отчёт) — мостим через
        # run_in_threadpool, чтобы НЕ блокировать event loop (тот же приём, что и
        # синхронный LLM `complete` в _answer_via_llm ниже).
        report, run_id = await run_in_threadpool(get_report_for_chat, db, payload.cad_num)
    except Exception:
        # Read-only сбой БД — не валим клиента 500-кой, отдаём pending (как
        # get_parcel_forecast). Клиент может повторить.
        logger.warning(
            "chat: report read failed for cad=%s — returning pending",
            payload.cad_num,
            exc_info=True,
        )
        return ChatAskResponse(
            answer=_PENDING_ANSWER,
            grounded_in=None,
            llm_used=False,
            fallback_reason="report_unavailable",
            report_status="pending",
        )

    if report is None or run_id is None:
        return ChatAskResponse(
            answer=_PENDING_ANSWER,
            grounded_in=None,
            llm_used=False,
            fallback_reason="report_pending",
            report_status="pending",
        )

    # Курируемый паспорт участка + градрегламент (§1 analyze-рана) — отдельный read-only
    # seam. §22-отчёт (форсайт) НЕ несёт тер.зону/ЗОУИТ/ЕГРН, поэтому дотягиваем их из
    # analyze-1.0 и вливаем в КОПИЮ report_dict под ключ "parcel_context" (tool
    # get_parcel_info режет именно его). Analyze-рана нет/сбой чтения → работаем как
    # раньше (только форсайт); НЕ меняем pending-поведение (оно завязано на §22-ран выше).
    report = await _with_parcel_context(db, payload.cad_num, report)

    if settings.llm_enabled:
        return await _answer_via_llm(db, payload, report, run_id)
    return _answer_deterministic(payload, report, run_id)


async def _with_parcel_context(
    db: Session,
    cad_num: str,
    report: dict[str, Any],
) -> dict[str, Any]:
    """Дотянуть курируемый паспорт участка и влить его в КОПИЮ report_dict.

    Read-only: sync-чтение analyze-рана мостим через run_in_threadpool (как §22-отчёт).
    None (рана нет) → возвращаем report без изменений. Сбой БД глотаем в pending-стиле
    эндпоинта: паспорт участка — обогащение, его отсутствие не должно ронять чат.
    """
    try:
        parcel_context = await run_in_threadpool(get_parcel_context_for_chat, db, cad_num)
    except Exception:
        logger.warning(
            "chat: parcel context read failed for cad=%s — continuing without it",
            cad_num,
            exc_info=True,
        )
        return report
    if not parcel_context:
        return report
    # Копия: не мутируем report_dict, пришедший из get_report_for_chat.
    merged = dict(report)
    merged["parcel_context"] = parcel_context
    return merged


def _answer_deterministic(
    payload: ChatAskRequest,
    report: dict[str, Any],
    run_id: int,
) -> ChatAskResponse:
    """Step-1 детерминированный ответ: intent → шаблонный RU-текст (числа из отчёта)."""
    intent = route_intent(payload.message, payload.intent)
    answer, sections = render_answer(intent, report)
    # fallback_reason: помечаем, когда intent не распознан (отдали меню тем) — для
    # аналитики (зеркалит поведение Step-1).
    fallback_reason = "intent_unknown" if intent is ChatIntent.unknown else None
    return ChatAskResponse(
        answer=answer,
        grounded_in=GroundedIn(
            run_id=run_id,
            schema_version=_FORECAST_SCHEMA_VERSION,
            sections=sections,
        ),
        llm_used=False,
        fallback_reason=fallback_reason,
        report_status="ready",
    )


async def _answer_via_llm(
    db: Session,
    payload: ChatAskRequest,
    report: dict[str, Any],
    run_id: int,
) -> ChatAskResponse:
    """Step-2 LLM tool-loop. Синхронный `complete` мостится через run_in_threadpool.

    Оркестратор инкапсулирует деградацию: ЛЮБОЙ сбой LLM → детерминированный ответ
    (llm_used=False + fallback_reason). Здесь просто переносим его поля в HTTP-контракт.
    """
    result = await run_in_threadpool(
        orchestrate_chat,
        db,
        payload.cad_num,
        payload.message,
        payload.history,
        report,
        run_id,
    )
    return ChatAskResponse(
        answer=result.answer,
        grounded_in=GroundedIn(
            run_id=run_id,
            schema_version=_FORECAST_SCHEMA_VERSION,
            sections=result.sections,
        ),
        llm_used=result.llm_used,
        fallback_reason=result.fallback_reason,
        report_status="ready",
    )
