"""§22 PPTX-экспортёр итогового советующего отчёта Site Finder v2 (python-pptx).

#959 (EPIC export) — ПОСЛЕДНИЙ формат набора выгрузки §22-форсайта (md/json/tg/docx
уже есть, этот завершает EPIC): рендерит `SiteFinderReport` (#987) в презентацию
(.pptx) и возвращает БАЙТЫ — готовые для
`Response(media_type="application/vnd.openxmlformats-officedocument.presentationml.presentation")`.
Аддитивный двойник DOCX/PDF/Markdown-экспортёров (`report_docx` / `report_pdf` /
`report_md`): то же содержание, НО presentation-форма — это колода слайдов, а не
документ, поэтому секции СЖАТЫ до ключевого (титул + 6 содержательных слайдов), без
длинных таблиц на каждый под-блок.

DRY: PURE-хелперы нормализации/форматирования НЕ дублируем — импортируем из
`report_pdf` (там они уже есть: `_normalize` — приём инстанса ИЛИ `as_dict()`-словаря,
`_fmt` — строковое форматирование значения/None→"—", `_level_ru`, `_as_dict`/`_as_list`,
`_scenario_deficit_cell`) + переиспользуем его named-константы
(`_DASH`/`_NO_DATA`/`_ADVISORY_MARKER`). Здесь добавлены ТОЛЬКО pptx-специфичные
микро-билдеры (слайды/буллеты/таблица python-pptx).

ДЕТЕРМИНИРОВАННЫЙ, БЕЗ LLM, БЕЗ БД/сети: только ПОТРЕБЛЯЕТ уже-собранный отчёт (его
наполняет сборщик #988) и раскладывает по слайдам. Принимает КАК `SiteFinderReport`-
инстанс, ТАК и его `as_dict()`-словарь (нормализуется через `_normalize`).

GRACEFUL (дух всего форсайт-стека): частичный/пустой/мусорный отчёт ВАЛИДЕН — пустой
блок рисует «нет данных», экспортёр НИКОГДА не падает (нет KeyError на тонком отчёте)
и возвращает минимальную валидную .pptx. Отчёт СОВЕТУЮЩИЙ: на титульном слайде —
заметный advisory-дисклеймер (оценка не основание для инвест-решения).

`python-pptx` (PyPI `python-pptx`, import `pptx`) — чистый Python (lxml/Pillow), без
нативных системных библиотек: Dockerfile не трогаем. Импортируется ЛОКАЛЬНО внутри
функции (зеркало python-docx в `report_docx` / WeasyPrint в `report_pdf` — не нужен при
импорте модуля).
"""

from __future__ import annotations

import io
import logging
from typing import TYPE_CHECKING, Any

from app.services.exporters.report_pdf import (
    _ADVISORY_MARKER,
    _DASH,
    _NO_DATA,
    _as_dict,
    _as_list,
    _fmt,
    _level_ru,
    _normalize,
    _scenario_deficit_cell,
)

if TYPE_CHECKING:
    # Только для аннотаций: тяжёлый python-pptx импортируем ЛОКАЛЬНО в render_report_pptx
    # (как python-docx в report_docx), чтобы импорт модуля был дешёвым и не требовал dep.
    from pptx.presentation import Presentation as _PptxPresentation
    from pptx.slide import Slide as _PptxSlide

logger = logging.getLogger(__name__)

# ── Named-константы: заголовки слайдов (зеркало report_docx — те же секции §22) ──

_TITLE_DECK: str = "Site Finder v2 — прогноз"
_TITLE_SUMMARY: str = "Сводка"
_TITLE_FUTURE_MARKET: str = "Будущий рынок"
_TITLE_SCENARIOS: str = "Сценарии"
_TITLE_PRODUCT_TZ: str = "Продукт ТЗ"
_TITLE_CONFIDENCE: str = "Уверенность"

# Краткий RU-дисклеймер (формулировка из ТЗ #959) — в подзаголовке титульного слайда.
_ADVISORY_DISCLAIMER: str = "Оценка advisory — не основание для инвест-решения."

# Индексы дефолтных layout'ов встроенного шаблона python-pptx (стабильны):
#   0 — «Title Slide»   (placeholder idx 0=title, 1=subtitle)
#   1 — «Title and Content» (placeholder idx 0=title, 1=body — буллеты)
#   5 — «Title Only»    (placeholder idx 0=title — под кастомную таблицу/текст)
_LAYOUT_TITLE: int = 0
_LAYOUT_TITLE_CONTENT: int = 1
_LAYOUT_TITLE_ONLY: int = 5

