"""CRUD-сервис для insight (#948 Part A, ТЗ §7.13/§8.9).

API:
- create_insight(db, created_by, payload) → InsightOut
- list_insights(db, filters..., limit, offset) → InsightList
- get_insight(db, insight_id) → InsightOut | None
- update_insight(db, insight_id, payload) → InsightOut | None
- delete_insight(db, insight_id) → bool

Паттерн: raw SQL через SQLAlchemy text() + CAST(:x AS type), psycopg v3
(зеркало app/services/site_finder/custom_pois.py #254). db.commit() после
каждой мутации (regression guard #261).

Access control: backend rbac_guard (app/main.py) хард-блокирует ТОЛЬКО
/api/v1/admin/*. На /api/v1/insights pilot отсекается FRONTEND-овым RouteGuard
(allowed_paths из /me) + Caddy, НЕ бэкендом. До эндпоинта доходят analyst+admin.
is_confidential здесь — хранимый+фильтруемый флаг (per-record backend-gate НЕТ),
НЕ скрывает строки от analyst (политика #962: analyst видит конфиденциальные
insight'ы).
"""

from __future__ import annotations

import logging
from typing import Any

from sqlalchemy import text

from app.schemas.insight import InsightCreate, InsightList, InsightOut, InsightUpdate

logger = logging.getLogger(__name__)

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

# geom не отдаём (бинарь); lon/lat достаточно для фронта/карты.
_SELECT_COLS = """
    id, created_by, title, body, category, is_confidential,
    district, cad_num, lon, lat, source, created_at, updated_at
"""

_INSERT_SQL = f"""
    INSERT INTO insight
        (created_by, title, body, category, is_confidential,
         district, cad_num, lon, lat, source)
    VALUES
        (:created_by, :title, :body, :category, CAST(:is_confidential AS boolean),
         :district, :cad_num, CAST(:lon AS double precision),
         CAST(:lat AS double precision), :source)
    RETURNING {_SELECT_COLS}
"""

_SELECT_BY_ID = f"""
    SELECT {_SELECT_COLS}
      FROM insight
     WHERE id = :insight_id
"""

_DELETE_SQL = """
    DELETE FROM insight
     WHERE id = :insight_id
    RETURNING id
"""


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


def _row_to_out(r: Any) -> InsightOut:
    return InsightOut(
        id=int(r["id"]),
        created_by=r["created_by"],
        title=r["title"],
        body=r["body"],
        category=r["category"],
        is_confidential=bool(r["is_confidential"]),
        district=r["district"],
        cad_num=r["cad_num"],
        lon=float(r["lon"]) if r["lon"] is not None else None,
        lat=float(r["lat"]) if r["lat"] is not None else None,
        source=r["source"],
        created_at=r["created_at"],
        updated_at=r["updated_at"],
    )


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


def create_insight(db: Any, created_by: str, payload: InsightCreate) -> InsightOut:
    """Создать insight. created_by — автор из X-Authenticated-User."""
    row = (
        db.execute(
            text(_INSERT_SQL),
            {
                "created_by": created_by,
                "title": payload.title,
                "body": payload.body,
                "category": payload.category,
                "is_confidential": payload.is_confidential,
                "district": payload.district,
                "cad_num": payload.cad_num,
                "lon": payload.lon,
                "lat": payload.lat,
                "source": payload.source,
            },
        )
        .mappings()
        .first()
    )
    db.commit()
    assert row is not None, "INSERT RETURNING вернул пустой результат"
    logger.info(
        "insight created: id=%s by=%s confidential=%s district=%s cad=%s",
        row["id"],
        created_by,
        payload.is_confidential,
        payload.district,
        payload.cad_num,
    )
    return _row_to_out(row)


def list_insights(
    db: Any,
    *,
    district: str | None = None,
    cad_num: str | None = None,
    category: str | None = None,
    is_confidential: bool | None = None,
    created_by: str | None = None,
    limit: int = 50,
    offset: int = 0,
) -> InsightList:
    """Список insight'ов с фильтрами + пагинацией (зеркало admin_leads).

    analyst видит ВСЁ, включая is_confidential=True (политика #962); фильтр
    is_confidential — это сужение выборки, НЕ access-gate.
    """
    where: list[str] = []
    params: dict[str, Any] = {"lim": limit, "off": offset}
    if district is not None:
        where.append("district = :district")
        params["district"] = district
    if cad_num is not None:
        where.append("cad_num = :cad_num")
        params["cad_num"] = cad_num
    if category is not None:
        where.append("category = :category")
        params["category"] = category
    if is_confidential is not None:
        where.append("is_confidential = CAST(:is_confidential AS boolean)")
        params["is_confidential"] = is_confidential
    if created_by is not None:
        where.append("created_by = :created_by")
        params["created_by"] = created_by

    where_sql = "WHERE " + " AND ".join(where) if where else ""

    rows = (
        db.execute(
            text(
                f"SELECT {_SELECT_COLS} FROM insight {where_sql} "
                "ORDER BY created_at DESC LIMIT :lim OFFSET :off"
            ),
            params,
        )
        .mappings()
        .all()
    )
    total = db.execute(
        text(f"SELECT COUNT(*) FROM insight {where_sql}"),
        {k: v for k, v in params.items() if k not in ("lim", "off")},
    ).scalar_one()

    return InsightList(
        total=int(total or 0),
        limit=limit,
        offset=offset,
        rows=[_row_to_out(r) for r in rows],
    )


def get_insight(db: Any, insight_id: int) -> InsightOut | None:
    """Вернуть один insight по id."""
    row = db.execute(text(_SELECT_BY_ID), {"insight_id": insight_id}).mappings().first()
    if row is None:
        return None
    return _row_to_out(row)


def update_insight(db: Any, insight_id: int, payload: InsightUpdate) -> InsightOut | None:
    """Partial update insight. Возвращает None если не найден.

    Зеркало update_custom_poi: динамический SET по переданным полям. `is None`
    как «не передано» — корректно для is_confidential, т.к. False is not None.
    """
    existing = get_insight(db, insight_id)
    if existing is None:
        return None

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

    if payload.title is not None:
        sets.append("title = :title")
        params["title"] = payload.title
    if payload.body is not None:
        sets.append("body = :body")
        params["body"] = payload.body
    if payload.category is not None:
        sets.append("category = :category")
        params["category"] = payload.category
    if payload.is_confidential is not None:
        sets.append("is_confidential = CAST(:is_confidential AS boolean)")
        params["is_confidential"] = payload.is_confidential
    if payload.district is not None:
        sets.append("district = :district")
        params["district"] = payload.district
    if payload.cad_num is not None:
        sets.append("cad_num = :cad_num")
        params["cad_num"] = payload.cad_num
    # lon/lat меняем независимо — geom GENERATED пересчитается (NULL если одна из них NULL).
    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.source is not None:
        sets.append("source = :source")
        params["source"] = payload.source

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

    return get_insight(db, insight_id)


def delete_insight(db: Any, insight_id: int) -> bool:
    """Удалить insight (hard delete — зеркало custom_pois). True если удалён."""
    result = db.execute(text(_DELETE_SQL), {"insight_id": insight_id}).first()
    if result is None:
        return False
    db.commit()
    logger.info("insight deleted: id=%s", insight_id)
    return True
