"""POI weighted score для Site Finder (B6).

Формула: weight = (1 / (distance_m + 100)) * category_weight

Возвращает top-7 ближайших POI из osm_poi_ekb, отсортированных по weight DESC.
Категории и их веса согласованы с _POI_WEIGHTS в parcels.py.
"""

from __future__ import annotations

import logging
from typing import Any

from pydantic import BaseModel
from sqlalchemy import text

logger = logging.getLogger(__name__)

# Веса по категории — согласованы с _POI_WEIGHTS в parcels.py + новые из vault B6.
# Задача: "2GIS-style ranking", метро самое приоритетное.
CATEGORY_WEIGHTS: dict[str, float] = {
    "metro_stop": 6.0,
    "school": 5.0,
    "kindergarten": 4.5,
    "hospital": 4.0,
    "shop_supermarket": 3.5,
    "shop_mall": 4.0,
    "park": 3.5,
    "bus_stop": 4.5,
    "tram_stop": 2.0,
    "pharmacy": 2.5,
    "shop_small": 2.0,
    "default": 1.0,
}


class PoiScoreItem(BaseModel):
    """Один POI в ranked-ответе."""

    name: str | None
    category: str
    distance_m: float
    weight: float
    address: str | None


class PoiScoreResponse(BaseModel):
    cad_num: str
    radius_m: int
    top_poi: list[PoiScoreItem]


def _category_weight(category: str) -> float:
    """Вернуть вес категории. Если не знаем — default."""
    return CATEGORY_WEIGHTS.get(category, CATEGORY_WEIGHTS["default"])


def compute_poi_weighted_top7(
    db: Any,
    cad_num: str,
    lat: float,
    lon: float,
    radius_m: int = 2000,
    top_n: int = 7,
) -> PoiScoreResponse:
    """Найти top-N POI вокруг (lat, lon) в radius_m, ранжировать по weighted score.

    Запрос к osm_poi_ekb через ST_DWithin + ST_Distance.
    Формула: weight = (1 / (distance_m + 100)) * category_weight

    Args:
        db: SQLAlchemy Session
        cad_num: кадастровый номер (для ответа)
        lat: широта центроида участка
        lon: долгота центроида участка
        radius_m: радиус поиска в метрах (default 2000)
        top_n: количество POI в ответе (default 7)

    Returns:
        PoiScoreResponse с отсортированными по weight DESC POI.
    """
    # ST_DWithin с geography=true использует метры напрямую.
    # ST_Distance тоже в метрах при geography=true.
    rows = (
        db.execute(
            text("""
                SELECT
                    p.name,
                    p.category,
                    p.tags,
                    CAST(
                        ST_Distance(
                            p.geom::geography,
                            ST_SetSRID(ST_MakePoint(:lon, :lat), 4326)::geography
                        ) AS double precision
                    ) AS distance_m
                FROM osm_poi_ekb p
                WHERE ST_DWithin(
                    p.geom::geography,
                    ST_SetSRID(ST_MakePoint(:lon, :lat), 4326)::geography,
                    :radius_m
                )
                ORDER BY distance_m ASC
                LIMIT :limit
            """),
            {
                "lat": lat,
                "lon": lon,
                "radius_m": radius_m,
                "limit": top_n * 10,  # запрашиваем больше, потом ранжируем
            },
        )
        .mappings()
        .all()
    )

    logger.debug(
        "poi_score: cad=%s lat=%.5f lon=%.5f radius=%dm → %d candidates",
        cad_num,
        lat,
        lon,
        radius_m,
        len(rows),
    )

    items: list[PoiScoreItem] = []
    for row in rows:
        distance_m = float(row["distance_m"])
        category = row["category"] or "default"
        cat_weight = _category_weight(category)
        weight = (1.0 / (distance_m + 100.0)) * cat_weight

        # Адрес из tags jsonb если есть
        tags: dict[str, str] = row["tags"] or {}
        addr_parts = [
            tags.get("addr:street"),
            tags.get("addr:housenumber"),
        ]
        address = ", ".join(p for p in addr_parts if p) or None

        items.append(
            PoiScoreItem(
                name=row["name"],
                category=category,
                distance_m=round(distance_m, 1),
                weight=round(weight, 6),
                address=address,
            )
        )

    # Сортировка по weight DESC, берём top_n
    items.sort(key=lambda x: x.weight, reverse=True)
    top_items = items[:top_n]

    return PoiScoreResponse(
        cad_num=cad_num,
        radius_m=radius_m,
        top_poi=top_items,
    )
