"""Review-инструмент авто-маппингов objective_complex_mapping (feat/mapping-review-page).

Авто-проходы (auto_core_dev_v5 / auto_core_geo_v6 / fuzzy_* / …) пишут строки
с is_reviewed=false. Здесь — чтение этих строк С ОБОГАЩЕНИЕМ для глаз ревьюера,
подтверждение (approve → is_reviewed=true) и отклонение (reject → DELETE строки,
чтобы отклонённый маппинг не травил mv_layout_velocity §4.2).

Обогащение:
  • domrf comm_name / dev_name — LATEST snapshot по domrf_obj_id
    (snapshot_date DESC — как в objective_backfill._DOMRF_UNMAPPED_SQL).
  • objective developers — агрегат objective_lots.developer, SCOPED по
    (project_name = objective_complex_name). Урок 3.8с: агрегат по project_name,
    НЕ глобальный — иначе список застройщиков одного проекта протекает в чужой.

Чтения — parametrized `text(...)` + CAST(:x AS type) (backend.md psycopg3).
DELETE (reject) — только по PK с RETURNING для полного лога удаляемой строки.
"""

from __future__ import annotations

import logging
from datetime import date
from typing import Any

from sqlalchemy import text
from sqlalchemy.orm import Session

logger = logging.getLogger(__name__)

# ── SQL: строки mapping + обогащение domrf latest snapshot + objective devs ────
# LEFT JOIN LATERAL к domrf_kn_objects: DISTINCT-ON-эквивалент через ORDER BY
# snapshot_date DESC LIMIT 1 (latest snapshot по obj_id). objective devs —
# коррелированный агрегат по project_name (scoped, урок 3.8с).
# Сорт: is_reviewed asc (unreviewed сверху), match_score asc NULLS FIRST
# (самые сомнительные / без скора сверху), id для стабильности.
_LIST_SQL = text("""
    SELECT
        m.id,
        m.objective_complex_name,
        m.objective_project_id,
        m.objective_group,
        m.domrf_obj_id,
        m.match_method,
        m.match_score,
        m.is_reviewed,
        m.note,
        m.created_at,
        d.comm_name       AS domrf_comm_name,
        d.dev_name        AS domrf_dev_name,
        odev.developers   AS objective_developers
    FROM objective_complex_mapping m
    LEFT JOIN LATERAL (
        SELECT o.comm_name, o.dev_name
        FROM domrf_kn_objects o
        WHERE o.obj_id = m.domrf_obj_id
        ORDER BY o.snapshot_date DESC NULLS LAST
        LIMIT 1
    ) d ON m.domrf_obj_id IS NOT NULL
    LEFT JOIN LATERAL (
        SELECT ARRAY_REMOVE(ARRAY_AGG(DISTINCT ol.developer), NULL) AS developers
        FROM objective_lots ol
        WHERE ol.project_name = m.objective_complex_name
          AND ol.developer IS NOT NULL
    ) odev ON TRUE
    WHERE (CAST(:only_unreviewed AS boolean) = FALSE OR m.is_reviewed = FALSE)
    ORDER BY m.is_reviewed ASC, m.match_score ASC NULLS FIRST, m.id ASC
    LIMIT CAST(:limit AS int) OFFSET CAST(:offset AS int)
""")

_COUNT_SQL = text("""
    SELECT COUNT(*) AS total
    FROM objective_complex_mapping m
    WHERE (CAST(:only_unreviewed AS boolean) = FALSE OR m.is_reviewed = FALSE)
""")

# ── SQL: approve — is_reviewed=true + append note. RETURNING для 404-детекта ───
# NULLIF/concat: пустой/NULL note → просто «approved <date>»; иначе append через
# перевод строки. Идемпотентно относительно is_reviewed (повторный approve OK).
_APPROVE_SQL = text("""
    UPDATE objective_complex_mapping
    SET is_reviewed = TRUE,
        note = CASE
            WHEN note IS NULL OR note = '' THEN CAST(:approve_note AS text)
            ELSE note || E'\\n' || CAST(:approve_note AS text)
        END,
        updated_at = NOW()
    WHERE id = CAST(:id AS bigint)
    RETURNING id, objective_complex_name, domrf_obj_id,
              match_method, match_score, is_reviewed, note
""")

