"""Landing page stats endpoint — 5 KPI для hero-секции."""

from __future__ import annotations

import logging
import threading
import time
from datetime import date
from typing import Annotated, Any

from fastapi import APIRouter, Depends
from pydantic import BaseModel
from sqlalchemy import text
from sqlalchemy.orm import Session

from app.core.db import get_db

logger = logging.getLogger(__name__)

router = APIRouter()


# ---------------------------------------------------------------------------
# In-process TTL-кэш результата _query_stats (перф-батч #2, #1953-followup)
# ---------------------------------------------------------------------------
# /landing/stats бьётся на КАЖДЫЙ load хоумпейджа и НЕ кэширован. KPI 3 (доля квартир
# с ценой) делает полный агрегат по physflat-дедупу objective_lots: price_per_m2_rub
# нет в covering-INCLUDE и фильтра нет → ~6.7 s heap-скан 603k физлотов на каждый
# запрос. Данные дневной гранулярности (snapshot_date) → 1 ч staleness приемлем.
#
# Кэшируем ВЕСЬ dict (все 5 KPI), не только KPI 3 — там ещё pg_class deals + 2
# MAX(snapshot_date). Только УСПЕШНЫЙ результат (None/fallback НЕ кэшируем — пустая
# dev-БД / сбой запроса не должны залипать на час). Потокобезопасно (threading.Lock,
# удерживается только на check/store); analyze идёт в threadpool под Uvicorn. Стиль —
# зеркало app.services.weather_cache. time.monotonic устойчив к скачкам системных часов.
_STATS_TTL_S: float = 3600.0  # 1 час — дневная гранулярность данных

# (result_dict, expires_at_monotonic_seconds). Только успешный результат.
_STATS_CACHE: dict[str, Any] | None = None
_STATS_EXPIRES_AT: float = 0.0
_STATS_LOCK = threading.Lock()


def _now() -> float:
    """Монотонный таймер (тесты подменяют этот helper, а не сам `time`)."""
    return time.monotonic()


# ---------------------------------------------------------------------------
# Response schema
# ---------------------------------------------------------------------------


class LandingStatsOut(BaseModel):
    zk_total: int
    deals_total: int
    price_coverage_pct: float
    mapping_coverage_pct: float
    last_data_update: str
    paradox: str
    stale: bool = False


# ---------------------------------------------------------------------------
# Fallback defaults (на случай пустой dev БД или ошибки запроса)
# ---------------------------------------------------------------------------

_FALLBACK_DATA: dict[str, Any] = {
    "zk_total": 0,
    "deals_total": 0,
    "price_coverage_pct": 0.0,
    "mapping_coverage_pct": 0.0,
    "last_data_update": "",
    "paradox": "",
    "stale": True,
}


