"""Thin deterministic reader for the unified macro_indicator series table.

ТЗ §8.8 / #945 (EPIC «Унифицированная сущность Макропоказатель»), PR A.

Архитектурный принцип: ДЕТЕРМИНИРОВАННО, БЕЗ LLM, без внешних вызовов — это просто
типизированное чтение временных рядов из таблицы macro_indicator (создаётся и
заполняется миграцией data/sql/123_macro_indicator.sql).

Это то, что будут потреблять последующие эпики: Mortgage Affordability Index,
макро-коэффициент, key-rate sensitivity. Держим helper МИНИМАЛЬНЫМ.

psycopg v3 / SQLAlchemy text: bind-параметры ВСЕГДА через CAST(:x AS type) —
никогда :x::type (парсер psycopg3 игнорирует :name::type → SyntaxError).

NB: analytics_queries._current_mortgage_rate НЕ трогаем в этом PR (отдельная задача).
"""

from __future__ import annotations

from datetime import date

from sqlalchemy import text
from sqlalchemy.orm import Session

# ── Маппинг cbr_mortgage_series.series_id → indicator_type (ЗЕРКАЛО SQL-миграции) ─
# Это pure-Python зеркало CASE series_id-выражения из 123_macro_indicator.sql. Сам
# backfill делается set-based в SQL (миграция не импортирует Python) — здесь дублируем
# логику ТОЛЬКО для unit-тестируемости и для возможного переиспользования из Python.
# При изменении маппинга в миграции — синхронно правьте здесь (и наоборот).
#
# Ключ — устойчивый series_id (НЕ title!): в prod строка «в рублях» живёт именно в
# series_id, а title общий и её не содержит, поэтому матчим series_id точным равенством.
# Берём ТОЛЬКО _в_рублях-варианты семейства _mortgage_ (ИЖК, форма 02): маппинг и
# _в_рублях, и _итого на один indicator_type коллиднул бы по PK (type,region,obs_date).
# НЕ включены: _итого-варианты, _housing_ (02_06/07/08), _scpa_mortgage_ (02_15-18),
# _Share_ (02_44) — все они дают None и в backfill не попадают.
_CBR_SERIES_MAP: dict[str, tuple[str, str]] = {
    "02_13_Rates_mortgage_ставка_в_рублях": ("mortgage_rate_weighted", "%"),
    "02_13_Rates_mortgage_срок_в_руб": ("mortgage_term", "мес"),
    "02_10_Quantity_mortgage_в_рублях": ("mortgage_issued_count", "шт"),
    "02_11_New_loans_mortgage_в_рублях": ("mortgage_issued_volume", "млн руб"),
    "02_14_Debt_mortgage_в_рублях": ("mortgage_debt", "млн руб"),
    "02_14_Debt_mortgage_в_т.ч._просроч._в_рублях": ("mortgage_overdue", "млн руб"),
}


def classify_cbr_series(series_id: str | None) -> tuple[str, str] | None:
    """Pure helper: cbr_mortgage_series.series_id → (indicator_type, unit) | None.

    Детерминированно, без БД. Ключуем по УСТОЙЧИВОМУ series_id (не по title — title
    в prod общий и не содержит «в рублях»). Возвращает None для рядов, которые НЕ
    сопоставляются ни одному чистому макротипу (они сознательно пропускаются в backfill:
    все _итого-варианты, _housing_, _scpa_mortgage_, _Share_ и любой неизвестный ключ).

    Зеркалит CASE series_id-логику миграции 123_macro_indicator.sql (держать в синхроне).
    """
    if not series_id:
        return None
    return _CBR_SERIES_MAP.get(series_id)


# ── Canonical region per indicator_type ───────────────────────────────────────────
# Реальная раскладка данных ЦБ в macro_indicator (проверено на prod):
#   • семейство CBR-ипотеки (формы 02, ИЖК) лежит под region='sverdl' — оно
#     бэкфиллится из cbr_mortgage_series, где единственный регион — 'sverdl'
#     (см. 123_macro_indicator.sql §2: region берётся из источника КАК ЕСТЬ);
#   • key_rate (и прочие национальные ряды) — под region='rf'.
# Это НЕ опечатка в данных, а фактический layout. Поэтому дефолтный регион ридера
# должен быть indicator-aware: иначе чтение mortgage_*-ряда без явного region молча
# вернёт пусто (footgun, #945 follow-up). Множество ипотечных типов берём как
# values() из _CBR_SERIES_MAP — единый источник правды, синхронный с миграцией.
_MORTGAGE_INDICATOR_TYPES: frozenset[str] = frozenset(
    itype for itype, _unit in _CBR_SERIES_MAP.values()
)

