"""§7.9 Mortgage Affordability Index (MAI) — прокси платёжной нагрузки по РЫНОЧНОЙ ставке.

#981 (952-B, Site Finder v2 / «GG-форсайт» ТЗ §7.9), EPIC 8 «что строить +
доступность». Это **СБОРОЧНЫЙ слой**: своей §9.x-математики не пересобирает —
переиспользует helper'ы (`get_monthly_macro` §9.5/§9.6 для ключевой ставки ЦБ,
`build_sales_series` для сегментной цены ₽/м²). Всё ДЕТЕРМИНИРОВАННО, БЕЗ LLM
(чистая аннуитет-арифметика + reuse; своего SQL НЕТ).

РЫНОЧНАЯ СТАВКА — KEY_RATE ПРОКСИ (#981 DoD: «НЕ mortgage_rate_weighted 7.83%»):
  DoD §7.9 требует РЫНОЧНОЙ стоимости заёмных денег (~16-21% по ЦБ Янв 2026), НЕ
  субсидированной средневзвешенной ИЖК-ставки (~7-8% с семейной/IT/ДВ-льготами —
  она ЗАНИЖАЕТ платёж против рынка). Прямого ряда «рыночная ипотека без льгот» в
  БД действительно нет, НО ключевая ставка ЦБ ЕСТЬ: `macro_indicator`
  (indicator_type='key_rate', region='rf', source='cbr'), залитая `cbr_macro_sync`
  и читаемая через `get_monthly_macro`. Ключевая ставка — ПРАВОМЕРНЫЙ рыночный
  прокси стоимости заёмных средств (рыночная ипотека = key_rate + спред банка). Спред
  КАЛИБРОВАН = 4.5 п.п. по прод-якорю `mortgage_rate_primary_domrf` (рыночная
  первичная ипотека) 19.125% @ 2026-04-19 vs key_rate ≈ 14.5% → наблюдаемый спред
  ≈ 4.6 п.п. (округлён до 4.5 консервативно; типичный диапазон РФ 3-5 п.п.). Поэтому
  `rate_used` ≈ key_rate + 4.5 ≈ 19% — РЕАЛЬНАЯ рыночная стоимость заёмных денег, а
  НЕ голая ключевая ставка как нижняя граница. `rate_kind='key_rate_proxy'`, НЕ 7.83%.
  Субсидированный путь сохранён в `_current_subsidized_rate` (explain/сравнение),
  но в индекс по умолчанию НЕ идёт.

ADVISORY / LOW-CONFIDENCE (КРИТИЧНО — честность важнее цифры):
  Истинный §7.9 = «рыночный аннуитет ÷ доход домохозяйства». ДОХОД домохозяйства
  (#946) в данных проекта ОТСУТСТВУЕТ → `payment_to_income` ВСЕГДА None,
  `confidence='low'` ВСЕГДА (числитель теперь рыночный, но знаменатель-дохода нет).
  `degraded_reason` объясняет остаточный пробел (доход) и сделанный выбор прокси
  ставки (key_rate + калиброванный рыночный спред 4.5 п.п.). Цифра — для
  explainability/прототипа, не для решения.

Graceful-on-thin-data (дух demand_supply_forecast / market_metrics): нет ставки →
платёж None; нет цены сегмента → платёж None; ставка ≤0 → аннуитет деградирует к
principal/months (без деления на ноль). НИКОГДА не crash, НИКОГДА 0-как-заглушка.
Детерминированно.
"""

from __future__ import annotations

import logging
import statistics
from dataclasses import dataclass
from typing import Any, Literal

from sqlalchemy.orm import Session

from app.services.analytics_queries import _current_mortgage_rate
from app.services.forecasting.macro_series import get_monthly_macro
from app.services.forecasting.sales_series import (
    SalesSource,
    SegmentSpec,
    build_sales_series,
)

logger = logging.getLogger(__name__)