# ── SQL: reject — DELETE по PK, RETURNING всей строки для лога (травит §4.2) ───
_DELETE_SQL = text("""
    DELETE FROM objective_complex_mapping
    WHERE id = CAST(:id AS bigint)
    RETURNING id, objective_complex_name, objective_project_id, objective_group,
              domrf_obj_id, match_method, match_score, is_reviewed, note,
              created_at
""")


def _serialize_row(row: Any) -> dict[str, Any]:
    """Сериализовать строку list-запроса в loose dict для API (паттерн admin_etl)."""
    score = row.match_score
    created = row.created_at
    return {
        "id": row.id,
        "objective_complex_name": row.objective_complex_name,
        "objective_project_id": row.objective_project_id,
        "objective_group": row.objective_group,
        "domrf_obj_id": row.domrf_obj_id,
        "match_method": row.match_method,
        "match_score": float(score) if score is not None else None,
        "is_reviewed": bool(row.is_reviewed),
        "note": row.note,
        "created_at": created.isoformat() if created else None,
        # Обогащение для глаз ревьюера:
        "domrf_comm_name": row.domrf_comm_name,
        "domrf_dev_name": row.domrf_dev_name,
        "objective_developers": list(row.objective_developers or []),
    }


def list_mapping_review(
    db: Session,
    *,
    only_unreviewed: bool = True,
    limit: int = 100,
    offset: int = 0,
) -> dict[str, Any]:
    """Список строк objective_complex_mapping + обогащение + total count.

    Сорт: is_reviewed asc, match_score asc NULLS FIRST (сомнительные сверху), id.

    Args:
        db: SQLAlchemy sync Session.
        only_unreviewed: True (default) — только is_reviewed=false.
        limit: страница (строк).
        offset: смещение страницы.

    Returns:
        dict: rows (list обогащённых строк), total (int, до limit/offset),
        limit, offset, only_unreviewed.
    """
    rows = db.execute(
        _LIST_SQL,
        {"only_unreviewed": only_unreviewed, "limit": limit, "offset": offset},
    ).all()
    total = db.execute(_COUNT_SQL, {"only_unreviewed": only_unreviewed}).scalar_one()

    return {
        "rows": [_serialize_row(r) for r in rows],
        "total": int(total),
        "limit": limit,
        "offset": offset,
        "only_unreviewed": only_unreviewed,
    }


def approve_mapping(db: Session, mapping_id: int) -> dict[str, Any] | None:
    """Подтвердить маппинг: is_reviewed=true + append «approved <date>» в note.

    Returns:
        dict обновлённой строки, либо None если id не найден (→ 404 в роутере).
    """
    approve_note = f"approved {date.today().isoformat()}"
    row = db.execute(_APPROVE_SQL, {"id": mapping_id, "approve_note": approve_note}).first()
    if row is None:
        db.rollback()
        return None
    db.commit()
    logger.info(
        "mapping-review approve id=%s name=%r method=%s → is_reviewed=true",
        row.id,
        row.objective_complex_name,
        row.match_method,
    )
    score = row.match_score
    return {
        "id": row.id,
        "objective_complex_name": row.objective_complex_name,
        "domrf_obj_id": row.domrf_obj_id,
        "match_method": row.match_method,
        "match_score": float(score) if score is not None else None,
        "is_reviewed": bool(row.is_reviewed),
        "note": row.note,
    }


def reject_mapping(db: Session, mapping_id: int) -> dict[str, Any] | None:
    """Отклонить маппинг: DELETE строки (травит mv_layout_velocity §4.2 иначе).

    Перед удалением полная строка логируется через logger.info (в note писать
    некуда — строки не будет). DELETE только по PK с RETURNING.

    Returns:
        dict удалённой строки (для эха в ответе), либо None если id не найден.
    """
    row = db.execute(_DELETE_SQL, {"id": mapping_id}).first()
    if row is None:
        db.rollback()
        return None

    created = row.created_at
    score = row.match_score
    deleted: dict[str, Any] = {
        "id": row.id,
        "objective_complex_name": row.objective_complex_name,
        "objective_project_id": row.objective_project_id,
        "objective_group": row.objective_group,
        "domrf_obj_id": row.domrf_obj_id,
        "match_method": row.match_method,
        "match_score": float(score) if score is not None else None,
        "is_reviewed": bool(row.is_reviewed),
        "note": row.note,
        "created_at": created.isoformat() if created else None,
    }
    logger.info("mapping-review reject DELETE id=%s row=%s", row.id, deleted)
    db.commit()
    return deleted
