"""SQL-билдеры страницы «доля квартир дома в продаже» (мигр. 143; окно 45д мигр. 146).

Читаем готовый view v_building_sale_share — логику доли НЕ пересчитываем.
Два процента на дом:
  - sale_share_pct      — active_secondary / flat_count_effective (только активные);
  - sale_share_pct_45d  — distinct vtorichka за последние 45д (активные ИЛИ недавно
    закрытые) / flat_count_effective.
Дом проходит фильтр, если ЛЮБОЙ из двух процентов в диапазоне [min_pct, max_pct].

Все запросы параметризованы (SQLAlchemy text + bind params, CAST(:x AS type) —
psycopg v3). Сортировка/колонка гистограммы — строго по whitelist, user-ввод в SQL
не интерполируется.
"""

from __future__ import annotations

# share_desc / share_45d_desc / active_desc / count_45d_desc / exposure_desc /
# price_asc / price_desc → безопасный ORDER BY. house_id как стабильный tiebreaker
# для детерминированной пагинации.
_SORT_SQL: dict[str, str] = {
    "share_desc": "sale_share_pct DESC NULLS LAST, house_id",
    "share_45d_desc": "sale_share_pct_45d DESC NULLS LAST, house_id",
    "active_desc": "active_secondary DESC NULLS LAST, house_id",
    "count_45d_desc": "listings_45d DESC NULLS LAST, house_id",
    "exposure_desc": "avg_days_on_market DESC NULLS LAST, house_id",
    "price_asc": "median_price_rub ASC NULLS LAST, house_id",
    "price_desc": "median_price_rub DESC NULLS LAST, house_id",
}
ALLOWED_SORTS: frozenset[str] = frozenset(_SORT_SQL)

# Гистограмма распределения процента — фиксированный порядок корзин.
HISTOGRAM_BUCKETS: tuple[str, ...] = (
    "0-5",
    "5-10",
    "10-20",
    "20-30",
    "30-50",
    "50-100",
    "100+",
)

# Колонки view, по которым можно строить гистограмму (whitelist против инъекции).
_HISTOGRAM_COLUMNS: frozenset[str] = frozenset({"sale_share_pct", "sale_share_pct_45d"})

# Колонки view, отдаваемые в список (в порядке SELECT). median_* округляем до
# bigint — percentile_cont отдаёт double precision (может быть x.5), а Pydantic
# int-поле на дробном float падает. over_100 — implausible-флаг по любому из %.
_LIST_COLUMNS = (
    "house_id, "
    "COALESCE(short_address, full_address, address) AS address, "
    "lat, lon, "
    "sale_share_pct, "
    "sale_share_pct_45d, "
    "(sale_share_pct > 100 OR sale_share_pct_45d > 100) AS over_100, "
    "active_secondary, "
    "listings_45d, "
    "flat_count_effective, "
    "gar_match_method, "
    "CAST(round(median_price_rub) AS bigint) AS median_price_rub, "
    "CAST(round(median_price_per_m2) AS bigint) AS median_price_per_m2, "
    "avg_days_on_market, "
    "year_built, house_type, total_floors, series_name, is_emergency"
)


