"""Site Finder ↔ финмодель bridge — лёгкий ТЭП из buildability → DCF (PR-4, #1881).

Участок в ``/analyze`` несёт **buildability** (площадь + предельные параметры зоны
НСПД/ПЗЗ: КСИТ/max_far, % застройки, этажность) + рыночную цену + gate-вердикт, но
НЕ полную проектную концепцию (нет реальной расстановки секций). Чтобы дать
девелоперу ОРИЕНТИРОВОЧНУЮ финмодель прямо на карточке участка, синтезируем лёгкий
ТЭП из предельных параметров зоны и прогоняем его через готовый
:func:`compute_financial` (полноценный статический каскад + помесячный DCF, PR-1/2/3).

Обе функции **чистые** — без БД / LLM / внешних API. Данные участка прокидывает
caller (analyze-эндпоинт) уже извлечёнными. Результат — ``dict`` (``FinancialModel``
+ bridge-метаданные) или ``None``, когда считать нельзя (gate против, нет чисел зоны,
нет площади).

ВАЖНО — HONEST CAVEAT
---------------------
Это финмодель по МАКСИМАЛЬНОЙ застройке согласно градрегламенту, НЕ по реальной
концепции: ТЭП синтезирован из предельных параметров зоны; класс жилья и темп
продаж — рыночные/нормативные прокси; ``land_cost`` = кадастровая стоимость (не
рыночная); график фаз — типовой. Caveat кладётся в результат (ключ ``caveat``) и
обязан показываться в UI/PDF рядом с цифрами.
"""

from __future__ import annotations

import logging
import math
from typing import Any

from app.schemas.concept import TEAP
from app.services.generative.financial import compute_financial

# Переиспользуем нормативные константы ТЭП из Stage 1c (НЕ дублируем): эффективность
# площади, средний лот, норма парковки по классу. Имена private (`_`) — импортируем
# их напрямую как single-source-of-truth, чтобы синтез не разошёлся с compute_teap.
from app.services.generative.teap import (
    _AVG_APARTMENT_SQM,
    _EFFICIENCY_BY_CLASS,
    _OFFICE_SHARE_OF_GFA,
    _PARKING_PER_APARTMENT,
    HousingClass,
)

logger = logging.getLogger(__name__)

# ── HEAVY caveat — обязателен в любом результате (UI/PDF MUST показать) ─────────
_FINANCIAL_CAVEAT: str = (
    "ОРИЕНТИРОВОЧНАЯ финмодель по МАКСИМАЛЬНОЙ застройке согласно градрегламенту "
    "НСПД/ПЗЗ (КСИТ/высота/%застройки), НЕ по реальной проектной концепции. "
    "ТЭП синтезирован из предельных параметров зоны; класс жилья и темп продаж — "
    "рыночные/нормативные прокси; land_cost = кадастровая стоимость (не рыночная); "
    "график фаз — типовой. Доля нежилого (коммерция/офисы 1-го этажа) — нормативный "
    "прокси по классу (комфорт/бизнес ~5% GFA, эконом 0%), не проектный факт."
)

# ── Класс жилья по цене продажи, руб/кв.м (рыночный прокси) ─────────────────────
# price < 125k → эконом · < 177k → комфорт · иначе → бизнес. None цены → комфорт
# (нейтральный дефолт массового сегмента ЕКБ).
_HOUSING_CLASS_ECONOM_MAX: float = 125_000.0
_HOUSING_CLASS_COMFORT_MAX: float = 177_000.0

# ── Тип застройки по этажности (длительность СМР в DCF-графике) ─────────────────
# <=4 → spot · 5–8 → mid_rise · >=9 → high_rise.
_DEV_TYPE_SPOT_MAX_FLOORS: int = 4
_DEV_TYPE_MID_RISE_MAX_FLOORS: int = 8


def _infer_housing_class(price_per_sqm: float | None) -> HousingClass:
    """Класс жилья из цены продажи (рыночный прокси). None → comfort (дефолт)."""
    if price_per_sqm is None:
        return "comfort"
    if price_per_sqm < _HOUSING_CLASS_ECONOM_MAX:
        return "econom"
    if price_per_sqm < _HOUSING_CLASS_COMFORT_MAX:
        return "comfort"
    return "business"