# ── Named-константы ───────────────────────────────────────────────────────────

# Эталонная площадь квартиры (м²) для расчёта «типового» тела кредита. ~50 м² ≈
# медианная 1-2-комнатная квартира масс-рынка ЕКБ (зеркалит ROOM_AREA_BUCKET 1-к
# 30-45 / 2-к 45-60). Держим явной константой — индекс сопоставим между сегментами
# при ФИКСИРОВАННОЙ площади (меняется только цена ₽/м²), а не плавает с метражом.
_REF_AREA_M2: float = 50.0

# Срок ипотеки (мес) для аннуитета. 240 мес = 20 лет — типовой срок ИЖК в РФ
# (медиана ЦБ ~ 20-25 лет; берём круглые 20 для детерминизма). Tunable.
_ANNUITY_TERM_MONTHS: int = 240

# Метка вида ставки в выводе. По умолчанию — рыночный прокси через ключевую ставку
# ЦБ (#981 DoD: рыночная стоимость денег, НЕ субсидия 7.83%). Субсидированная метка
# сохранена для explain-сравнения, но в индекс по умолчанию НЕ идёт.
_RATE_KIND_KEY_RATE_PROXY: str = "key_rate_proxy"
_RATE_KIND_SUBSIDIZED: str = "subsidized_weighted"
_RATE_KIND_UNAVAILABLE: str = "market_unavailable"

# Спред банка над ключевой ставкой для рыночной ипотеки (п.п.). Рыночная ставка ≈
# key_rate + спред. Калибровано по прод-якорю: mortgage_rate_primary_domrf
# (rf/domrf — РЫНОЧНАЯ первичная ипотека) = 19.125% @ 2026-04-19 при key_rate ЦБ
# ≈ 14.5% на ту же дату → наблюдаемый спред ≈ 4.6 п.п. Округляем до 4.5 п.п.
# (слегка консервативно, внутри типичного для РФ диапазона key_rate→рыночная
# ипотека 3-5 п.п.). Так rate_used ≈ key_rate + 4.5 (реальная рыночная стоимость
# заёмных денег ~19%), а НЕ голая ключевая ставка как нижняя граница. Tunable.
# TODO: при появлении надёжного МЕСЯЧНОГО ряда mortgage_rate_primary_domrf сделать
# спред data-driven (сейчас это единичный национальный снимок — слишком тонко,
# чтобы зависеть от него напрямую, поэтому фиксируем калиброванной константой).
_KEY_RATE_MARKET_SPREAD_PP: float = 4.5

# Источник сегментной цены ₽/м² по умолчанию: Source B (objective_lots) — несёт
# per-lot цену для price/room-сегментации (Source A её не даёт). Каведат
# survivorship bias задокументирован в sales_series; для прокси-индекса приемлемо.
_PRICE_SOURCE: SalesSource = "objective_lots"

# Текст деградации — объясняет выбор рыночного прокси ставки И остаточный пробел
# (доход), чтобы вывод нельзя было прочитать как истинный §7.9 индекс. Используется
# в degraded_reason, когда ключевая ставка ДОСТУПНА (рыночный путь отработал).
_DEGRADED_REASON: str = (
    "Прокси платёжной нагрузки по РЫНОЧНОЙ ставке, НЕ истинный §7.9 индекс "
    "доступности: (1) рыночная стоимость денег взята как ключевая ставка ЦБ "
    "(key_rate) + калиброванный рыночный спред 4.5 п.п. (≈ key_rate + 4.5, ~19%) — "
    "прямого месячного ряда «рыночная ипотека без льгот» в БД нет, спред откалиброван "
    "по mortgage_rate_primary_domrf 19.125% vs key_rate ≈ 14.5% (наблюдаемый ≈4.6 "
    "п.п.), что отражает реальную рыночную стоимость заёмных денег (честнее "
    "субсидированных ~7-8%); (2) доход домохозяйства (#946) недоступен, поэтому "
    "payment_to_income не рассчитывается (всегда None). confidence='low' — "
    "знаменатель-дохода отсутствует."
)