def build_sale_share_query(
    *,
    min_pct: float = 5.0,
    max_pct: float | None = None,
    city: str | None = None,
    price_min: int | None = None,
    price_max: int | None = None,
    year_min: int | None = None,
    year_max: int | None = None,
    house_type: str | None = None,
    sort: str = "share_desc",
    limit: int = 200,
) -> tuple[str, dict[str, object]]:
    """SELECT домов из v_building_sale_share по фильтрам.

    Дом проходит, если ЛЮБОЙ из двух процентов (now / 45д) попадает в диапазон
    [min_pct, max_pct]. NULL-процент (неправдоподобный знаменатель) не проходит
    сравнение, поэтому отдельный `IS NOT NULL` не нужен — рядки без обоих % выпадают.
    """
    where: list[str] = []
    args: dict[str, object] = {}

    # Каждая ветка требует, чтобы ИМЕННО её % был в диапазоне; building qualifies
    # on either. Без max_pct сводится к `(pct_now>=min OR pct_45d>=min)`.
    args["min_pct"] = min_pct
    qual_now = "sale_share_pct >= CAST(:min_pct AS numeric)"
    qual_45d = "sale_share_pct_45d >= CAST(:min_pct AS numeric)"
    if max_pct is not None:
        args["max_pct"] = max_pct
        qual_now += " AND sale_share_pct <= CAST(:max_pct AS numeric)"
        qual_45d += " AND sale_share_pct_45d <= CAST(:max_pct AS numeric)"
    where.append(f"(({qual_now}) OR ({qual_45d}))")

    if city:
        # case-insensitive substring по всем адресным полям (город может быть в любом).
        where.append(
            "(COALESCE(short_address, '') || ' ' || COALESCE(full_address, '') "
            "|| ' ' || COALESCE(address, '')) ILIKE CAST(:city_like AS text)"
        )
        args["city_like"] = f"%{city}%"

    if price_min is not None:
        where.append("median_price_rub >= CAST(:price_min AS bigint)")
        args["price_min"] = price_min
    if price_max is not None:
        where.append("median_price_rub <= CAST(:price_max AS bigint)")
        args["price_max"] = price_max

    if year_min is not None:
        where.append("year_built >= CAST(:year_min AS integer)")
        args["year_min"] = year_min
    if year_max is not None:
        where.append("year_built <= CAST(:year_max AS integer)")
        args["year_max"] = year_max

    if house_type:
        where.append("house_type = CAST(:house_type AS text)")
        args["house_type"] = house_type

    order_sql = _SORT_SQL.get(sort, _SORT_SQL["share_desc"])
    args["limit"] = max(1, min(int(limit), 1000))

    sql = (
        f"SELECT {_LIST_COLUMNS} "
        "FROM v_building_sale_share "
        f"WHERE {' AND '.join(where)} "
        f"ORDER BY {order_sql} "
        "LIMIT CAST(:limit AS integer)"
    )
    return sql, args


def build_listings_query(house_id: int) -> tuple[str, dict[str, object]]:
    """Уникальные активные вторичные квартиры одного дома (дедуп по сигнатуре
    (rooms, round(area_m2), floor) — та же, что в v_building_sale_share мигр.148: одна
    физическая квартира на avito+cian+domclick = одна строка, не 2-4). Представитель —
    самый дешёвый листинг; sources — все площадки, где квартира видна. Гео-фильтр ≤300м
    (мигр.150) против чужих листингов. Листинги/дома без geom — КЕЕП.

    Floors-guard (мигр.152, та же логика, что во view): отбрасываем листинги, чья
    этажность (l.total_floors) отличается от этажности дома
    (COALESCE(h.zhkh_floors, h.total_floors)) более чем на 3 — отсекает близких соседей
    в радиусе 300м (5-этажка vs соседние 9/16/19-этажные башни). Листинги с неизвестной
    этажностью листинга ИЛИ неизвестной этажностью дома — КЕЕП (режем только явный
    mismatch >3).

    DISTINCT ON-ключи (rooms, round(area_m2), floor) лидируют в ORDER BY, поэтому
    представитель детерминирован (price_rub ASC NULLS LAST, id → берётся самый дешёвый).
    round(area_m2), а не raw area_m2, используется и в DISTINCT ON, и в ORDER BY, и в
    GROUP BY src — иначе сигнатура разъехалась бы с мигр.148. Внешний SELECT
    пересортировывает уникальные квартиры по цене (контракт панели — дешёвые сверху).
    Join-ключи src — IS NOT DISTINCT FROM (NULL-safe), чтобы листинги с NULL
    rooms/area/floor не выпадали из дедупа.
    """
    sql = (
        "WITH active AS ("
        "  SELECT l.id, l.source, l.source_url, l.price_rub, l.price_per_m2, l.rooms, "
        "         l.area_m2, l.floor, l.total_floors, l.days_on_market, l.listing_date, "
        "         l.address "
        "  FROM listings l JOIN houses h ON h.id = l.house_id_fk "
        "  WHERE l.house_id_fk = CAST(:house_id AS bigint) "
        "    AND l.is_active AND l.listing_segment = 'vtorichka' "
        "    AND (l.geom IS NULL OR h.geom IS NULL "
        "         OR ST_DistanceSphere(l.geom, h.geom) <= 300)"
        "    AND (l.total_floors IS NULL "
        "         OR COALESCE(h.zhkh_floors, h.total_floors) IS NULL "
        "         OR abs(l.total_floors - COALESCE(h.zhkh_floors, h.total_floors)) <= 3)"
        "), src AS ("
        "  SELECT rooms, round(area_m2) AS sig_area, floor, "
        "         array_agg(DISTINCT source ORDER BY source) AS sources "
        "  FROM active GROUP BY rooms, round(area_m2), floor"
        "), dedup AS ("
        "  SELECT DISTINCT ON (a.rooms, round(a.area_m2), a.floor) "
        "    a.id AS listing_id, a.source, a.source_url, a.price_rub, a.price_per_m2, "
        "    a.rooms, CAST(a.area_m2 AS double precision) AS area_m2, a.floor, "
        "    a.total_floors, a.days_on_market, a.listing_date, a.address, s.sources "
        "  FROM active a "
        "  JOIN src s ON s.rooms IS NOT DISTINCT FROM a.rooms "
        "    AND s.sig_area IS NOT DISTINCT FROM round(a.area_m2) "
        "    AND s.floor IS NOT DISTINCT FROM a.floor "
        "  ORDER BY a.rooms, round(a.area_m2), a.floor, a.price_rub ASC NULLS LAST, a.id"
        ") "
        "SELECT listing_id, source, source_url, price_rub, price_per_m2, rooms, area_m2, "
        "floor, total_floors, days_on_market, listing_date, address, sources "
        "FROM dedup ORDER BY price_rub ASC NULLS LAST, listing_id"
    )
    return sql, {"house_id": house_id}