def _infer_development_type(max_floors: int | None) -> str:
    """Тип застройки из этажности зоны. None → mid_rise (нейтральный дефолт)."""
    if max_floors is None:
        return "mid_rise"
    if max_floors <= _DEV_TYPE_SPOT_MAX_FLOORS:
        return "spot"
    if max_floors <= _DEV_TYPE_MID_RISE_MAX_FLOORS:
        return "mid_rise"
    return "high_rise"


def select_calibrated_price(
    *,
    market_price: dict[str, Any] | None,
    geo_radius_price_median: float | None,
    district_price_median: float | None,
) -> tuple[float | None, str]:
    """Выбрать откалиброванную цену продажи жилья + её источник (single source of truth).

    Приоритет от точного рынка к нормативу (совпадает с исходной логикой
    :func:`synthesize_parcel_financial`, вынесен сюда для переиспользования оптимизатором
    программы, чтобы выбор цены не разошёлся):

    1. ``market_price.median`` — квартальная MV перепродаж → ``objective_district_median``.
    2. ``geo_radius_price_median`` — медиана новостроек Objective в гео-радиусе →
       ``objective_geo_radius``.
    3. ``district_price_median`` — name-match медиана админ-района → ``district_reference``.
    4. ничего из рынка → ``(None, "class_norm")`` — caller использует норматив класса.

    Returns:
        ``(price, price_source)`` — ``price`` может быть ``None`` (тогда source
        ``"class_norm"``), что сигналит caller-у про fallback на норматив класса.
    """
    if market_price and _as_float(market_price.get("median")) is not None:
        return _as_float(market_price.get("median")), "objective_district_median"
    if _as_float(geo_radius_price_median) is not None:
        return _as_float(geo_radius_price_median), "objective_geo_radius"
    if district_price_median is not None:
        return float(district_price_median), "district_reference"
    return None, "class_norm"


def synthesize_teap_from_buildability(
    *,
    area_m2: float | None,
    max_far: float | None,
    max_building_pct: float | None,
    max_floors: int | None,
    housing_class: HousingClass,
) -> TEAP | None:
    """Синтезировать лёгкий :class:`TEAP` из buildability-параметров зоны.

    Формулы (максимальная застройка по градрегламенту):

    * ``GFA`` (total_floor_area) = ``area × max_far``; если ``max_far`` нет, но есть
      ``max_building_pct`` + ``max_floors`` → ``GFA = area × (pct/100) × floors``.
    * ``built`` (пятно) = ``area × (max_building_pct/100)``; если % нет →
      оценка ``GFA / max_floors`` (или ``GFA`` при отсутствии этажности).
    * ``office`` (нежилое 1-го этажа) = ``GFA × office_share[class]``, вырезается из
      GFA до расчёта жилой (зеркалит ``compute_teap``; см. ``_OFFICE_SHARE_OF_GFA``).
    * ``residential`` = ``(GFA − office) × efficiency[class]`` (вычет МОП/тех по классу).
    * ``apartments`` = ``floor(residential / avg_apt[class])``.
    * ``parking`` = ``ceil(apartments × parking_norm[class])``.
    * ``density`` = ``GFA / area`` (= ``max_far``).

    Возвращает ``None``, если нет площади ИЛИ нельзя вычислить GFA (нет ни
    ``max_far``, ни пары ``max_building_pct`` + ``max_floors``).
    """
    if area_m2 is None or area_m2 <= 0:
        return None

    # ── GFA: предпочитаем КСИТ/max_far; иначе % застройки × этажность ───────────
    gfa: float
    if max_far is not None and max_far > 0:
        gfa = area_m2 * max_far
    elif (
        max_building_pct is not None
        and max_building_pct > 0
        and max_floors is not None
        and max_floors > 0
    ):
        gfa = area_m2 * (max_building_pct / 100.0) * max_floors
    else:
        # Ни КСИТ, ни (%застройки + этажность) — GFA не вывести.
        return None

    # ── Пятно застройки (built) ────────────────────────────────────────────────
    if max_building_pct is not None and max_building_pct > 0:
        built_area = area_m2 * (max_building_pct / 100.0)
    elif max_floors is not None and max_floors > 0:
        # Нет %застройки → пятно ≈ GFA / этажность.
        built_area = gfa / max_floors
    else:
        built_area = gfa

    # Нежилое (коммерция/офисы 1-го этажа) вырезаем из GFA до расчёта жилой — точно
    # как compute_teap: жилая считается по ОСТАВШЕЙСЯ GFA, total (gfa) не меняется.
    office_share = _OFFICE_SHARE_OF_GFA[housing_class]
    office_area = gfa * office_share
    residential_gfa = gfa - office_area

    efficiency = _EFFICIENCY_BY_CLASS[housing_class]
    residential_area = residential_gfa * efficiency

    avg_apartment = _AVG_APARTMENT_SQM[housing_class]
    apartments_count = math.floor(residential_area / avg_apartment) if avg_apartment else 0

    parking_norm = _PARKING_PER_APARTMENT[housing_class]
    parking_spaces = math.ceil(apartments_count * parking_norm)

    # Плотность застройки = FAR = GFA / площадь участка (= max_far, если он задан).
    density = gfa / area_m2 if area_m2 > 0 else 0.0

    return TEAP(
        built_area_sqm=round(built_area, 1),
        total_floor_area_sqm=round(gfa, 1),
        office_area_sqm=round(office_area, 1),
        residential_area_sqm=round(residential_area, 1),
        apartments_count=apartments_count,
        density=round(density, 3),
        parking_spaces=parking_spaces,
    )


