"""CRUD-сервис для user_custom_pois (#254).

API:
- create_custom_poi(db, user_id, payload) → CustomPoiOut
- list_custom_pois(db, user_id, parcel_cad?) → list[CustomPoiOut]
- get_custom_poi(db, poi_id, user_id) → CustomPoiOut | None
- update_custom_poi(db, poi_id, user_id, payload) → CustomPoiOut | None
- delete_custom_poi(db, poi_id, user_id) → bool
- get_overlaps_for_scoring(db, parcel_geom_wkt, user_id, parcel_cad?) → list[dict]

Паттерн: raw SQL через SQLAlchemy text() + CAST(:x AS type), psycopg v3.
"""

from __future__ import annotations

import logging
from typing import Any

from sqlalchemy import text

from app.schemas.custom_poi import CustomPoiCreate, CustomPoiOut, CustomPoiUpdate

logger = logging.getLogger(__name__)

# ── SQL ────────────────────────────────────────────────────────────────────────

_SELECT_COLS = """
    id, user_id, parcel_cad, name, category, weight, lon, lat, notes,
    created_at, updated_at
"""

_INSERT_SQL = f"""
    INSERT INTO user_custom_pois
        (user_id, parcel_cad, name, category, weight, lon, lat, notes)
    VALUES
        (:user_id, :parcel_cad, :name, :category,
         CAST(:weight AS real), CAST(:lon AS double precision),
         CAST(:lat AS double precision), :notes)
    RETURNING {_SELECT_COLS}
"""

_SELECT_BY_USER_ALL = f"""
    SELECT {_SELECT_COLS}
      FROM user_custom_pois
     WHERE user_id = :user_id
     ORDER BY created_at DESC
"""

_SELECT_BY_USER_PARCEL = f"""
    SELECT {_SELECT_COLS}
      FROM user_custom_pois
     WHERE user_id = :user_id
       AND (parcel_cad = :parcel_cad OR parcel_cad IS NULL)
     ORDER BY created_at DESC
"""

_SELECT_BY_ID = f"""
    SELECT {_SELECT_COLS}
      FROM user_custom_pois
     WHERE id = :poi_id
       AND user_id = :user_id
"""

_DELETE_SQL = """
    DELETE FROM user_custom_pois
     WHERE id = :poi_id
       AND user_id = :user_id
    RETURNING id
"""

# Запрос для scoring: custom POI в радиусе 1 км от центроида участка.
# Возвращает POI (global + parcel-specific) пользователя с расстоянием.
_OVERLAPS_SQL = """
    SELECT p.id, p.name, p.category, p.weight, p.lon, p.lat,
           ST_Distance(
               p.geom,
               ST_Centroid(ST_GeomFromText(CAST(:wkt AS text), 4326))::geography
           ) AS distance_m
      FROM user_custom_pois p
     WHERE p.user_id = :user_id
       AND (p.parcel_cad IS NULL OR p.parcel_cad = :parcel_cad)
       AND ST_DWithin(
               p.geom,
               ST_Centroid(ST_GeomFromText(CAST(:wkt AS text), 4326))::geography,
               1000
           )
     ORDER BY distance_m ASC
"""


# ── Row mapper ─────────────────────────────────────────────────────────────────


def _row_to_out(r: Any) -> CustomPoiOut:
    return CustomPoiOut(
        id=int(r["id"]),
        user_id=r["user_id"],
        parcel_cad=r["parcel_cad"],
        name=r["name"],
        category=r["category"],
        weight=float(r["weight"]),
        lon=float(r["lon"]),
        lat=float(r["lat"]),
        notes=r["notes"],
        created_at=r["created_at"],
        updated_at=r["updated_at"],
    )


# ── CRUD ───────────────────────────────────────────────────────────────────────


def create_custom_poi(db: Any, user_id: str, payload: CustomPoiCreate) -> CustomPoiOut:
    """Создать кастомную POI точку для пользователя."""
    row = (
        db.execute(
            text(_INSERT_SQL),
            {
                "user_id": user_id,
                "parcel_cad": payload.parcel_cad,
                "name": payload.name,
                "category": payload.category,
                "weight": payload.weight,
                "lon": payload.lon,
                "lat": payload.lat,
                "notes": payload.notes,
            },
        )
        .mappings()
        .first()
    )
    db.commit()
    assert row is not None, "INSERT RETURNING вернул пустой результат"
    logger.info(
        "custom_poi created: id=%s user=%s parcel=%s weight=%s",
        row["id"],
        user_id,
        payload.parcel_cad,
        payload.weight,
    )
    return _row_to_out(row)