# Региональные ряды Росстата (#946) кладутся под region='sverdl' (субъект-агрегат) и
# 'ekb' (г. Екатеринбург), НЕ под 'rf'. Два источника:
#   • open-data (source='rosstat'): population_total (ОКТМО 65 / ЕКБ);
#   • ЕМИСС/fedstat (source='emiss'): income_per_capita (среднедушевые доходы, ОКАТО 65 —
#     только субъект, ЕКБ-среза в ОКАТО-размерности ЕМИСС нет).
# Дефолтный region таких типов = 'sverdl' (субъект — основной уровень потребления
# downstream affordability/forecast). Явный region='ekb' читатель задаёт сам.
# Источник правды по типам — DatasetSpec/EmissIndicatorSpec в rosstat_emiss (синхронить).
_ROSSTAT_SVERDL_INDICATOR_TYPES: frozenset[str] = frozenset(
    {"population_total", "income_per_capita"}
)


def _canonical_region(indicator_type: str) -> str:
    """Канонический region для indicator_type, когда вызыватель не задал явный.

    Детерминированно, без БД:
      • 6 CBR-ипотечных типов (mortgage_*) → 'sverdl' (так лежат на prod);
      • региональные ряды Росстата (population_total, income_per_capita, #946) → 'sverdl';
      • всё остальное (key_rate и пр.) → 'rf'.
    Источники истины — _CBR_SERIES_MAP (зеркало миграции 123) и _ROSSTAT_SVERDL_INDICATOR_TYPES
    (зеркало DatasetSpec'ов rosstat_emiss).
    """
    if indicator_type in _MORTGAGE_INDICATOR_TYPES:
        return "sverdl"
    if indicator_type in _ROSSTAT_SVERDL_INDICATOR_TYPES:
        return "sverdl"
    return "rf"


# ── Reader ──────────────────────────────────────────────────────────────────────


def get_macro_series(
    db: Session,
    indicator_type: str,
    region: str | None = None,
    since: date | None = None,
) -> list[tuple[date, float]]:
    """Весь ряд (obs_date, value) для (indicator_type, region), ASC по obs_date.

    Args:
        db: SQLAlchemy sync Session.
        indicator_type: напр. 'mortgage_rate_weighted', 'key_rate'.
        region: 'rf' | 'sverdl' | ... Если None — резолвится indicator-aware дефолт
            через _canonical_region (CBR-ипотека → 'sverdl', остальное → 'rf'),
            т.к. на prod эти семейства лежат в РАЗНЫХ регионах. Явный region
            ВСЕГДА выигрывает (вызыватель может переопределить дефолт).
        since: если задано — только obs_date >= since.

    Returns:
        Список (obs_date, value) по возрастанию даты; строки с NULL value отброшены.
    """
    if region is None:
        region = _canonical_region(indicator_type)
    rows = db.execute(
        text(
            """
            SELECT obs_date, value
              FROM macro_indicator
             WHERE indicator_type = CAST(:itype AS text)
               AND region = CAST(:region AS text)
               AND value IS NOT NULL
               AND (CAST(:since AS date) IS NULL OR obs_date >= CAST(:since AS date))
             ORDER BY obs_date ASC
            """
        ),
        {"itype": indicator_type, "region": region, "since": since},
    ).all()
    return [(r[0], float(r[1])) for r in rows]


def get_latest_macro(
    db: Session,
    indicator_type: str,
    region: str | None = None,
) -> tuple[date, float] | None:
    """Последняя (по obs_date) непустая точка ряда или None, если ряда нет.

    Args:
        db: SQLAlchemy sync Session.
        indicator_type: тип показателя.
        region: 'rf' | 'sverdl' | ... Если None — резолвится indicator-aware дефолт
            через _canonical_region (CBR-ипотека → 'sverdl', остальное → 'rf'):
            на prod эти семейства лежат в РАЗНЫХ регионах. Явный region ВСЕГДА
            выигрывает (вызыватель может переопределить дефолт).

    Returns:
        (obs_date, value) самой свежей точки, либо None.
    """
    if region is None:
        region = _canonical_region(indicator_type)
    row = db.execute(
        text(
            """
            SELECT obs_date, value
              FROM macro_indicator
             WHERE indicator_type = CAST(:itype AS text)
               AND region = CAST(:region AS text)
               AND value IS NOT NULL
             ORDER BY obs_date DESC
             LIMIT 1
            """
        ),
        {"itype": indicator_type, "region": region},
    ).first()
    if row is None:
        return None
    return row[0], float(row[1])