def synthesize_parcel_financial(
    *,
    area_m2: float | None,
    nspd_zoning: dict[str, Any] | None,
    market_price: dict[str, Any] | None,
    district_price_median: float | None,
    cadastral_value_rub: float | None,
    gate_verdict: dict[str, Any] | None,
    geo_radius_price_median: float | None = None,
    velocity_sqm_per_month: float | None = None,
) -> dict[str, Any] | None:
    """Мост buildability участка → ОРИЕНТИРОВОЧНАЯ финмодель (DCF) или ``None``.

    Gate-guard: считаем ТОЛЬКО когда gate допускает МКД и есть числа зоны + площадь.

    Цена продажи калибруется по приоритету (от точного рыночного к нормативу):

    1. ``market_price.median`` — квартальная MV перепродаж (resale) →
       ``price_source="objective_district_median"``.
    2. ``geo_radius_price_median`` — медиана новостроек Objective в гео-радиусе вокруг
       центроида участка → ``price_source="objective_geo_radius"``. Закрывает пробел
       district_reference (только 4 из 9 админ-районов ЕКБ матчатся по имени).
    3. ``district_price_median`` — name-match медиана админ-района →
       ``price_source="district_reference"``.
    4. норматив класса → ``price_source="class_norm"``.

    Args:
        area_m2: площадь участка, кв.м (из parcel_meta/EGRN или geometry_suitability).
        nspd_zoning: блок зоны НСПД/ПЗЗ — берём ``max_far`` / ``max_building_pct`` /
            ``max_floors`` (merged regulation-поля из EKB-geoportal, #1067).
        market_price: квартальная ценовая статистика (берём ``median``).
        district_price_median: медиана цены по району, руб/кв.м (fallback к market).
        cadastral_value_rub: кадастровая стоимость участка → land_cost (НЕ рыночная).
        gate_verdict: can-build-MKD вердикт — гейт на расчёт.
        geo_radius_price_median: медиана цены новостроек Objective (``price_per_m2_rub``)
            в гео-радиусе вокруг центроида участка, руб/кв.м. Приоритет МЕЖДУ квартальной
            MV и ``district_price_median``. ``None`` → слой пропускается (back-compat).
        velocity_sqm_per_month: RANK 1 — темп абсорбции ОДНОГО типового конкурента
            района (``velocity.project_absorption_sqm_per_month``), м²/мес. Прокидывается
            в :func:`compute_financial` как ``market_velocity_sqm_per_month`` → драйвит
            окно продаж DCF. ``None`` → дефолт-30-мес график (``schedule_is_default``).

    Returns:
        ``dict`` = ``FinancialModel.model_dump()`` + bridge-метаданные
        (``housing_class_inferred`` / ``development_type_inferred`` / ``teap_synth`` /
        ``caveat`` / ``schedule_is_default``), либо ``None`` когда считать нельзя.
    """
    # ── Gate-guard: только при разрешённом МКД ─────────────────────────────────
    can_build = (gate_verdict or {}).get("can_build_mkd")
    if can_build is not True:
        # False (запрещено) или "unknown" (нет данных) — финмодель не строим.
        return None

    # ── Числа зоны обязательны ─────────────────────────────────────────────────
    if not nspd_zoning:
        return None
    max_far = _as_float(nspd_zoning.get("max_far"))
    max_building_pct = _as_float(nspd_zoning.get("max_building_pct"))
    max_floors = _as_int(nspd_zoning.get("max_floors"))
    # Нужен хотя бы КСИТ ИЛИ (%застройки + этажность) — иначе GFA не вывести.
    if max_far is None and not (max_building_pct is not None and max_floors is not None):
        return None

    # ── Площадь обязательна ────────────────────────────────────────────────────
    if area_m2 is None or area_m2 <= 0:
        return None

    # ── Цена продажи: quarter-MV → гео-радиус → district median (рыночный прокси) ─
    # Приоритет от точного рынка к нормативу. Гео-радиус вставлен МЕЖДУ квартальной MV
    # и district_reference: даёт реальную цену 5 не-name-match районам ЕКБ. Логика
    # вынесена в общий helper (single-source-of-truth с program_optimizer).
    price, price_source = select_calibrated_price(
        market_price=market_price,
        geo_radius_price_median=geo_radius_price_median,
        district_price_median=district_price_median,
    )

    housing_class = _infer_housing_class(price)
    development_type = _infer_development_type(max_floors)

    # ── Синтез ТЭП из предельных параметров зоны ───────────────────────────────
    teap = synthesize_teap_from_buildability(
        area_m2=area_m2,
        max_far=max_far,
        max_building_pct=max_building_pct,
        max_floors=max_floors,
        housing_class=housing_class,
    )
    if teap is None:
        return None

    # ── Полноценный статический каскад + помесячный DCF (PR-1/2/3) ──────────────
    fin = compute_financial(
        teap=teap,
        housing_class=housing_class,
        land_cost_rub=cadastral_value_rub,
        market_price_per_sqm=price,
        price_source=price_source,
        development_type=development_type,
        # RANK 1: окно продаж по рыночной абсорбции района (один типовой конкурент).
        market_velocity_sqm_per_month=velocity_sqm_per_month,
    )

    logger.info(
        "parcel financial bridge: area=%.0f far=%s floors=%s class=%s dev=%s "
        "gfa=%.0f resid=%.0f apts=%d npv=%.0f irr=%.3f%s sales_dur=%sm sched=%s",
        area_m2,
        max_far,
        max_floors,
        housing_class,
        development_type,
        teap.total_floor_area_sqm,
        teap.residential_area_sqm,
        teap.apartments_count,
        fin.npv_rub,
        fin.irr,
        " (proxy)" if fin.irr_is_proxy else "",
        fin.sales_duration_months,
        "default" if fin.schedule_is_default else "market",
    )

    return {
        # Полный FinancialModel-каскад (revenue/cost/net/roi/margin/irr/npv/pbp + цена).
        **fin.model_dump(),
        # ── Bridge-метаданные (синтез из buildability) ─────────────────────────
        "housing_class_inferred": housing_class,
        "development_type_inferred": development_type,
        "teap_synth": {
            "residential_area_sqm": teap.residential_area_sqm,
            "total_floor_area_sqm": teap.total_floor_area_sqm,
            "office_area_sqm": teap.office_area_sqm,
            "parking_spaces": teap.parking_spaces,
            "apartments_count": teap.apartments_count,
        },
        # HEAVY caveat — обязан показываться в UI/PDF рядом с цифрами.
        "caveat": _FINANCIAL_CAVEAT,
    }


def _as_float(value: Any) -> float | None:
    """Best-effort numeric coercion → ``float`` или ``None`` (нечисла/bool → None)."""
    if value is None or isinstance(value, bool):
        return None
    try:
        out = float(value)
    except (ValueError, TypeError):
        return None
    return out if math.isfinite(out) else None


def _as_int(value: Any) -> int | None:
    """Best-effort numeric coercion → ``int`` или ``None`` (нечисла/bool → None)."""
    f = _as_float(value)
    return int(f) if f is not None else None


__all__ = [
    "select_calibrated_price",
    "synthesize_parcel_financial",
    "synthesize_teap_from_buildability",
]
