"""SQL builder for /api/v1/search — parameterized, psycopg v3 (CAST(:x AS type))."""

from __future__ import annotations

from app.schemas.search import SearchParams

_SORT_SQL: dict[str, str] = {
    "price_asc": "price_rub ASC NULLS LAST",
    "price_desc": "price_rub DESC NULLS LAST",
    "area_desc": "total_area DESC NULLS LAST",
    "area_asc": "total_area ASC NULLS LAST",
    "date_desc": "scraped_at DESC NULLS LAST",
    "dist_asc": (
        "ST_Distance(geom::geography, "
        "ST_MakePoint(CAST(:lon AS double precision), "
        "CAST(:lat AS double precision))::geography) ASC"
    ),
}

# Сегмент рынка (#1188). listings_search_mv не несёт колонку listing_segment,
# поэтому фильтруем через подзапрос к базовой таблице listings, переиспользуя
# canon-предикат #1186: NULL = legacy вторичка до миграции 011.
_VTORICHKA_GUARD = "(listing_segment IS NULL OR listing_segment = 'vtorichka')"
_SEGMENT_SQL: dict[str, str | None] = {
    "vtorichka": (f"listing_id IN (SELECT id FROM listings WHERE {_VTORICHKA_GUARD})"),
    "novostroyki": (
        "listing_id IN (SELECT id FROM listings WHERE listing_segment = 'novostroyki')"
    ),
    "all": None,
}


def build_search_query(params: SearchParams) -> tuple[str, dict[str, object]]:
    """Возвращает (sql, args) для SELECT из listings_search_mv."""
    where: list[str] = ["1=1"]
    args: dict[str, object] = {}

    if params.lat is not None and params.lon is not None:
        where.append(
            "ST_DWithin(geom::geography, "
            "ST_MakePoint(CAST(:lon AS double precision), "
            "CAST(:lat AS double precision))::geography, "
            "CAST(:radius_m AS integer))"
        )
        args["lat"] = params.lat
        args["lon"] = params.lon
        args["radius_m"] = params.radius_m

    if params.rooms is not None:
        where.append("rooms = CAST(:rooms AS integer)")
        args["rooms"] = params.rooms
    if params.rooms_in:
        where.append("rooms = ANY(CAST(:rooms_in AS integer[]))")
        args["rooms_in"] = params.rooms_in

    if params.area_m2_min is not None:
        where.append("total_area >= CAST(:area_min AS double precision)")
        args["area_min"] = params.area_m2_min
    if params.area_m2_max is not None:
        where.append("total_area <= CAST(:area_max AS double precision)")
        args["area_max"] = params.area_m2_max

    if params.price_rub_min is not None:
        where.append("price_rub >= CAST(:price_min AS bigint)")
        args["price_min"] = params.price_rub_min
    if params.price_rub_max is not None:
        where.append("price_rub <= CAST(:price_max AS bigint)")
        args["price_max"] = params.price_rub_max
    if params.price_per_m2_max is not None:
        where.append("price_per_m2 <= CAST(:ppm2_max AS bigint)")
        args["ppm2_max"] = params.price_per_m2_max

    if params.floor_min is not None:
        where.append("floor >= CAST(:floor_min AS integer)")
        args["floor_min"] = params.floor_min
    if params.floor_max is not None:
        where.append("floor <= CAST(:floor_max AS integer)")
        args["floor_max"] = params.floor_max

    if params.year_built_min is not None:
        where.append("year_built >= CAST(:yb_min AS integer)")
        args["yb_min"] = params.year_built_min
    if params.year_built_max is not None:
        where.append("year_built <= CAST(:yb_max AS integer)")
        args["yb_max"] = params.year_built_max

    if params.house_class:
        where.append("house_class = ANY(CAST(:hclass AS text[]))")
        args["hclass"] = params.house_class

    if params.floors_total_min is not None:
        where.append("total_floors >= CAST(:fl_total_min AS integer)")
        args["fl_total_min"] = params.floors_total_min
    if params.floors_total_max is not None:
        where.append("total_floors <= CAST(:fl_total_max AS integer)")
        args["fl_total_max"] = params.floors_total_max

    if params.has_kadastr:
        where.append("cadastral_number IS NOT NULL")

    segment_clause = _SEGMENT_SQL[params.segment]
    if segment_clause is not None:
        where.append(segment_clause)

    if params.sources:
        where.append("sources && CAST(:sources AS text[])")
        args["sources"] = params.sources
    if params.multi_source_only:
        where.append("source_count >= 2")
    if params.require_avito:
        where.append("has_avito = true")
    if params.require_cian:
        where.append("has_cian = true")
    if params.require_yandex:
        where.append("has_yandex = true")

    if params.address_query:
        where.append("address ILIKE CAST(:addr_like AS text)")
        args["addr_like"] = f"%{params.address_query}%"
    if params.description_query:
        where.append("tsv @@ plainto_tsquery('russian', CAST(:descq AS text))")
        args["descq"] = params.description_query

    where_sql = " AND ".join(where)
    order_sql = _SORT_SQL[params.sort]

    sql = (
        "SELECT listing_id, source, source_url, address, lat, lng, "
        "rooms, total_area, floor, total_floors, price_rub, price_per_m2, "
        "cadastral_number, scraped_at, "
        "house_id, year_built, house_class, developer_name, "
        "house_rating, house_ratings_count, "
        "source_count, sources, has_avito, has_cian, has_yandex, "
        "house_median_ppm2, district "
        "FROM listings_search_mv "
        f"WHERE {where_sql} "
        f"ORDER BY {order_sql} "
        "LIMIT CAST(:limit AS integer) OFFSET CAST(:offset AS integer)"
    )
    args["limit"] = params.page_size
    args["offset"] = params.offset

    return sql, args


def build_count_query(params: SearchParams) -> tuple[str, dict[str, object]]:
    """COUNT(*) — отдельный запрос для total."""
    sql, args = build_search_query(params)
    where_start = sql.find("WHERE ")
    order_start = sql.find(" ORDER BY ")
    where_clause = sql[where_start:order_start]
    count_args = {k: v for k, v in args.items() if k not in ("limit", "offset")}
    count_sql = f"SELECT count(*) AS total FROM listings_search_mv {where_clause}"
    return count_sql, count_args