# Текст деградации, когда ключевой ставки в БД НЕТ вовсе (рыночный сигнал
# недоступен) — НЕ подменяем субсидией, честно сообщаем data-gap (rate_used=None).
_DEGRADED_REASON_NO_MARKET: str = (
    "Прокси платёжной нагрузки НЕ рассчитан: рыночной ставки нет — ни прямого "
    "ряда рыночной ипотеки, ни ключевой ставки ЦБ (key_rate) в macro_indicator. "
    "Субсидированной ставкой (~7-8%) НЕ подменяем (исказила бы рыночный платёж). "
    "rate_used=None, rate_kind='market_unavailable'. Доход (#946) также "
    "недоступен → payment_to_income None. confidence='low' — data-gap по ставке."
)


@dataclass(frozen=True)
class MortgageAffordabilityIndex:
    """§7.9 MAI — прокси платёжной нагрузки по РЫНОЧНОЙ ставке (считается на лету).

    Все денежные поля = None при недостатке данных (НИКОГДА 0-как-заглушка).
    `payment_to_income` ВСЕГДА None (дохода нет в данных, #946). `rate_kind`:
    'key_rate_proxy' (ключевая ставка ЦБ как рыночный прокси — путь по умолчанию,
    #981 DoD) или 'market_unavailable' (если key_rate в БД нет — rate_used тогда
    None, субсидией НЕ подменяем). `confidence` ВСЕГДА 'low' (нет дохода-знаменателя).
    `degraded_reason` объясняет выбор прокси ставки + пробел дохода. Это
    explainability-прокси, НЕ основание для инвест-решения.
    """

    segment: dict[str, str | None]
    monthly_payment_rub: float | None  # аннуитет на эталонную квартиру (₽/мес)
    rate_used: float | None  # годовая рыночная ставка (%) — key_rate + спред (#981)
    rate_kind: str  # 'key_rate_proxy' | 'market_unavailable'
    rate_period: str | None  # период точки ставки (explain)
    ref_area_m2: float  # эталонная площадь кредита (м²)
    price_per_m2: float | None  # сегментная цена ₽/м² (вход тела кредита)
    annuity_term_months: int  # срок ипотеки (мес)
    payment_to_income: float | None  # ВСЕГДА None — дохода нет (#946)
    payment_at_scenario: dict[int, float] | None  # платёж/горизонт при rate_path
    degraded_reason: str  # объясняет оба пробела (ставка + доход)
    confidence: Literal["low"]  # ВСЕГДА 'low' (деградация)

    def as_dict(self) -> dict[str, Any]:
        return {
            "segment": dict(self.segment),
            "monthly_payment_rub": _round_or_none(self.monthly_payment_rub, 0),
            "rate_used": _round_or_none(self.rate_used, 2),
            "rate_kind": self.rate_kind,
            "rate_period": self.rate_period,
            "ref_area_m2": self.ref_area_m2,
            "price_per_m2": _round_or_none(self.price_per_m2, 0),
            "annuity_term_months": self.annuity_term_months,
            "payment_to_income": self.payment_to_income,
            "payment_at_scenario": (
                {h: round(v, 0) for h, v in self.payment_at_scenario.items()}
                if self.payment_at_scenario is not None
                else None
            ),
            "degraded_reason": self.degraded_reason,
            "confidence": self.confidence,
        }


def _round_or_none(value: float | None, digits: int) -> float | None:
    return round(value, digits) if value is not None else None


# ──────────────────────────────────────────────────────────────────────────────
# Pure-арифметика — без БД, полностью юнит-тестируемо.
# ──────────────────────────────────────────────────────────────────────────────


