from typing import Annotated, Any, Literal

from pydantic import BaseModel, Field


class BuildingProgramItem(BaseModel):
    """Stage 3a (#1965, эпик #1953) — один пункт программы застройки (типовой дом × N).

    Пользователь набирает программу из ТИПОВЫХ домов каталога
    (``app.services.generative.catalog.HOUSE_TYPES``) вместо max-FAR жадной раскладки:
    «поставь ``count`` секций типа ``section_type`` этажностью ``floors``». Габариты
    пятна секции берутся из каталога по ключу ``section_type`` (контракт несёт только
    ключ, не геометрию — единый справочник на бэке).
    """

    # Ключ типа дома из каталога (``HouseType.section_type``). Валидируется на слое
    # размещения по ``catalog.available_section_types`` — неизвестный ключ → 422.
    section_type: str = Field(..., description="Catalog house-type key (HOUSE_TYPES)")
    # Этажность этой группы секций; диапазон шире, чем у target_floors (до 40 — башни).
    floors: int = Field(..., ge=1, le=40, description="Floors for this house-type group")
    # Сколько секций этого типа разместить.
    count: int = Field(..., ge=1, le=50, description="Number of sections to place")
    # ADDITIVE (Stage 3c): опциональное ручное пятно секции (ширина × глубина, м).
    # None → габариты берутся из каталога по ``section_type`` (byte-for-byte
    # backward-compat). Заданы → place_program строит footprint из них вместо
    # каталожных (пользователь «вписал пятно»). Оба должны быть заданы вместе;
    # частичное задание (только w / только d) трактуется как «не задано» на слое
    # размещения. Границы [4, 120] м — вменяемый габарит секции/корпуса.
    footprint_w_m: float | None = Field(
        None, ge=4, le=120, description="Optional manual section footprint width, m"
    )
    footprint_d_m: float | None = Field(
        None, ge=4, le=120, description="Optional manual section footprint depth, m"
    )


class HouseTypeCatalogItem(BaseModel):
    """Stage 3b (#1965, эпик #1953) — один тип дома каталога, ОТДАВАЕМЫЙ фронту.

    Read-only проекция ``app.services.generative.catalog.HouseType`` для пикера
    Stage 3b: фронт показывает ``label_ru`` + габариты пятна + дефолтную этажность
    + класс, а в ``BuildingProgramItem.section_type`` кладёт ``section_type``. Это
    делает каталог ЕДИНЫМ источником истины — фронт ничего не хардкодит. Поля
    зеркалят dataclass на бэке (см. ``GET /api/v1/concepts/house-types``).
    """

    # Стабильный машинный ключ типа — значение для BuildingProgramItem.section_type.
    section_type: str = Field(..., description="Catalog house-type key (HOUSE_TYPES)")
    # Человекочитаемый русский лейбл для UI.
    label_ru: str = Field(..., description="Human-readable Russian label")
    # Габариты пятна секции, метры (ширина × глубина).
    footprint_w_m: float = Field(..., description="Section footprint width, m")
    footprint_d_m: float = Field(..., description="Section footprint depth, m")
    # Площадь пятна секции, кв.м (helper для UI: ширина × глубина).
    footprint_sqm: float = Field(..., description="Section footprint area, m² (w × d)")
    # Дефолтная этажность типа — UI подставляет, пользователь меняет в [1, 40].
    default_floors: int = Field(..., description="Default floors for this type")
    # Подходящий класс жилья (econom / comfort / business).
    housing_class: Literal["econom", "comfort", "business"] = Field(
        ..., description="Suitable housing class"
    )


class HouseTypeCatalog(BaseModel):
    """Stage 3b (#1965) — каталог типовых домов, отдаваемый ``GET /concepts/house-types``."""

    house_types: list[HouseTypeCatalogItem]


