"""Сборка ИРД-блока для analyze_parcel (#1067 D9b — analyze-wiring).

Единая точка, собирающая «слой ИРД/future-supply» участка для ответа analyze (GG-форсайт).
Объединяет ТРИ источника, каждый — graceful (сбой одного не валит блок и тем более analyze):

  1. ``parcel_ird_overlaps`` (DB, м.132 ird_overlays) — ограничения/обременения + opportunity-ЗУ,
     пересекающие участок (ЗОУИТ / ПЗЗ-границы / ОКН / ООПТ + opportunity_* из #1086).
     Дёшево (GIST ST_Intersects), всегда безопасно.
  2. ЕКБ-геопортал WFS по centroid (network, опц.): функц.зона генплана (#1058).
  3. ПЗЗ-регламент зоны (#1067 C8b): zone_regulation_cache по индексу зоны (cache-first через
     ``get_or_fetch_zone_regulation`` — WFS index дёшев, urbanCard только на cache-miss).

КРТ (#1060) перенесён из live WFS (≈4s) в DB (#1130): ``_krt_at`` читает ST_Intersects к
ekb_krt_geometry — дёшево и без сетевой задержки. Harvest обновляет таблицу раз в неделю
(krt_geometry_sync Celery-воркер). Пустая/отсутствующая таблица graceful → [].

Вызывается ТОЛЬКО за feature-flag ``settings.enable_ird_analyze`` (default off): источник 3 —
live-зависимость от внешнего геопортала (reverse-eng, без SLA) в hot-пути analyze. Флаг off →
parcels.py даже не зовёт этот модуль. Сетевой timeout укорочен (geoportal_timeout) чтобы не
держать sync-handler.

Логика вынесена из parcels.py (HOT-файл) сюда — в parcels.py остаётся 1 import + gated-блок.
"""

from __future__ import annotations

import logging
from typing import Any

from sqlalchemy import text
from sqlalchemy.orm import Session

from app.core.config import settings
from app.services.scrapers.ekb_geoportal_client import EKBGeoportalClient
from app.services.site_finder.functional_zone_lookup import parcel_functional_zones
from app.services.site_finder.granddoc_lookup import parcel_granddoc
from app.services.site_finder.ird_overlay_lookup import parcel_ird_overlaps
from app.services.site_finder.krt_lookup import parcel_krt_requisites
from app.services.site_finder.okn_lookup import parcel_okn_objects
from app.services.site_finder.pat_lookup import parcel_pat_subzones
from app.services.site_finder.planning_lookup import parcel_planning_overlaps
from app.services.site_finder.ppt_tep_lookup import parcel_ppt_tep
from app.services.site_finder.reservation_lookup import parcel_reservations
from app.services.site_finder.zone_regulation import get_or_fetch_zone_regulation

logger = logging.getLogger(__name__)

# Sentinel: «регламент НЕ передан извне» — отличает «caller не резолвил» от «резолв дал None»
# (None — валидный результат: в точке нет терзоны). См. resolved_zone_regulation в
# build_ird_analyze_block (#1850 Fix 2).
_UNSET: Any = object()

# SQL: читаем КРТ-геометрии из БД (м.140 ekb_krt_geometry) по точке centroid участка.
# ST_Intersects с GIST → дёшево (~1ms), без сетевой задержки.
# Возвращает те же поля что ранее WFS: num_oks_krt, status, full_name, area.
# «type» в WFS-свойствах слоя krt не задокументирован отдельным полем; status покрывает его.
_KRT_AT_SQL = text("""
    SELECT num_oks_krt, status, full_name, area
    FROM ekb_krt_geometry
    WHERE ST_Intersects(geom, ST_SetSRID(ST_MakePoint(:lon, :lat), 4326))
""")


def _functional_zone_at(
    client: EKBGeoportalClient, lon: float, lat: float
) -> dict[str, Any] | None:
    """Функц.зона генплана (#1058) в точке — properties первой WFS-фичи; None при сбое/пусто."""
    try:
        feats = client.features_at_point("functional_zone", lon, lat)
    except Exception as exc:  # внешний геопортал, graceful
        logger.warning("ird_analyze: functional_zone WFS failed: %s", exc)
        return None
    return feats[0].properties if feats else None