def _annuity(
    principal: float | None,
    annual_rate: float | None,
    months: int,
) -> float | None:
    """Стандартный аннуитетный платёж по кредиту. PURE, graceful (без /0).

    Формула (annual_rate в ПРОЦЕНТАХ годовых → месячная доля i = rate/100/12):
        payment = principal · i · (1+i)^n / ((1+i)^n − 1).

    Правила деградации (честность важнее «красивой» цифры):
      • principal None ИЛИ annual_rate None → None (нет тела/ставки — нечего считать).
      • months ≤ 0 → None (срок не задан).
      • annual_rate ≤ 0 → principal / months (беспроцентный кредит: НЕ делим на
        ноль в формуле, где i=0 даёт 0/0; равномерное погашение тела).

    Args:
        principal: тело кредита (₽); None → None.
        annual_rate: годовая ставка в процентах (напр. 7.5); None → None; ≤0 →
            беспроцентная деградация principal/months.
        months: срок кредита (мес); ≤0 → None.

    Returns:
        Месячный платёж (₽) или None.
    """
    if principal is None or annual_rate is None:
        return None
    if months <= 0:
        return None
    if annual_rate <= 0:
        # Беспроцентный кредит: формула вырождается (i=0 → 0/0). Равномерное тело.
        return principal / float(months)
    i = annual_rate / 100.0 / 12.0
    growth = (1.0 + i) ** months
    return principal * i * growth / (growth - 1.0)


# ──────────────────────────────────────────────────────────────────────────────
# Ставки — рыночный прокси (key_rate, #981) + субсидированный (explain-only).
# ──────────────────────────────────────────────────────────────────────────────


def _current_market_rate(
    db: Session, *, spread_pp: float = _KEY_RATE_MARKET_SPREAD_PP
) -> tuple[float | None, str | None]:
    """Рыночная стоимость заёмных денег как ключевая ставка ЦБ + спред (#981 DoD).

    Прямого ряда «рыночная ипотека без льгот» в БД нет, но ключевая ставка ЦБ есть
    (`macro_indicator` indicator_type='key_rate', region='rf', залита cbr_macro_sync,
    читается через get_monthly_macro). Берём САМУЮ СВЕЖУЮ непустую key_rate и
    добавляем калиброванный рыночный спред банка (по умолчанию 4.5 п.п. — реальная
    рыночная стоимость денег, ≈ key_rate + 4.5 ~19%, а НЕ нижняя граница). Это
    rate_kind='key_rate_proxy' — НЕ субсидированные 7.83%.

    Graceful: нет ни одной точки key_rate (пустой macro_indicator / сбой БД) →
    (None, None) — caller помечает rate_kind='market_unavailable' и НЕ подменяет
    субсидией. Reuse get_monthly_macro (свой SQL не пишем). Детерминированно.

    Args:
        db: SQLAlchemy sync Session.
        spread_pp: калиброванный рыночный спред банка над ключевой ставкой в п.п.
            (по умолчанию _KEY_RATE_MARKET_SPREAD_PP = 4.5).

    Returns:
        (рыночная ставка %, период YYYY-MM последней точки) либо (None, None).
    """
    macro = get_monthly_macro(db)
    latest_rate: float | None = None
    latest_period: str | None = None
    for m in macro:  # ряд ASC по month → последняя непустая = самая свежая
        if m.key_rate is not None:
            latest_rate = m.key_rate
            latest_period = m.month.strftime("%Y-%m")
    if latest_rate is None:
        return None, None
    return latest_rate + spread_pp, latest_period


# `_current_subsidized_rate` — субсидированная средневзвешенная ИЖК-ставка (~7-8%).
# СОХРАНЕНА для explain/сравнения, но в индекс по умолчанию НЕ идёт (#981 DoD требует
# рыночную). Тонкий алиас на §аналитики _current_mortgage_rate (источник истины).
def _current_subsidized_rate(db: Session) -> tuple[float | None, str | None]:
    """Субсидированная средневзвешенная ИЖК-ставка (~7-8%) — explain-only (#981).

    Reuse analytics_queries._current_mortgage_rate. НЕ используется в индексе по
    умолчанию (рыночный путь — _current_market_rate); оставлена для явного
    сравнения «рыночная vs субсидия» downstream при необходимости.
    """
    return _current_mortgage_rate(db)


