"""User weight profiles для Site Finder POI scoring.

Per #114 (Макс feedback): кастомизируемые веса для каждой POI категории.
Table schema: data/sql/90_user_weight_profiles.sql.

API surface:
- list_profiles(db, user_id) → list[WeightProfile]
- get_profile(db, user_id, profile_id) → WeightProfile | None
- get_default_profile(db, user_id) → WeightProfile | None
- create_profile(db, payload) → WeightProfile
- update_profile(db, user_id, profile_id, payload) → WeightProfile | None
- delete_profile(db, user_id, profile_id) → bool
- resolve_weights(db, user_id, profile_id) → dict[str, float]
"""

from __future__ import annotations

import json
import logging
import math
from datetime import datetime
from typing import Any

from pydantic import BaseModel, Field, field_validator
from sqlalchemy import text

logger = logging.getLogger(__name__)

# Sentinel user_id для системных preset-профилей (не привязаны к реальному пользователю).
# Seed: data/sql/100_user_weight_profiles_default_seed.sql
SYSTEM_USER_ID: str = "__system__"

# Allowed POI categories — single source of truth; imported by api/v1/parcels.py
ALLOWED_CATEGORIES: set[str] = {
    "school",
    "kindergarten",
    "pharmacy",
    "hospital",
    "shop_mall",
    "shop_supermarket",
    "shop_small",
    "park",
    "bus_stop",
    "metro_stop",
    "tram_stop",
}

# Weight value bounds (per #114 spec)
MIN_WEIGHT: float = -2.0
MAX_WEIGHT: float = 3.0

# System defaults — single source of truth; imported as _POI_WEIGHTS by api/v1/parcels.py
_SYSTEM_POI_WEIGHTS: dict[str, float] = {
    "school": 1.5,
    "kindergarten": 1.5,
    "pharmacy": 0.8,
    "hospital": 0.6,
    "shop_mall": 1.2,
    "shop_supermarket": 1.0,
    "shop_small": 0.5,
    "park": 1.8,
    "bus_stop": 0.3,
    "metro_stop": 1.5,
    "tram_stop": -0.5,
}

# ── SQL helpers ────────────────────────────────────────────────────────────────

_SELECT_COLS = """
    id, user_id, profile_name, weights, is_default, description,
    created_at, updated_at
"""

_SELECT_BY_USER = f"""
    SELECT {_SELECT_COLS}
      FROM user_weight_profiles
     WHERE user_id = :user_id
     ORDER BY is_default DESC, id ASC
"""

_SELECT_BY_USER_WITH_SYSTEM = f"""
    SELECT {_SELECT_COLS}
      FROM user_weight_profiles
     WHERE user_id = :user_id
        OR user_id = :system_user_id
     ORDER BY
         CASE WHEN user_id = :system_user_id THEN 1 ELSE 0 END ASC,
         is_default DESC,
         id ASC
"""

_SELECT_BY_ID = f"""
    SELECT {_SELECT_COLS}
      FROM user_weight_profiles
     WHERE user_id = :user_id
       AND id = :profile_id
"""

_SELECT_DEFAULT = f"""
    SELECT {_SELECT_COLS}
      FROM user_weight_profiles
     WHERE user_id = :user_id
       AND is_default = TRUE
     LIMIT 1
"""

_UNSET_DEFAULT = """
    UPDATE user_weight_profiles
       SET is_default = FALSE
     WHERE user_id = :user_id
       AND is_default = TRUE
"""

_INSERT = """
    INSERT INTO user_weight_profiles
        (user_id, profile_name, weights, is_default, description)
    VALUES
        (:user_id, :profile_name, CAST(:weights AS jsonb), :is_default, :description)
    RETURNING id, created_at, updated_at
"""


# ── Pydantic models ────────────────────────────────────────────────────────────


def _validate_weights_dict(v: dict[str, float]) -> dict[str, float]:
    """Shared validation logic for weights dict."""
    bad_keys = set(v.keys()) - ALLOWED_CATEGORIES
    if bad_keys:
        raise ValueError(f"Unknown POI categories: {sorted(bad_keys)}")
    for k, w in v.items():
        if not isinstance(w, int | float):
            raise ValueError(f"Weight for '{k}' must be number, got {type(w).__name__}")
        if not math.isfinite(w) or w < MIN_WEIGHT or w > MAX_WEIGHT:
            raise ValueError(f"Weight for '{k}' = {w} out of bounds [{MIN_WEIGHT}, {MAX_WEIGHT}]")
    return v


class WeightProfileBase(BaseModel):
    profile_name: str = Field(..., min_length=1, max_length=64)
    weights: dict[str, float] = Field(default_factory=dict)
    is_default: bool = False
    description: str | None = None

    @field_validator("weights")
    @classmethod
    def _validate_weights(cls, v: dict[str, float]) -> dict[str, float]:
        return _validate_weights_dict(v)


class WeightProfileCreate(WeightProfileBase):
    user_id: str = Field(..., min_length=1, max_length=128)


class WeightProfileUpdate(BaseModel):
    profile_name: str | None = Field(None, min_length=1, max_length=64)
    weights: dict[str, float] | None = None
    is_default: bool | None = None
    description: str | None = None

    @field_validator("weights")
    @classmethod
    def _validate_weights(cls, v: dict[str, float] | None) -> dict[str, float] | None:
        if v is None:
            return v
        return _validate_weights_dict(v)


class WeightProfile(WeightProfileBase):
    id: int
    user_id: str
    created_at: datetime
    updated_at: datetime

    model_config = {"from_attributes": True}


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