class ConceptInput(BaseModel):
    """Stage 1a — input contract. Frozen interface for frontend codegen."""

    parcel_geojson: dict[str, Any] = Field(
        ..., description="GeoJSON Polygon of the parcel (WGS84 / EPSG:4326)"
    )
    housing_class: Literal["econom", "comfort", "business"] = "comfort"
    target_floors: int = Field(9, ge=1, le=30)
    development_type: Literal["spot", "mid_rise", "high_rise"] = "mid_rise"
    land_cost_rub: float | None = Field(
        None, ge=0, description="Optional land cost for financial model"
    )
    # Stage 3a (#1965): ОПЦИОНАЛЬНАЯ программа застройки из типовых домов каталога.
    # ADDITIVE поле (default None). None → существующая жадная раскладка (Stage 1b,
    # byte-for-byte backward-compat). Задано → раскладка кладёт РОВНО эту программу
    # (place_program): для каждого пункта ставит count секций каталожного пятна с его
    # floors, вместо coverage-cap sweep. См. app.services.generative.placement.
    building_program: list[BuildingProgramItem] | None = Field(
        None,
        description="Optional typed house program; None → greedy max-FAR placement",
    )


class TEAP(BaseModel):
    """Технико-экономические показатели."""

    built_area_sqm: float
    total_floor_area_sqm: float
    # Нежилое (коммерция/офисы 1-го этажа) = GFA × нормативная доля по классу
    # (см. ``_OFFICE_SHARE_OF_GFA`` в teap.py). Вырезается из GFA до расчёта жилой —
    # жилая ужимается ровно на эту площадь, total_floor_area_sqm не меняется. Additive
    # поле (default 0.0) → backward-compat: 0.0 = нежилое не моделировалось/не нужно.
    office_area_sqm: float = 0.0
    residential_area_sqm: float
    apartments_count: int
    density: float
    parking_spaces: int