def _query_stats(db: Session) -> dict[str, Any] | None:
    """Выполняет агрегирующие запросы к БД. Возвращает None при любой ошибке."""
    try:
        # KPI 1: COUNT ЖК DOM.РФ (ЕКБ)
        row_zk = db.execute(
            text("SELECT COUNT(*) FROM domrf_kn_objects WHERE is_ekb = TRUE")
        ).scalar()
        zk_total = int(row_zk or 0)

        # KPI 2: COUNT ДДУ-сделок (через pg_class для партицированной таблицы)
        row_deals = db.execute(
            text(
                "SELECT COALESCE(SUM(CAST(reltuples AS bigint)), 0)"
                " FROM pg_class"
                " WHERE relname LIKE 'rosreestr_deals_%'"
                "   AND relkind = 'r'"
            )
        ).scalar()
        deals_total = int(row_deals or 0)

        # KPI 3: % objective_lots с ценой
        # #1964: physflat-дедуп view — доля квартир с ценой по ФИЗИЧЕСКИМ лотам, а не
        # по пере-листингам (сырой objective_lots раздут ~2.91× мульти lot_id/физлот).
        row_price = db.execute(
            text(
                "SELECT"
                "  COUNT(*) FILTER (WHERE price_per_m2_rub IS NOT NULL) * 100.0"
                "    / NULLIF(COUNT(*), 0)"
                " FROM v_objective_lots_latest"
            )
        ).scalar()
        price_coverage_pct = round(float(row_price or 0.0), 1)

        # KPI 4: % mapping (objective_complex_mapping / domrf_kn_objects ekb)
        row_mapping = db.execute(
            text(
                "SELECT"
                "  (SELECT COUNT(*) FROM objective_complex_mapping) * 100.0"
                "    / NULLIF("
                "        (SELECT COUNT(*) FROM domrf_kn_objects WHERE is_ekb = TRUE),"
                "        0"
                "      )"
            )
        ).scalar()
        mapping_coverage_pct = round(float(row_mapping or 0.0), 1)

        # KPI 5: дата последнего обновления (MAX snapshot_date из objective_lots + domrf)
        row_date_obj = db.execute(text("SELECT MAX(snapshot_date) FROM objective_lots")).scalar()
        row_date_domrf = db.execute(
            text("SELECT MAX(snapshot_date) FROM domrf_kn_objects")
        ).scalar()

        dates = [d for d in (row_date_obj, row_date_domrf) if d is not None]
        last_data_update: str
        if dates:
            last_data_update = str(max(dates))
        else:
            last_data_update = str(date.today())

        # Если все KPI нули — скорее всего пустая БД → stale
        if zk_total == 0 and deals_total == 0:
            logger.warning("landing/stats: all KPIs are zero — returning stale=True")
            return None

        return {
            "zk_total": zk_total,
            "deals_total": deals_total,
            "price_coverage_pct": price_coverage_pct,
            "mapping_coverage_pct": mapping_coverage_pct,
            "last_data_update": last_data_update,
            "paradox": (
                f"из {zk_total} ЖК у {price_coverage_pct}% есть цены"
                " — но в публичном доступе только 0.3%"
            ),
            "stale": False,
        }
    except Exception:
        logger.exception("landing/stats: DB query failed, returning stale=True")
        return None


def _query_stats_cached(db: Session) -> dict[str, Any] | None:
    """`_query_stats` с in-process TTL-кэшем (TTL=1 ч). См. блок-комментарий вверху.

    Кэширует ТОЛЬКО успешный dict; None (сбой/пустая БД) не кэшируется — следующий
    запрос попробует снова. Lock удерживается лишь на check/store; сам DB-запрос идёт
    ВНЕ lock'а (не сериализуем одновременные cold-start, last-write wins — запрос
    идемпотентен, TTL длинный).
    """
    global _STATS_CACHE, _STATS_EXPIRES_AT
    now = _now()
    with _STATS_LOCK:
        if _STATS_CACHE is not None and _STATS_EXPIRES_AT > now:
            return _STATS_CACHE
    # MISS / истёк → DB-запрос ВНЕ lock'а.
    result = _query_stats(db)
    if result is None:
        # Не кэшируем fallback — пусть следующий запрос повторит попытку.
        return None
    with _STATS_LOCK:
        _STATS_CACHE = result
        _STATS_EXPIRES_AT = _now() + _STATS_TTL_S
    return result


@router.get("/landing/stats", response_model=LandingStatsOut)
def landing_stats(
    db: Annotated[Session, Depends(get_db)],
) -> LandingStatsOut:
    """5 KPI для hero-секции лендинга.

    Поля ответа:
    - zk_total: количество ЖК DOM.РФ в ЕКБ
    - deals_total: суммарное кол-во ДДУ-сделок в индексе Росреестра
    - price_coverage_pct: % объектов objective_lots с ценой
    - mapping_coverage_pct: % маппинга objective → domrf_kn_objects (ЕКБ)
    - last_data_update: дата последнего обновления данных (YYYY-MM-DD)
    - paradox: строка-парадокс портфеля для hero CTA
    - stale: true если данные недоступны (DB ошибка или пустая БД)
    """
    result = _query_stats_cached(db)
    if result is None:
        return LandingStatsOut(**_FALLBACK_DATA)
    return LandingStatsOut(**result)