# Основной продуктовый горизонт (мес) — подпись сводного дефицита сценария
# (зеркало report_docx._PRIMARY_HORIZON_MONTHS; значение тянет _scenario_deficit_cell —
# при fallback на чужой горизонт буллет несёт «(гор. N мес)», #1590).
_PRIMARY_HORIZON_MONTHS: int = 12

# Сколько USP-ниш выводить буллетами на слайд «Продукт ТЗ» (колода — держим компактно).
_USP_TOP_N: int = 4
# Сколько факторов уверенности выводить буллетами на слайд «Уверенность».
_FACTORS_TOP_N: int = 6


# ──────────────────────────────────────────────────────────────────────────────
# pptx-специфичные микро-билдеры. Принимают `prs`/`slide` python-pptx и пишут в них
# (мутируют презентацию). Хелперы нормализации/форматирования — импортированы из
# report_pdf, не дублируются (DRY). Числа/None уже причёсаны `_fmt` → str.
# ──────────────────────────────────────────────────────────────────────────────


def _add_title_slide(prs: _PptxPresentation, title: str, subtitle: str) -> _PptxSlide:
    """Титульный слайд (layout «Title Slide»): заголовок + подзаголовок. Graceful."""
    slide = prs.slides.add_slide(prs.slide_layouts[_LAYOUT_TITLE])
    slide.shapes.title.text = title
    # Подзаголовок (placeholder idx 1) есть в дефолтном layout; defensive на кастомный шаблон.
    if len(slide.placeholders) > 1:
        slide.placeholders[1].text = subtitle
    return slide


def _add_bullets_slide(
    prs: _PptxPresentation, title: str, bullets: list[tuple[str, Any]]
) -> _PptxSlide:
    """Слайд «Title and Content»: заголовок + буллеты «метка: значение». Graceful.

    Метка — статичная RU-строка, значение — через `_fmt` (None → "—"). Пустой список →
    один буллет «нет данных» (слайд всё равно валиден). Зеркало `report_docx._add_kv_lines`
    (там docx-абзацы; здесь — параграфы text-frame'а content-плейсхолдера).
    """
    slide = prs.slides.add_slide(prs.slide_layouts[_LAYOUT_TITLE_CONTENT])
    slide.shapes.title.text = title
    body = slide.placeholders[1].text_frame
    body.clear()

    if not bullets:
        body.paragraphs[0].text = _NO_DATA
        return slide

    for idx, (label, value) in enumerate(bullets):
        # Первый параграф уже существует (после clear) — переиспользуем, дальше add_paragraph.
        para = body.paragraphs[0] if idx == 0 else body.add_paragraph()
        para.text = f"{label}: {_fmt(value)}" if label else _fmt(value)
        para.level = 0
    return slide


def _add_table_slide(
    prs: _PptxPresentation, title: str, headers: list[str], rows: list[list[Any]]
) -> _PptxSlide:
    """Слайд «Title Only» + кастомная таблица: шапка (RU-метки) + строки (через `_fmt`).

    Пустой `rows` → одна строка-заглушка «нет данных» под шапкой (таблица валидна).
    Все ячейки данных проходят `_fmt`. Зеркало `report_docx._add_table` (там docx Table;
    здесь — python-pptx GraphicFrame table на отдельном слайде). Размеры — в EMU через
    `pptx.util.Inches` (импортируем локально, как и сам Presentation).
    """
    from pptx.util import Inches

    slide = prs.slides.add_slide(prs.slide_layouts[_LAYOUT_TITLE_ONLY])
    slide.shapes.title.text = title

    body_rows = rows if rows else [[_NO_DATA] + [_DASH] * (len(headers) - 1)]
    n_rows = len(body_rows) + 1  # +1 на шапку
    n_cols = len(headers)

    left, top, width, height = Inches(0.5), Inches(1.6), Inches(9.0), Inches(0.4)
    table = slide.shapes.add_table(n_rows, n_cols, left, top, width, height).table

    for col_idx, header in enumerate(headers):
        table.cell(0, col_idx).text = header

    for row_idx, row in enumerate(body_rows, start=1):
        for col_idx in range(n_cols):
            # Строка может оказаться короче шапки (defensive) — недостающее → "—".
            value = row[col_idx] if col_idx < len(row) else None
            table.cell(row_idx, col_idx).text = _fmt(value)
    return slide