class FinancialModel(BaseModel):
    """Static developer P&L + monthly-DCF investment metrics (PR-1/PR-3, эпик #1881).

    Full static cost cascade + VAT + profit tax → net profit + ROI (PR-1). On top
    of that static P&L, PR-3 lays the cascade onto a **monthly cashflow schedule**
    (default phase norms — ПИР → СМР → продажи) and runs a real DCF: NPV, IRR
    (bisection over the monthly cashflow) and *undiscounted* cumulative payback.
    See ``compute_financial`` docstring for the schedule assumptions and the
    налоги/рассрочка phasing choices.

    ``irr`` is a **real** annualised DCF IRR when ``irr_is_proxy is False``; for a
    degenerate cashflow with no sign change (e.g. always-loss) the bisection cannot
    bracket a root, so we fall back to the PR-1 annualised-ROI proxy and flag
    ``irr_is_proxy=True``. VAT is a documented simplification (see docstring), not
    exact НК РФ mechanics.

    ``levered_irr`` — IRR на собственные средства (equity cashflow при LTC): тот же
    DCF-IRR + roi-proxy fallback, но по equity-потоку графика долга. При положительном
    леверидже выше unlevered ``irr``; вырожденные случаи → 0.0 + ``levered_irr_is_proxy``.

    The legacy summary fields (``revenue_rub`` / ``cost_rub`` /
    ``gross_margin_rub`` / ``irr``) are kept for backward-compat; the new fields
    expose the full cascade, the net P&L and the DCF metrics.
    """

    # ── Legacy summary (backward-compat) ───────────────────────────────────────
    revenue_rub: float
    cost_rub: float
    gross_margin_rub: float
    irr: float

    # ── Revenue breakdown ──────────────────────────────────────────────────────
    revenue_residential_rub: float
    revenue_parking_rub: float
    # Выручка нежилого (коммерция/офисы 1-го этажа) = office_area_sqm × цена нежилого
    # (см. ``_OFFICE_SALE_PRICE_PER_SQM`` в financial.py). Чисто ADDITIVE выручка:
    # СМР нежилого уже сидит в ``construction_rub`` (нежилая GFA входит в total_floor_area
    # → construction = total_floor_area × ставка уже её покрывает; double-count'а нет).
    # Нежилое ОБЛАГАЕТСЯ НДС (в отличие от льготы жилья ст.149 НК) — его value-added
    # входит в базу НДС. Default 0.0 → backward-compat (нежилое не моделировалось).
    revenue_office_rub: float = 0.0

    # ── Cost cascade ───────────────────────────────────────────────────────────
    construction_rub: float
    pir_rub: float
    networks_rub: float
    developer_services_rub: float
    contingency_rub: float
    marketing_rub: float
    land_rub: float

    # ── БДР / taxes → net profit ───────────────────────────────────────────────
    vat_rub: float
    profit_before_tax_rub: float
    profit_tax_rub: float
    net_profit_rub: float

    # ── Metrics ────────────────────────────────────────────────────────────────
    roi: float  # net profit / total cost
    margin_pct: float  # net profit / revenue
    irr_is_proxy: bool = True  # frontend caveat: True → IRR is the ROI-proxy, not DCF

    # ── DCF / investment metrics (PR-3, эпик #1881) ────────────────────────────
    # NPV дисконтированного помесячного cashflow по ставке ``discount_rate_used``.
    npv_rub: float = 0.0
    # Окупаемость (мес) по НЕдисконтированному накопительному cashflow, линейная
    # интерполяция дробного месяца. ``None`` — проект не окупается на горизонте.
    payback_months: float | None = None
    # Годовая ставка дисконтирования, фактически применённая в NPV (норматив).
    discount_rate_used: float = 0.0
    # График фаз/продаж — типовые нормативные ДОПУЩЕНИЯ (не график конкретного
    # проекта). True → UI/PDF показывают caveat «график — типовое допущение». RANK 1:
    # False, когда окно продаж построено по реальной рыночной скорости абсорбции
    # района (а не дефолт-30-мес).
    schedule_is_default: bool = True
    # RANK 1: реализованное окно продаж (sales_end − sales_start), мес — для UI/PDF.
    # Драйвится рыночной абсорбцией района, когда ``schedule_is_default is False``;
    # иначе дефолт-норматив (с эскроу-привязкой к завершению стройки). ``None`` —
    # финмодель не строилась/окно неизвестно.
    sales_duration_months: float | None = None

    # ── Financing overlay (PR-5, эпик #1881) ───────────────────────────────────
    # OVERLAY поверх unlevered cashflow: кредит покрывает кассовый разрыв, проценты
    # капитализируются (compound). Unlevered NPV/IRR (headline) НЕ затронуты.
    financing_enabled: bool = False  # True когда overlay посчитан
    annual_rate_used: float = 0.0  # _CREDIT_RATE_ANNUAL (норматив, отдельно от дисконта)
    peak_debt_rub: float = 0.0  # «задолженность по кредиту» Excel — пик долга
    total_interest_rub: float = 0.0  # «поток по финдеятельности» Excel — Σ процентов
    net_profit_after_financing_rub: float = 0.0  # net_profit − total_interest
    # honest-флаг: 100% покрытие разрыва, compound %, эскроу не моделируется точно.
    financing_is_simplified: bool = True
    # Levered (equity) IRR: доходность на собственные средства (equity cashflow при LTC).
    # При положительном леверидже > unlevered ``irr``. Считается DCF-IRR'ом equity-потока
    # того же графика долга, с тем же roi-proxy fallback'ом, что и unlevered IRR.
    levered_irr: float = 0.0  # IRR на собственные средства (equity cashflow при LTC)
    # honest-флаг: True → levered_irr — roi-proxy (вырожденный equity-поток / нет equity
    # под риском при LTC>=1 / финансирование выключено), не настоящий DCF-IRR.
    levered_irr_is_proxy: bool = True  # True → levered_irr — roi-proxy, не DCF

    # ── Price calibration (PR-2, эпик #1881) ───────────────────────────────────
    # Цена продажи жилья, руб/кв.м, фактически использованная в выручке. Либо
    # калиброванная по рынку (Objective / district reference), либо норматив класса.
    # ``price_source`` honest-flag для UI/PDF: не выдавать норматив за рынок.
    price_per_sqm_used: float
    price_is_calibrated: bool = False
    # Значения: "objective_district_median" | "district_reference" | "class_norm".
    price_source: str = "class_norm"


