"""IO contracts for the rule-based квартирография recommender.

POST /api/v1/analytics/recommend/mix
"""

from typing import Any, Literal

from pydantic import BaseModel, Field

ClassLiteral = Literal["Comfort", "Comfort+", "Business", "Elite"]


class RecommendMixInput(BaseModel):
    district_name: str = Field(..., min_length=2, max_length=80)
    area_total_m2: float | None = Field(default=None, ge=100, le=500_000)
    target_class: ClassLiteral | None = None
    months_window: int = Field(default=12, ge=3, le=24)
    # Velocity / pricing scenario knobs (live-tuned client-side; backend just
    # ships base coefficients so frontend can recompute without round-trips).
    # 0.01..3.0 = -99%..+200% к рынку. min=0.01 (а не 0) чтобы избежать
    # деления на 0 / pf^elasticity = ∞ при «бесплатной» цене.
    price_factor: float = Field(default=1.0, ge=0.01, le=3.0)
    target_months: int | None = Field(default=None, ge=3, le=120)
    # #982 (953-A) forecast-overlay opt-in. Оба None по умолчанию → живой микс
    # БАЙТ-в-БАЙТ как раньше (overlay не добавляется). horizon_months задан →
    # advisory overlay под scope["forecast"]; cad_num задан → demand_supply-режим
    # (с supply/конкурентами участка), иначе demand_only (только темп спроса).
    horizon_months: int | None = Field(default=None, ge=3, le=24)
    cad_num: str | None = Field(default=None, max_length=40)


class RecommendBucket(BaseModel):
    bucket: str
    share_pct: float
    deal_count: int
    area_avg_m2: float
    area_median_m2: float
    price_median_per_m2: float
    price_p25_per_m2: float
    price_p75_per_m2: float
    units_planned: int | None = None
    revenue_planned_rub: float | None = None
    # Velocity baseline (units/month for THIS project allocated to this bucket
    # at price_factor=1.0). Frontend scales by price_factor^elasticity for live
    # what-if recompute.
    velocity_per_month: float | None = None
    months_to_sellout: float | None = None
    # Per-bucket эластичность спроса по цене (#1204) — recommend_mix считает её
    # отдельно на каждую квартирографию (см. analytics_queries.py:2592-2606), фронт
    # скейлит velocity_per_month как `velocity * price_factor^elasticity` при движении
    # слайдера цены (RecommendVelocityPanel.tsx:68). Без этих полей в схеме
    # response_model молча их отбрасывал → фронт всегда падал на глобальную
    # эластичность, расходясь с серверным months_to_sellout и UI-таблицей сегментов.
    elasticity: float | None = None
    elasticity_r2: float | None = None
    elasticity_n: int | None = None
    elasticity_source: str | None = None
    # Источник базового темпа продаж (analytics_queries → 'hedonic'|'deals'|'fallback').
    velocity_source: str | None = None
    # Success-driven mix flag (issue #25): bucket has top success_score in district
    is_top_success: bool = False


class RecommendComparable(BaseModel):
    obj_id: int
    comm_name: str | None = None
    dev_name: str | None = None
    obj_class: str | None = None
    flat_count: int | None = None
    sold_perc: float | None = None
    cad_quarter: str | None = None
    lat: float | None = None
    lon: float | None = None
    buildings_n: int | None = None


class RecommendReason(BaseModel):
    """§16 структурная причина одного советующего пункта overlay (#983, 953-B). ADVISORY.

    Объясняет, ПОЧЕМУ пункт попал в совет, ЧТО на него повлияло и ЧТО его изменило бы:
      • `why` — RU-предложение (сегмент, deficit_index на горизонте, спрос/предложение).
      • `drivers` — драйверы сигнала ([{factor, value, direction}]).
      • `rejected` — отвергнутые альтернативы из РАНКИНГА (runner-up'ы / негатив-дефицит).
      • `what_would_change` — рычаги §9.x, которые сдвинули бы индекс (ставка/предложение/
        горизонт), полными RU-предложениями.
    `advisory` ВСЕГДА True (наследует advisory-статус §9.x). Все поля схемы — для
    explainability, НЕ основание для инвест-решения. Это «контракт-намёк»: overlay
    пробрасывает причины как открытые dict'ы (см. RecommendForecastSegment.reason).
    """

    why: str
    drivers: list[dict[str, Any]] = []
    rejected: list[dict[str, Any]] = []
    what_would_change: list[str] = []
    confidence: Literal["high", "medium", "low"]
    advisory: bool


class RecommendForecastSegment(BaseModel):
    """Одна ранжированная ячейка §9.7 forecast-overlay (#982). ADVISORY.

    `deficit_index`: в demand_supply-режиме ∈ [−1,+1] (>0 строить / <0 затоварка,
    supply-based); в demand_only-режиме ∈ (0,1] = ПРОКСИ относительной силы спроса
    (NOT supply-based — геометрии участка нет). `balance_units` (demand−supply)
    None в demand_only (предложение неизмеримо без cad_num). `reason` (#983, §16) —
    структурная причина ячейки (RecommendReason-shaped dict) или None.
    """

    bucket: str
    obj_class: str | None = None
    deficit_index: float
    balance_units: float | None = None
    confidence: Literal["high", "medium", "low"]
    reason: dict[str, Any] | None = None


class RecommendForecastOverlay(BaseModel):
    """§9.7 СОВЕТУЮЩИЙ forecast-overlay поверх живого микса (#982, 953-A; #983, 953-B).

    Кладётся ТОЛЬКО под scope["forecast"] при заданном horizon_months; живой микс
    (4 поля RecommendMixOutput) НЕ затрагивается. `advisory` ВСЕГДА True (наследует
    advisory-статус §9.x — не основание для инвест-решения). `mode`: demand_supply
    (cad_num задан — supply/конкуренты учтены) или demand_only (cad_num=None — только
    темп спроса; см. warnings).

    #983 (953-B) — ADDITIVE-расширения поверх #982 (старые поля/поведение НЕ тронуты):
      • `class_reco` — §10.2 рекомендация класса (сильнейший агрегатный дефицит) + §16
        reason, либо None (нет ранжированных данных).
      • `usp` — §10.5 USP-из-дефицитов: top-K самых недообеспеченных сегментов как
        «дефицит формата X — стройте его» (пустой при отсутствии данных).
      • `commercial` — §10.4 советующий коммерческий сигнал (доля коммерции) или
        degraded-honest {available: False, caveat, advisory} при тонких данных, либо None.
    """

    horizon_months: int
    mode: Literal["demand_supply", "demand_only"]
    advisory: bool
    ranked_segments: list[RecommendForecastSegment]
    warnings: list[str]
    class_reco: dict[str, Any] | None = None
    usp: list[dict[str, Any]] = []
    commercial: dict[str, Any] | None = None


class RecommendMixOutput(BaseModel):
    scope: dict[str, Any]
    buckets: list[RecommendBucket]
    summary: dict[str, Any]
    comparables: list[RecommendComparable]