def _join_horizons(values: list[Any]) -> Any:
    """Свернуть список горизонтов в «6, 12, 18» или None (→ `_fmt` отдаст "—"). PURE."""
    return ", ".join(str(v) for v in values) if values else None


# ──────────────────────────────────────────────────────────────────────────────
# Построители слайдов — по одному на содержательный слайд §22 (сжато: колода).
# Каждый graceful (пустой блок → «нет данных», не падает) и ПИШЕТ в `prs`.
# ──────────────────────────────────────────────────────────────────────────────


def _build_title(prs: _PptxPresentation, report: dict[str, Any]) -> None:
    """Титульный слайд: «Site Finder v2 — прогноз» + cad/район/горизонты + дисклеймер."""
    meta = _as_dict(report.get("meta"))
    cad = _fmt(meta.get("cad_num"))
    district = _fmt(meta.get("district"))
    horizons = _fmt(_join_horizons(_as_list(meta.get("horizons"))))

    subtitle = (
        f"Кадастровый номер: {cad}\n"
        f"Район: {district}\n"
        f"Горизонты (мес): {horizons}\n"
        f"{_ADVISORY_DISCLAIMER} {_ADVISORY_MARKER}"
    )
    _add_title_slide(prs, _TITLE_DECK, subtitle)


def _build_summary(prs: _PptxPresentation, report: dict[str, Any]) -> None:
    """Слайд «Сводка»: headline + ключевые числа (deficit/MOI/overall/confidence)."""
    exec_summary = _as_dict(report.get("exec_summary"))
    headline = exec_summary.get("headline")
    key_numbers = _as_dict(exec_summary.get("key_numbers"))
    overall_conf = _level_ru(exec_summary.get("overall_confidence"))

    bullets: list[tuple[str, Any]] = [("Вывод", headline)]
    bullets.extend((str(key), value) for key, value in key_numbers.items())
    bullets.append(("Общая уверенность", overall_conf))
    _add_bullets_slide(prs, _TITLE_SUMMARY, bullets)


def _build_future_market(prs: _PptxPresentation, report: dict[str, Any]) -> None:
    """Слайд «Будущий рынок»: компактная таблица forecasts_by_horizon + давление."""
    future = _as_dict(report.get("future_market"))
    forecasts = _as_list(future.get("forecasts_by_horizon"))

    headers = ["Горизонт, мес", "Дефицит", "Мес. запаса", "Ставка", "Уверенность"]
    rows = [
        [
            f.get("horizon_months"),
            f.get("deficit_index"),
            f.get("months_of_inventory"),
            f.get("rate_future"),
            _level_ru(f.get("confidence")),
        ]
        for f in forecasts
        if isinstance(f, dict)
    ]
    _add_table_slide(prs, _TITLE_FUTURE_MARKET, headers, rows)


def _build_scenarios(prs: _PptxPresentation, report: dict[str, Any]) -> None:
    """Слайд «Сценарии»: base/aggressive/conservative — дефицит (12 мес) + rate_path."""
    scenarios = _as_dict(report.get("scenarios"))
    by_scenario = _as_dict(scenarios.get("by_scenario"))

    bullets: list[tuple[str, Any]] = []
    for name, payload in by_scenario.items():
        data = _as_dict(payload)
        rate_path = _as_dict(data.get("rate_path"))
        rate_str = (
            ", ".join(f"{k}: {_fmt(v)}" for k, v in rate_path.items()) if rate_path else _DASH
        )
        deficit = _fmt(_scenario_deficit_cell(data))
        bullets.append(
            (str(name), f"дефицит ({_PRIMARY_HORIZON_MONTHS} мес) {deficit}; ставка {rate_str}")
        )
    _add_bullets_slide(prs, _TITLE_SCENARIOS, bullets)


def _build_product_tz(prs: _PptxPresentation, report: dict[str, Any]) -> None:
    """Слайд «Продукт ТЗ»: класс + квартирография (mix) + USP-ниши (буллеты)."""
    product = _as_dict(report.get("product_tz"))
    mix = _as_list(product.get("mix"))
    usp = _as_list(product.get("usp"))

    bullets: list[tuple[str, Any]] = [
        ("Рекомендованный класс", product.get("obj_class")),
    ]
    mix_str = (
        "; ".join(
            f"{_fmt(m.get('bucket'))} — {_fmt(m.get('pct'))}%" for m in mix if isinstance(m, dict)
        )
        or None
    )
    bullets.append(("Квартирография", mix_str))
    for item in usp[:_USP_TOP_N]:
        if isinstance(item, dict):
            bullets.append((f"USP · {_fmt(item.get('segment'))}", item.get("usp_text")))
    _add_bullets_slide(prs, _TITLE_PRODUCT_TZ, bullets)