class ConceptVariant(BaseModel):
    strategy: Literal["max_area", "max_insolation", "balanced"]
    buildings_geojson: dict[str, Any]
    teap: TEAP
    financial: FinancialModel
    # Stage 3a (#1965) — честный сигнал частичного размещения для program-режима.
    # Когда задан ``ConceptInput.building_program``: ``requested_count`` — сколько секций
    # просили (Σ count по программе), ``placed_count`` — сколько реально влезло в участок.
    # placed < requested → участок мал, разместилось N из M (фронт Stage 3b показывает
    # «разместилось N из M», без hard-422). Оба None в greedy-режиме (программа не задана)
    # → ADDITIVE, backward-compat: старый ответ концепции не меняется. Annotated+`= None`:
    # рантайм-дефолт виден mypy (без pydantic-плагина), Field несёт только OpenAPI-описание.
    placed_count: Annotated[
        int | None, Field(description="Stage 3a: sections actually placed (program mode only)")
    ] = None
    requested_count: Annotated[
        int | None, Field(description="Stage 3a: sections requested by program (program mode only)")
    ] = None


class ConceptOutput(BaseModel):
    variants: list[ConceptVariant]


class MassingProgram(BaseModel):
    """Stage 2a (#1965) — агрегированная массинг-программа для LIVE-пересчёта экономики.

    Контракт POST ``/api/v1/concepts/recompute``: вход — уже СВЁРНУТАЯ программа из
    фронтового ``computeModel`` интерактивного 3D-массинга (суммарное пятно × этажность),
    без покомпонентной геометрии секций. Эндпоинт синтезирует из неё :class:`TEAP` и
    прогоняет готовый ``compute_financial`` → пересчитанная экономика для дебаунс-слайдеров
    Stage 2b.

    Класс жилья / тип застройки — те же Literal-множества, что у :class:`ConceptInput`
    и ``compute_financial`` (single source of truth по допустимым значениям).
    """

    total_footprint_sqm: float = Field(
        ..., ge=0, description="Суммарное пятно застройки всех секций, кв.м (скаляр)"
    )
    floors: int = Field(..., ge=1, le=40, description="Этажность программы")
    sections: int = Field(1, ge=1, le=6, description="Число секций (метаданные программы)")
    site_area_sqm: float = Field(..., ge=0, description="Площадь участка для плотности (FAR)")
    housing_class: Literal["econom", "comfort", "business"] = "comfort"
    development_type: Literal["spot", "mid_rise", "high_rise"] = "mid_rise"
    land_cost_rub: float | None = Field(
        None, ge=0, description="Стоимость участка для финмодели (опционально)"
    )
    # Предрезолвленная рыночная цена жилья, руб/кв.м — позволяет hot-эндпоинту ПРОПУСТИТЬ
    # per-keystroke DB-lookup (фронт резолвит цену один раз и шлёт её в каждом запросе).
    # None → эндпоинт сам сделает _lookup_market_price по центроиду (медленный путь).
    market_price_per_sqm: float | None = Field(
        None, ge=0, description="Предрезолвленная рыночная цена жилья, руб/кв.м (skip DB lookup)"
    )
    # ПОДЛИННЫЙ источник предрезолвленной цены, который фронт получил из financial_estimate
    # (objective_geo_radius / objective_district_median / district_reference / class_norm).
    # Honest-флаг для UI/PDF: НЕ подменяем его захардкоженным лейблом. Применяется ТОЛЬКО
    # вместе с market_price_per_sqm; None → дефолтный лейбл (см. эндпоинт recompute_massing).
    price_source: str | None = Field(
        None, description="Подлинный источник предрезолвленной цены (forward из financial_estimate)"
    )
    # Центроид участка (WKT-точка WGS84) для DB-fallback цены, когда market_price_per_sqm
    # не передан. None → пропускаем lookup и берём норматив класса (нет геопривязки).
    parcel_centroid_wkt: str | None = Field(
        None, description="WKT-точка центроида участка (WGS84) для DB-резолва цены при fallback"
    )


class MassingRecomputeOutput(BaseModel):
    """Stage 2a (#1965) — результат LIVE-пересчёта: синтезированный ТЭП + финмодель."""

    teap: TEAP
    financial: FinancialModel