def _krt_at(db: Session, lon: float, lat: float) -> list[dict[str, Any]]:
    """КРТ-объекты (#1060/#1130) в точке — из БД ekb_krt_geometry.

    Возвращает список properties-dicts в той же форме, что ранее WFS:
    num_oks_krt / status / full_name / area.
    [] при отсутствии пересечений или если таблица пуста/не существует.
    """
    try:
        with db.begin_nested():
            rows = db.execute(_KRT_AT_SQL, {"lon": lon, "lat": lat}).fetchall()
    except Exception as exc:  # таблица не существует / DB-сбой → graceful
        logger.warning("ird_analyze: _krt_at DB query failed: %s", exc)
        return []

    return [
        {
            "num_oks_krt": row.num_oks_krt,
            "status": row.status,
            "full_name": row.full_name,
            "area": float(row.area) if row.area is not None else None,
        }
        for row in rows
    ]


def _zone_regulation_at(
    db: Session, client: EKBGeoportalClient, lon: float, lat: float
) -> dict[str, Any] | None:
    """ПЗЗ-регламент зоны (#1067 C8b) в точке — cache-first. None при отсутствии/сбое."""
    try:
        return get_or_fetch_zone_regulation(db, lon, lat, client=client)
    except Exception as exc:  # внешний геопортал/БД, graceful
        logger.warning("ird_analyze: zone_regulation failed: %s", exc)
        return None