def _build_confidence(prs: _PptxPresentation, report: dict[str, Any]) -> None:
    """Слайд «Уверенность»: уровень + топ-факторы (буллеты). Graceful."""
    confidence = _as_dict(report.get("confidence"))
    level = _level_ru(confidence.get("level"))
    factors = _as_dict(confidence.get("factors"))

    bullets: list[tuple[str, Any]] = [("Уровень", level)]
    for name, payload in list(factors.items())[:_FACTORS_TOP_N]:
        # Факторы #990: {name: {value, level, note}} ИЛИ плоское {name: value}. Defensive.
        if isinstance(payload, dict):
            detail = payload.get("note") or _level_ru(payload.get("level"))
            bullets.append((str(name), detail))
        else:
            bullets.append((str(name), payload))
    _add_bullets_slide(prs, _TITLE_CONFIDENCE, bullets)


# Реестр построителей. Порядок = порядок слайдов после титула (зеркало report_docx:
# Сводка → Будущий рынок → Сценарии → Продукт ТЗ → Уверенность; title идёт отдельно).
_SLIDE_BUILDERS: tuple[Any, ...] = (
    _build_summary,
    _build_future_market,
    _build_scenarios,
    _build_product_tz,
    _build_confidence,
)


# ──────────────────────────────────────────────────────────────────────────────
# Публичный API — рендер отчёта в PPTX-байты (без файлового I/O на диск).
# ──────────────────────────────────────────────────────────────────────────────


def render_report_pptx(report: Any) -> bytes:
    """§22 Отрендерить `SiteFinderReport` (#987) в презентацию (.pptx) и вернуть БАЙТЫ.

    Титульный слайд («Site Finder v2 — прогноз» + cad/район/горизонты + advisory-
    дисклеймер в подзаголовке), далее пять содержательных слайдов §22 (Сводка / Будущий
    рынок / Сценарии / Продукт ТЗ / Уверенность) — содержание ЗЕРКАЛИТ
    `report_docx`/`report_pdf`, но СЖАТО под формат колоды (буллеты + одна компактная
    таблица прогноза). Числа форматируются через импортированный `_fmt` (None → "—"),
    уровни уверенности — RU-метками.

    ДЕТЕРМИНИРОВАННО, БЕЗ LLM/БД/сети. Принимает КАК `SiteFinderReport`-инстанс, ТАК и
    его `as_dict()`-словарь (нормализуется через импортированный `_normalize`). GRACEFUL:
    частичный/пустой/мусорный отчёт → слайды с «нет данных», НИКОГДА не падает (нет
    KeyError на тонком отчёте) — возвращает минимальную валидную .pptx.

    `python-pptx` импортируется ЛОКАЛЬНО (как python-docx в `report_docx`): не нужен при
    импорте модуля. Презентация собирается в памяти (`io.BytesIO`) — без файлового I/O.

    Args:
        report: `SiteFinderReport`-инстанс или его `as_dict()`-словарь (или мусор → {}).

    Returns:
        Непустые PPTX-байты (OOXML — это zip, начинаются с `b"PK"`), готовые для
        `Response(media_type=
        "application/vnd.openxmlformats-officedocument.presentationml.presentation")`.
    """
    # python-pptx импортируем локально — тяжёлый (lxml/Pillow); не нужен при импорте модуля.
    try:
        from pptx import Presentation
    except ImportError as exc:
        raise RuntimeError(
            "python-pptx не установлен. Добавь 'python-pptx>=1.0.0' в pyproject.toml."
        ) from exc

    data = _normalize(report)
    prs = Presentation()

    _build_title(prs, data)
    for builder in _SLIDE_BUILDERS:
        builder(prs, data)

    buffer = io.BytesIO()
    prs.save(buffer)
    pptx_bytes = buffer.getvalue()

    meta = _as_dict(data.get("meta"))
    logger.info(
        "render_report_pptx: cad_num=%s slides=%d size=%d bytes advisory=%s",
        meta.get("cad_num"),
        len(_SLIDE_BUILDERS) + 1,  # +1 титульный
        len(pptx_bytes),
        data.get("advisory"),
    )
    return pptx_bytes