def _row_to_profile(r: Any) -> WeightProfile:
    """Map SQLAlchemy mapping row → WeightProfile."""
    weights_raw = r["weights"]
    if isinstance(weights_raw, str):
        weights_raw = json.loads(weights_raw)
    weights: dict[str, float] = weights_raw or {}

    return WeightProfile(
        id=r["id"],
        user_id=r["user_id"],
        profile_name=r["profile_name"],
        weights=weights,
        is_default=bool(r["is_default"]),
        description=r["description"],
        created_at=r["created_at"],
        updated_at=r["updated_at"],
    )


# ── CRUD service ───────────────────────────────────────────────────────────────


def list_profiles(db: Any, user_id: str) -> list[WeightProfile]:
    """Вернуть все профили пользователя, default первым."""
    rows = db.execute(text(_SELECT_BY_USER), {"user_id": user_id}).mappings().all()
    return [_row_to_profile(r) for r in rows]


def list_profiles_with_system(db: Any, user_id: str) -> list[WeightProfile]:
    """Вернуть профили пользователя + системные preset-профили.

    Пользовательские профили идут первыми (default сверху), затем системные
    presets (Эконом, Комфорт, Бизнес). Предназначен для endpoint с
    include_system=true — UI dropdown видит и пользовательские, и preset.
    """
    rows = (
        db.execute(
            text(_SELECT_BY_USER_WITH_SYSTEM),
            {"user_id": user_id, "system_user_id": SYSTEM_USER_ID},
        )
        .mappings()
        .all()
    )
    return [_row_to_profile(r) for r in rows]


def get_profile(db: Any, user_id: str, profile_id: int) -> WeightProfile | None:
    """Вернуть профиль по id (scoped к пользователю)."""
    row = (
        db.execute(text(_SELECT_BY_ID), {"user_id": user_id, "profile_id": profile_id})
        .mappings()
        .first()
    )
    if row is None:
        return None
    return _row_to_profile(row)


def get_default_profile(db: Any, user_id: str) -> WeightProfile | None:
    """Вернуть профиль-default пользователя или None."""
    row = db.execute(text(_SELECT_DEFAULT), {"user_id": user_id}).mappings().first()
    if row is None:
        return None
    return _row_to_profile(row)


def create_profile(db: Any, payload: WeightProfileCreate) -> WeightProfile:
    """Создать новый профиль.

    Если is_default=True — снять is_default у всех существующих профилей
    пользователя в одной транзакции.
    """
    if payload.is_default:
        db.execute(text(_UNSET_DEFAULT), {"user_id": payload.user_id})

    result = (
        db.execute(
            text(_INSERT),
            {
                "user_id": payload.user_id,
                "profile_name": payload.profile_name,
                "weights": json.dumps(payload.weights, ensure_ascii=False),
                "is_default": payload.is_default,
                "description": payload.description,
            },
        )
        .mappings()
        .first()
    )

    db.commit()

    assert result is not None, "INSERT RETURNING вернул пустой результат"

    return WeightProfile(
        id=result["id"],
        user_id=payload.user_id,
        profile_name=payload.profile_name,
        weights=payload.weights,
        is_default=payload.is_default,
        description=payload.description,
        created_at=result["created_at"],
        updated_at=result["updated_at"],
    )


def update_profile(
    db: Any, user_id: str, profile_id: int, payload: WeightProfileUpdate
) -> WeightProfile | None:
    """Обновить поля профиля (PATCH-style). Вернуть None если не найден.

    При установке is_default=True — снять is_default у остальных профилей
    пользователя в одной транзакции.
    """
    existing = get_profile(db, user_id, profile_id)
    if existing is None:
        return None

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

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

    if payload.weights is not None:
        sets.append("weights = CAST(:weights AS jsonb)")
        params["weights"] = json.dumps(payload.weights, ensure_ascii=False)

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

    if payload.is_default is not None:
        sets.append("is_default = :is_default")
        params["is_default"] = payload.is_default
        if payload.is_default:
            db.execute(text(_UNSET_DEFAULT), {"user_id": user_id})

    if len(sets) > 1:  # есть что обновлять кроме updated_at
        db.execute(
            text(
                f"UPDATE user_weight_profiles SET {', '.join(sets)}"
                " WHERE user_id = :user_id AND id = :profile_id"
            ),
            params,
        )
        db.commit()

    return get_profile(db, user_id, profile_id)


def delete_profile(db: Any, user_id: str, profile_id: int) -> bool:
    """Удалить профиль. Вернуть True если удалён, False если не найден."""
    result = db.execute(
        text(
            "DELETE FROM user_weight_profiles"
            " WHERE user_id = :user_id AND id = :profile_id"
            " RETURNING id"
        ),
        {"user_id": user_id, "profile_id": profile_id},
    ).first()
    if result is None:
        return False
    db.commit()
    return True


def resolve_weights(db: Any, user_id: str | None, profile_id: int | None) -> dict[str, float]:
    """Вернуть эффективные веса для analyze_parcel.

    Порядок приоритетов:
    1. profile_id задан → загрузить именно этот профиль
    2. user_id задан → загрузить default-профиль пользователя
    3. Иначе → вернуть системные значения _SYSTEM_POI_WEIGHTS
    """
    if profile_id is not None and user_id is not None:
        profile = get_profile(db, user_id, profile_id)
        if profile is not None and profile.weights:
            logger.debug(
                "resolve_weights: user=%s profile_id=%s → custom weights", user_id, profile_id
            )
            return dict(profile.weights)

    if user_id is not None:
        profile = get_default_profile(db, user_id)
        if profile is not None and profile.weights:
            logger.debug("resolve_weights: user=%s → default profile weights", user_id)
            return dict(profile.weights)

    logger.debug("resolve_weights: returning system defaults")
    return dict(_SYSTEM_POI_WEIGHTS)