def build_ird_analyze_block(
    db: Session,
    geom_wkt: str | None,
    lon: float | None,
    lat: float | None,
    cad_num: str | None = None,
    *,
    client: EKBGeoportalClient | None = None,
    resolved_zone_regulation: Any = _UNSET,
) -> dict[str, Any]:
    """Собрать ИРД-блок участка для analyze (#1067 D9b).

    Args:
        db: сессия.
        geom_wkt: геометрия участка WKT (EPSG:4326) для DB-overlay ``ird_overlays``.
        lon/lat: centroid участка (EPSG:4326) для DB КРТ-lookup + geoportal zone_regulation.
        cad_num: кадастровый номер ЗУ для lookup изъятия/резервирования (м.136 land_reservation).
            None → reservations=[].
        client: переиспользуемый EKBGeoportalClient (для тестов/одного TLS-пула). По умолчанию
            создаётся с укороченным timeout.
        resolved_zone_regulation: УЖЕ резолвленный ПЗЗ-регламент (dict|None) из parcels.py
            zoning-merge блока (#1850 Fix 2). Если передан (включая None — «зоны нет») —
            ``_zone_regulation_at`` НЕ вызывается, используется переданное значение (исключает
            повторный DB-read кэша на тот же centroid). Если НЕ передан (``_UNSET``, дефолт) —
            блок резолвит сам (путь «enable_zoning_regulation_in_analyze off, enable_ird_analyze
            on»). Network-стоимость второго резолва уже снята мемоизацией coord→zone_index
            (Fix 1); этот параметр убирает оставшийся лишний DB-read.

    Returns:
        dict: ``ird_overlaps``/``ird_by_kind`` (DB-overlay, incl opportunity_*),
        ``opportunity_overlaps`` (подмножество layer_kind='opportunity_*'),
        ``planning_projects`` (ППТ/ПМТ overlap, DB, м.134 #1085),
        ``ppt_tep`` (list, ТЭП-контент ППТ/ПМТ из ekb_ppt_tep #1085/#1136: zone_balance / tep /
            phasing для участков в границах ППТ/ПМТ; связка doc_ref↔source_key/doc_full_name,
            graceful, пусто если ingest ещё не запускался или матча нет),
        ``reservations`` (list, изъятие/резервирование из land_reservation по cad_num, #1091/#1062),
        ``krt_requisites`` (list, КРТ-реквизиты из ekb_krt_sites + planning_projects, #1060),
        ``functional_zones`` (list, генплан-зоны из ekb_genplan_functional_zone, #1058),
        ``functional_zone`` (первая зона|None, back-compat; dict|None),
        ``krt`` (list, КРТ-геометрии из DB ekb_krt_geometry, #1130),
        ``zone_regulation`` (dict|None),
        ``okn_objects`` (list, ОКН в радиусе 100 м от участка из okn_objects, #1066;
            [{source_id, category, distance_m}], пусто при отсутствии объектов/таблицы).
        ``pat_subzones`` (list, приаэродромные подзоны ПАТ из pat_subzones, #1151;
            [{subzone_no, name, restriction, aerodrome}], пусто при отсутствии пересечений/таблицы).
        ``granddoc`` — разработанная граддокументация (ППТ/ПМТ) на участок: статус + реквизиты
            ПАГЕ (#1061); [{project_type, doc_status_name, is_active, paga_number, paga_date,
            subject, doc_full_name}]. Правообладатель/срок по КРТ-территориям отдаётся отдельно
            в ``krt_requisites`` (#1131).
        Любой источник деградирует независимо — блок всегда well-formed.
    """
    overlay = parcel_ird_overlaps(db, geom_wkt)
    overlaps: list[dict[str, Any]] = overlay.get("ird_overlaps", [])
    opportunity = [o for o in overlaps if str(o.get("layer_kind", "")).startswith("opportunity_")]
    block: dict[str, Any] = {
        "ird_overlaps": overlaps,
        "ird_by_kind": overlay.get("ird_by_kind", {}),
        "opportunity_overlaps": opportunity,
        "planning_projects": parcel_planning_overlaps(db, geom_wkt),
        # ТЭП-контент ППТ/ПМТ (#1136 follow-up #1133, GG-форсайт #1067 / #1085):
        # zone_balance / tep / phasing из ekb_ppt_tep, привязанные к planning_projects по
        # doc_ref↔source_key/doc_full_name. DB JOIN, дёшево (GIST), graceful — пусто если
        # таблица ekb_ppt_tep ещё пуста (нет ingest) или матча doc_ref нет. Идёт рядом с
        # planning_projects/krt_requisites — DB-источники основного потока.
        "ppt_tep": parcel_ppt_tep(db, geom_wkt),
        "reservations": parcel_reservations(db, cad_num),
        # Граддокументация (#1061): status «разработана» + реквизиты ПАГЕ из planning_projects.
        # DB, дёшево (ST_Intersects GIST), graceful. Вместе с planning_projects — один источник.
        # Правообладатель/срок КРТ-территорий → krt_requisites (#1131), не сюда.
        "granddoc": parcel_granddoc(db, geom_wkt),
        # ОКН-объекты (#1066): ST_DWithin к okn_objects (428 строк, GIST), дёшево, graceful.
        # Идёт рядом с reservations — DB-источники основного потока, без сетевых зависимостей.
        "okn_objects": parcel_okn_objects(db, geom_wkt),
        # ПАТ-подзоны (#1151/#1065): приаэродромные ограничения подзон 1-6 из pat_subzones.
        # DB ST_Intersects (GIST), дёшево, graceful. Рядом с okn_objects — DB-источник основного
        # потока.
        "pat_subzones": parcel_pat_subzones(db, geom_wkt),
        # КРТ-реквизиты (DB, дёшево, graceful): planning_projects ⋈ ekb_krt_sites (#1060).
        # Идёт рядом с planning_projects — оба DB-источника, не в geoportal-конкурентной части.
        "krt_requisites": parcel_krt_requisites(db, geom_wkt),
        # Функц.зоны генплана (#1058): DB-вызов ekb_genplan_functional_zone, дёшево, graceful.
        # functional_zones — полный список; functional_zone — первая зона|None (back-compat).
        "functional_zones": [],
        "functional_zone": None,
        # КРТ-геометрии (#1130): теперь из DB ekb_krt_geometry, не из живого WFS.
        # Заполняется ниже через _krt_at(db, lon, lat) — дёшевый DB-вызов.
        "krt": [],
        "zone_regulation": None,
    }
    # Функц.зоны генплана (#1058): DB ekb_genplan_functional_zone, дёшево, в основном потоке.
    fz = parcel_functional_zones(db, geom_wkt)
    block["functional_zones"] = fz
    block["functional_zone"] = fz[0] if fz else None

    # #1850 Fix 2: если регламент УЖЕ резолвлен в parcels.py (zoning-merge блок), берём его
    # без повторного резолва — исключает лишний DB-read кэша на тот же centroid. None — тоже
    # валидное переданное значение («зоны нет»), поэтому отличаем «передан» через _UNSET.
    if resolved_zone_regulation is not _UNSET:
        block["zone_regulation"] = resolved_zone_regulation

    # guard активен только в unit-тестах; в prod centroid_lon/lat всегда float
    # (EKB-fallback в parcels.py).
    if lon is not None and lat is not None:
        # КРТ из БД — дёшево, выполняем до geoportal-вызова (zone_regulation).
        # Нет нужды в ThreadPool: krt теперь DB-вызов, не сеть.
        block["krt"] = _krt_at(db, lon, lat)

        # zone_regulation остаётся единственным живым geoportal-вызовом в hot-пути.
        # Резолвим САМИ только если caller не передал готовый регламент (Fix 2).
        # Выполняем в основном потоке (Session не потокобезопасна).
        if resolved_zone_regulation is _UNSET:
            client = client or EKBGeoportalClient(timeout=settings.geoportal_timeout_s)
            block["zone_regulation"] = _zone_regulation_at(db, client, lon, lat)

    return block


__all__ = ["build_ird_analyze_block"]