def list_custom_pois(db: Any, user_id: str, parcel_cad: str | None = None) -> list[CustomPoiOut]:
    """Вернуть custom POI пользователя.

    Если parcel_cad задан — возвращает global (parcel_cad IS NULL) + parcel-specific.
    Если parcel_cad=None — возвращает все POI пользователя.
    """
    if parcel_cad is not None:
        rows = (
            db.execute(
                text(_SELECT_BY_USER_PARCEL),
                {"user_id": user_id, "parcel_cad": parcel_cad},
            )
            .mappings()
            .all()
        )
    else:
        rows = db.execute(text(_SELECT_BY_USER_ALL), {"user_id": user_id}).mappings().all()
    return [_row_to_out(r) for r in rows]


def get_custom_poi(db: Any, poi_id: int, user_id: str) -> CustomPoiOut | None:
    """Вернуть одну POI по id (scoped к user_id)."""
    row = db.execute(text(_SELECT_BY_ID), {"poi_id": poi_id, "user_id": user_id}).mappings().first()
    if row is None:
        return None
    return _row_to_out(row)


def update_custom_poi(
    db: Any, poi_id: int, user_id: str, payload: CustomPoiUpdate
) -> CustomPoiOut | None:
    """PATCH-style обновление кастомной POI. Возвращает None если не найдена."""
    existing = get_custom_poi(db, poi_id, user_id)
    if existing is None:
        return None

    sets: list[str] = ["updated_at = NOW()"]
    params: dict[str, Any] = {"poi_id": poi_id, "user_id": user_id}

    if payload.name is not None:
        sets.append("name = :name")
        params["name"] = payload.name

    if payload.category is not None:
        sets.append("category = :category")
        params["category"] = payload.category

    if payload.weight is not None:
        sets.append("weight = CAST(:weight AS real)")
        params["weight"] = payload.weight

    # lon/lat изменяем только вместе — geom GENERATED ALWAYS пересчитается автоматически
    if payload.lon is not None:
        sets.append("lon = CAST(:lon AS double precision)")
        params["lon"] = payload.lon

    if payload.lat is not None:
        sets.append("lat = CAST(:lat AS double precision)")
        params["lat"] = payload.lat

    if payload.parcel_cad is not None:
        sets.append("parcel_cad = :parcel_cad")
        params["parcel_cad"] = payload.parcel_cad

    if payload.notes is not None:
        sets.append("notes = :notes")
        params["notes"] = payload.notes

    if len(sets) > 1:
        db.execute(
            text(
                f"UPDATE user_custom_pois SET {', '.join(sets)}"
                " WHERE id = :poi_id AND user_id = :user_id"
            ),
            params,
        )
        db.commit()

    return get_custom_poi(db, poi_id, user_id)


def delete_custom_poi(db: Any, poi_id: int, user_id: str) -> bool:
    """Удалить кастомную POI. Возвращает True если удалена, False если не найдена."""
    result = db.execute(
        text(_DELETE_SQL),
        {"poi_id": poi_id, "user_id": user_id},
    ).first()
    if result is None:
        return False
    db.commit()
    logger.info("custom_poi deleted: id=%s user=%s", poi_id, user_id)
    return True


def get_overlaps_for_scoring(
    db: Any,
    parcel_geom_wkt: str,
    user_id: str,
    parcel_cad: str | None = None,
) -> list[dict[str, Any]]:
    """Вернуть custom POI в радиусе 1 км с расстоянием для scoring.

    Включает global POI (parcel_cad IS NULL) + parcel-specific если parcel_cad задан.
    Distance decay применяется в parcels.py аналогично OSM POI.

    Returns:
        list[dict] с ключами: id, name, category, weight, lon, lat, distance_m
    """
    _parcel_cad = parcel_cad or ""
    try:
        # SAVEPOINT — db shared с caller (POST .../analyze, Depends(get_db)). Голый
        # db.rollback() orphan'ит outer SessionTransaction (см. velocity.py:170) —
        # begin_nested откатывает только эту savepoint, оставляя db usable для
        # остальных запросов analyze_parcel на этой же сессии.
        with db.begin_nested():
            rows = (
                db.execute(
                    text(_OVERLAPS_SQL),
                    {"wkt": parcel_geom_wkt, "user_id": user_id, "parcel_cad": _parcel_cad},
                )
                .mappings()
                .all()
            )
        return [
            {
                "id": int(r["id"]),
                "name": r["name"],
                "category": r["category"],
                "weight": float(r["weight"]),
                "lon": float(r["lon"]),
                "lat": float(r["lat"]),
                "distance_m": float(r["distance_m"]),
            }
            for r in rows
        ]
    except Exception as e:
        logger.warning("get_overlaps_for_scoring failed user=%s: %s", user_id, e)
        return []