# ──────────────────────────────────────────────────────────────────────────────
# DB-оркестратор — тонкий, graceful. Pure-логика выше тестируется без него.
# ──────────────────────────────────────────────────────────────────────────────


def compute_affordability(
    db: Session,
    *,
    spec: SegmentSpec,
    price_per_m2: float | None = None,
    rate_path: dict[int, float] | None = None,
    ref_area_m2: float = _REF_AREA_M2,
    price_source: SalesSource = _PRICE_SOURCE,
) -> MortgageAffordabilityIndex:
    """§7.9 MAI — прокси платёжной нагрузки по РЫНОЧНОЙ ставке для сегмента (#981).

    ADVISORY / LOW-CONFIDENCE: это ПРОКСИ, а НЕ истинный §7.9 индекс доступности.
    Числитель теперь РЫНОЧНЫЙ (ключевая ставка ЦБ), но дохода домохозяйства (#946)
    в данных нет — см. module docstring и `degraded_reason`. `payment_to_income`
    ВСЕГДА None, `confidence` ВСЕГДА 'low' (нет знаменателя-дохода).

    Что считает:
      • rate, period = _current_market_rate(db) — ключевая ставка ЦБ + спред как
        рыночный прокси (~16-21%; #981 DoD). rate_kind='key_rate_proxy'. Если
        key_rate в БД нет → (None, None), rate_kind='market_unavailable' (субсидией
        НЕ подменяем — это исказило бы рыночный платёж; честный data-gap).
      • price_per_m2 — из аргумента ИЛИ сегментная средняя (reuse build_sales_series,
        усреднение НЕнулевых месяцев). None → платёж None (graceful).
      • monthly_payment = _annuity(principal=price×ref_area, annual_rate=rate,
        months=_ANNUITY_TERM_MONTHS).
      • payment_at_scenario[h] = _annuity(... annual_rate=rate_path[h] + спред) —
        платёж на каждом горизонте сценарной КЛЮЧЕВОЙ ставки ЦБ, приведённой к той
        же рыночной базе, что и monthly_payment_rub (key_rate + калиброванный спред
        4.5 п.п.); None rate_path → поле None (#1639).

    Graceful: нет ставки/цены → платёж None; ставка ≤0 → аннуитет деградирует к
    principal/months. НИКОГДА не crash. confidence ВСЕГДА 'low'. Детерминированно.

    Args:
        db: SQLAlchemy sync Session.
        spec: целевой сегмент (для сегментной цены, если price_per_m2 не задан).
        price_per_m2: цена ₽/м² (None → берём сегментную среднюю reuse-ом).
        rate_path: сценарный {horizon: КЛЮЧЕВАЯ ставка ЦБ %} для payment_at_scenario
            (контракт #952/#984: конверт key_rate, НЕ рыночная — к ней внутри
            добавляется тот же спред _KEY_RATE_MARKET_SPREAD_PP, что и в базовом
            monthly_payment_rub); None → payment_at_scenario None.
        ref_area_m2: эталонная площадь тела кредита (по умолчанию _REF_AREA_M2).
        price_source: источник сегментной цены (по умолчанию _PRICE_SOURCE = B).

    Returns:
        MortgageAffordabilityIndex (всегда; тонко → None-поля, confidence='low').
    """
    segment = spec.as_dict()
    # #981 DoD: рыночная ставка (key_rate ЦБ + спред), НЕ субсидированная 7.83%.
    # Нет key_rate в БД → rate None, помечаем market_unavailable (НЕ подменяем
    # субсидией — это исказило бы рыночный платёж).
    rate, rate_period = _current_market_rate(db)
    if rate is not None:
        rate_kind = _RATE_KIND_KEY_RATE_PROXY
        degraded_reason = _DEGRADED_REASON
    else:
        rate_kind = _RATE_KIND_UNAVAILABLE
        degraded_reason = _DEGRADED_REASON_NO_MARKET

    resolved_price = (
        price_per_m2
        if price_per_m2 is not None
        else _segment_avg_price(db, spec=spec, source=price_source)
    )
    principal = resolved_price * ref_area_m2 if resolved_price is not None else None

    monthly_payment = _annuity(principal, rate, _ANNUITY_TERM_MONTHS)

    payment_at_scenario: dict[int, float] | None = None
    if rate_path is not None:
        scenario: dict[int, float] = {}
        for horizon, scenario_rate in rate_path.items():
            # rate_path[h] = КЛЮЧЕВАЯ ставка ЦБ сценария (#952/#984 контракт:
            # demand_supply_forecast.py:586, scenarios.py — конверт key_rate), НЕ
            # рыночная. Приводим к той же рыночной базе, что и monthly_payment_rub:
            # key_rate + калиброванный спред (_current_market_rate, строка 251).
            # Иначе сценарный платёж считался бы по «голой» key_rate (≈ на 4.5 п.п.
            # ниже базовой ставки) и был бы НЕсопоставим с monthly_payment_rub (#1639).
            market_scenario_rate = (
                scenario_rate + _KEY_RATE_MARKET_SPREAD_PP
                if scenario_rate is not None
                else None
            )
            payment = _annuity(principal, market_scenario_rate, _ANNUITY_TERM_MONTHS)
            if payment is not None:
                scenario[horizon] = payment
        payment_at_scenario = scenario

    logger.info(
        "affordability: segment=%s rate=%s kind=%s period=%s price_per_m2=%s "
        "ref_area=%.1f monthly_payment=%s confidence=low "
        "(market proxy, payment_to_income=None)",
        segment,
        _round_or_none(rate, 2),
        rate_kind,
        rate_period,
        _round_or_none(resolved_price, 0),
        ref_area_m2,
        _round_or_none(monthly_payment, 0),
    )

    return MortgageAffordabilityIndex(
        segment=segment,
        monthly_payment_rub=monthly_payment,
        rate_used=rate,
        rate_kind=rate_kind,
        rate_period=rate_period,
        ref_area_m2=ref_area_m2,
        price_per_m2=resolved_price,
        annuity_term_months=_ANNUITY_TERM_MONTHS,
        payment_to_income=None,  # дохода нет в данных (#946) — НИКОГДА не считаем
        payment_at_scenario=payment_at_scenario,
        degraded_reason=degraded_reason,
        confidence="low",  # ВСЕГДА low — нет дохода-знаменателя, не истинный §7.9
    )


def _segment_avg_price(db: Session, *, spec: SegmentSpec, source: SalesSource) -> float | None:
    """Сегментная средняя цена ₽/м² — reuse build_sales_series (НЕ новый SQL).

    Берём monthly ряд продаж сегмента и усредняем НЕнулевые (не-None) месячные
    avg_price_per_m2 — простое среднее по месяцам с реальными сделками. Нет ни
    одного месяца с ценой (тонкий сегмент / сбой) → None (graceful, НЕ 0). PURE-ish
    (тонкая обёртка над reuse).

    Args:
        db: SQLAlchemy sync Session.
        spec: сегмент рынка.
        source: источник ряда (objective_lots несёт per-lot цену).

    Returns:
        Средняя цена ₽/м² по месяцам со сделками или None.
    """
    series = build_sales_series(db, spec=spec, source=source)
    prices = [p for p in series.avg_price_per_m2 if p is not None]
    if not prices:
        return None
    return statistics.fmean(prices)