def build_summary_scalars_query() -> str:
    """Скаляры сводки: всего домов, с знаменателем (любой %), coverage, max/p95 обоих %."""
    return (
        "SELECT "
        "count(*) AS total_secondary_buildings, "
        "count(*) FILTER (WHERE sale_share_pct IS NOT NULL OR sale_share_pct_45d IS NOT NULL) "
        "AS buildings_with_denominator, "
        "COALESCE(round("
        "100.0 * count(*) FILTER "
        "(WHERE sale_share_pct IS NOT NULL OR sale_share_pct_45d IS NOT NULL) "
        "/ NULLIF(count(*), 0), 1), 0) AS coverage_pct, "
        "max(sale_share_pct) AS max_pct, "
        "percentile_cont(0.95) WITHIN GROUP (ORDER BY sale_share_pct) AS p95_pct, "
        "max(sale_share_pct_45d) AS max_pct_45d, "
        "percentile_cont(0.95) WITHIN GROUP (ORDER BY sale_share_pct_45d) AS p95_pct_45d "
        "FROM v_building_sale_share"
    )


def build_summary_histogram_query(column: str = "sale_share_pct") -> str:
    """Гистограмма по фиксированным корзинам (только non-null) для одного из двух %.

    column — whitelist {sale_share_pct, sale_share_pct_45d}; чужое → fallback на
    sale_share_pct (user-ввод в SQL не интерполируется).
    """
    col = column if column in _HISTOGRAM_COLUMNS else "sale_share_pct"
    return (
        "SELECT "
        "CASE "
        f"WHEN {col} < 5 THEN '0-5' "
        f"WHEN {col} < 10 THEN '5-10' "
        f"WHEN {col} < 20 THEN '10-20' "
        f"WHEN {col} < 30 THEN '20-30' "
        f"WHEN {col} < 50 THEN '30-50' "
        f"WHEN {col} <= 100 THEN '50-100' "
        "ELSE '100+' "
        "END AS bucket, "
        "count(*) AS count "
        "FROM v_building_sale_share "
        f"WHERE {col} IS NOT NULL "
        "GROUP BY bucket"
    )
