"""Gate Verdict aggregator — собирает can-build-MKD signal из NSPD lookups.

Per #32 G5: user-facing answer на главный вопрос девелопера "Можно ли тут МКД?"
Foundation: nspd_zoning, nspd_zouit_overlaps, nspd_engineering_nearby (Sprint 1.1).
Pure function — no DB dependencies, consumes already-fetched data from analyze_parcel.
"""

from __future__ import annotations

import re
from typing import Literal, TypedDict

from app.services.site_finder.network_obremenenie import (
    classify_network_zone,
    network_kind_label,
)

# ── Zone matching ─────────────────────────────────────────────────────────────

# Regex для zone_code из PKK6 / pzz_zones_ekb: "Ж", "Ж-1", "Жс", "ЖС-2" и т.д.
# Кейс-инсенситив для защиты от "ж-1" в нестандартных источниках.
# NSPD quarter-dump в zone_code кладёт кадастровый рег.номер (напр. "66:41-7.14") —
# он никогда не начинается на "Ж", поэтому этот regex не даст false positive.
_RESIDENTIAL_CODE_RE = re.compile(r"^[Жж]", re.UNICODE)

# Fallback по zone_name (lowercase substring) — "жилая", "жилой", "жилая зона" и т.д.
RESIDENTIAL_KEYWORDS = ("жил",)

# NSPD territorial zones layer 875838: subcategory (int) → тип зоны.
# Значения получены из реальных данных nspd_quarter_dumps (ЕКБ, 2026-02):
#   subcategory=1  — 39 объектов  — Жилые зоны малоэтажной застройки (ИЖС/таунхаусы)
#   subcategory=2  — 221 объект   — Жилые зоны (МКД разрешён, крупнейшая группа ЕКБ)
#   subcategory=3  — 126 объектов — Зоны смешанного использования (МКД по ПЗЗ ЕКБ допустим)
#   subcategory=4  — 77 объектов  — Общественно-деловые
#   subcategory=5  — 112 объектов — Производственные
#   subcategory=6  — 92 объекта   — Рекреационные
#   subcategory=8  — 16 объектов  — Специальные
#   subcategory=10 — 113 объектов — Промышленные/транспортные
# subcategory=1 исключён намеренно: ИЖС-зона МКД не допускает.
NSPD_RESIDENTIAL_SUBCATEGORIES: frozenset[int] = frozenset({2, 3})

# ── ЗОУИТ taxonomy ────────────────────────────────────────────────────────────

# Subcategory codes которые блокируют МКД (охранные зоны ЛЭП/газа/трубопровода).
# Используется для overlaps из NSPD dump (subcategory INT там заполнен).
BLOCKER_SUBCATEGORIES: dict[int, str] = {
    17: "Инженерные коммуникации (охранная зона ЛЭП/газа/трубопровода)",
}

# Keyword-based blockers для cad_zouit fallback (#232).
# cad_zouit.subcategory = 100% NULL, поэтому классификация по type_zone substring.
# Порядок: lowercase, substring match (any wins → blocker).
BLOCKER_TYPE_ZONE_KEYWORDS: tuple[str, ...] = (
    "охранная зона",
    "трубопровод",
    "электр",
    "газ",
)

# type_zone substrings, которые дают warning (не blocker) из cad_zouit.
WARNING_TYPE_ZONE_KEYWORDS: tuple[str, ...] = ("сзз", "санитарно-защитная")

# Engineering nearby search radius (метры) — совпадает с quarter_dump_lookup.py
ENGINEERING_NEARBY_THRESHOLD_M = 200

# Warning codes that are purely informational — they appear in warnings[] for the user
# but do NOT downgrade the final verdict from «Можно» to «С ограничениями» (#1617).
# A clean residential parcel with only these warnings still gets verdict_label="Можно".
INFORMATIONAL_WARNING_CODES: frozenset[str] = frozenset({"NO_ENGINEERING_NEARBY"})


# ── TypedDicts ────────────────────────────────────────────────────────────────


class Blocker(TypedDict):
    code: str
    detail: str


class Warning(TypedDict):
    code: str
    detail: str


class GateVerdict(TypedDict):
    can_build_mkd: bool | Literal["unknown"]
    verdict_label: str  # "Можно" | "Нельзя" | "С ограничениями" | "Нужна проверка"
    blockers: list[Blocker]
    warnings: list[Warning]
    checks_performed: list[str]
    source: Literal["nspd_dump", "nspd_dump_partial", "no_data"]


# ── Helpers ───────────────────────────────────────────────────────────────────


def is_residential_zone(
    zone_code: str | None,
    zone_name: str | None,
    raw_props: dict | None = None,
) -> bool:
    """Check if ПЗЗ zone permits residential (МКД) building.

    Detection strategy (in priority order):

    1. zone_code regex «^Ж» — ПЗЗ коды PKK6/pzz_zones_ekb: "Ж-1", "Жс", "ЖС-2" и т.д.
       NSPD quarter-dump кладёт в zone_code кадастровый рег.номер ("66:41-7.14") —
       он не начинается на «Ж», ложных срабатываний нет.

    2. NSPD subcategory из raw_props — единственный надёжный тип-дискриминатор в
       реальных данных nspd_quarter_dumps (layer 875838, ЕКБ 2026-02):
       subcategory=2 ("Жилые зоны") и subcategory=3 ("Смешанное использование")
       допускают МКД по ПЗЗ ЕКБ. subcategory=1 (ИЖС) — намеренно исключён.

    3. zone_name substring «жил» — fallback для нестандартных источников.
    """
    # 1. PKK6-style zone_code: starts with Ж (case-insensitive)
    if zone_code and _RESIDENTIAL_CODE_RE.match(zone_code):
        return True

    # 2. NSPD subcategory from raw_props (primary discriminator for quarter-dump data)
    if raw_props:
        sub_raw = raw_props.get("subcategory")
        if sub_raw is not None:
            try:
                if int(sub_raw) in NSPD_RESIDENTIAL_SUBCATEGORIES:
                    return True
            except (ValueError, TypeError):
                pass

    # 3. zone_name substring fallback
    if zone_name:
        zn_lower = zone_name.lower()
        if any(kw in zn_lower for kw in RESIDENTIAL_KEYWORDS):
            return True

    return False


# ── Main aggregator ───────────────────────────────────────────────────────────


def compute_gate_verdict(
    nspd_zoning: dict | None,
    nspd_zouit_overlaps: list[dict] | None,
    nspd_engineering_nearby: list[dict] | None,
    nspd_dump: dict | None,
) -> GateVerdict:
    """Aggregate NSPD lookups into can-build-MKD verdict.

    Checks performed (in order):
    1. ПЗЗ residential zone match (blocker if non-residential)
    2. ЗОУИТ subcategory 17 overlap (blocker); other subcategories → warnings
    3. Engineering nearby presence check (warning-only, not a blocker)

    Returns GateVerdict с blockers/warnings/checks_performed и source label.
    """
    blockers: list[Blocker] = []
    warnings: list[Warning] = []
    checks: list[str] = []

    # Guard: no NSPD data at all
    dump_available = (nspd_dump or {}).get("available", False)
    dump_stale = (nspd_dump or {}).get("stale", False)
    if not dump_available:
        return GateVerdict(
            can_build_mkd="unknown",
            verdict_label="Нужна проверка",
            blockers=[],
            warnings=[
                Warning(
                    code="NO_NSPD_DUMP",
                    detail="Данные NSPD ещё не подгружены",
                )
            ],
            checks_performed=[],
            source="no_data",
        )

    source: Literal["nspd_dump", "nspd_dump_partial"] = (
        "nspd_dump_partial" if dump_stale else "nspd_dump"
    )

    # Check 1 — ПЗЗ residential zone
    checks.append("ПЗЗ зональность")
    if nspd_zoning:
        if not is_residential_zone(
            nspd_zoning.get("zone_code"),
            nspd_zoning.get("zone_name"),
            nspd_zoning.get("raw_props"),
        ):
            zone_label = (
                nspd_zoning.get("zone_name") or nspd_zoning.get("zone_code") or "неизвестна"
            )
            blockers.append(
                Blocker(
                    code="PZZ_NOT_RESIDENTIAL",
                    detail=f"Зона: {zone_label}",
                )
            )
    else:
        warnings.append(
            Warning(
                code="PZZ_UNKNOWN",
                detail="ПЗЗ зона не определена",
            )
        )

    # Check 2 — ЗОУИТ overlaps
    checks.append("ЗОУИТ пересечения")
    for overlap in nspd_zouit_overlaps or []:
        src = overlap.get("source", "nspd-quarter-dump")
        if src == "cad_zouit":
            # cad_zouit fallback path: classify by type_zone keywords (#232).
            # subcategory = NULL в cad_zouit, поэтому subcategory-based logic не применяется.
            type_zone_lower = (overlap.get("type_zone") or overlap.get("layer") or "").lower()
            # #1070: классификация «сетевого обременения» (охранная зона инж.сети).
            # Флаг приходит из _get_cad_zouit_overlaps (is_network_zone/network_kind);
            # если overlap из старого пути без флага — определяем здесь же.
            net_kind = overlap.get("network_kind")
            if net_kind is None and overlap.get("is_network_zone") is None:
                net_kind = classify_network_zone(overlap.get("type_zone") or overlap.get("layer"))
            # Порядок веток: СЗЗ-warning ПЕРВЫМ (#1341) — маркер 'сзз'/'санитарно-защитная'
            # специфичнее широких blocker-подстрок ('газ'/'электр'), СЗЗ газораспределительной
            # станции/электроподстанции = ограничение, а не hard-blocker. Затем сетевое
            # обременение (#1070, выделенный код раньше широких подстрок), затем общие
            # blocker-keywords, иначе — generic warning.
            if any(kw in type_zone_lower for kw in WARNING_TYPE_ZONE_KEYWORDS):
                warnings.append(
                    Warning(
                        code="ZOUIT_CAD_SZZ",
                        detail=(
                            f"СЗЗ ({overlap.get('type_zone', '')}): " f"{overlap.get('name', '')}"
                        ),
                    )
                )
            elif net_kind is not None:
                kind_label = overlap.get("network_kind_label") or network_kind_label(net_kind)
                blockers.append(
                    Blocker(
                        code="ZOUIT_NETWORK_OBREMENENIE",
                        detail=(
                            f"Сетевое обременение ({kind_label}): "
                            f"{overlap.get('type_zone', '')} {overlap.get('name', '')}".strip()
                        ),
                    )
                )
            elif any(kw in type_zone_lower for kw in BLOCKER_TYPE_ZONE_KEYWORDS):
                blockers.append(
                    Blocker(
                        code="ZOUIT_CAD_BLOCKER",
                        detail=(
                            f"Охранная зона ({overlap.get('type_zone', '')}): "
                            f"{overlap.get('name', '')}"
                        ),
                    )
                )
            else:
                warnings.append(
                    Warning(
                        code="ZOUIT_CAD_OTHER",
                        detail=(
                            f"ЗОУИТ cad ({overlap.get('type_zone', '')}): "
                            f"{overlap.get('name', '')}"
                        ),
                    )
                )
        else:
            # NSPD dump path: subcategory-based logic (backward-compat).
            # subcategory из _get_zouit_overlaps = `subcategory or type_zone` → может быть
            # строкой ('17') или fallback на type_zone (#1360). Нормализуем к int как
            # _get_zouit_engineering_overlaps, иначе blocker subcategory-17 тихо
            # деградирует до warning.
            sub_raw = overlap.get("subcategory")
            sub: int | None
            if isinstance(sub_raw, bool):
                sub = None
            else:
                try:
                    sub = int(sub_raw)  # type: ignore[arg-type]
                except (ValueError, TypeError):
                    sub = None
            if sub is not None and sub in BLOCKER_SUBCATEGORIES:
                blockers.append(
                    Blocker(
                        code=f"ZOUIT_OVERLAP_SUB{sub}",
                        detail=f"{BLOCKER_SUBCATEGORIES[sub]}: {overlap.get('name', '')}",
                    )
                )
            else:
                # Сохраняем исходную метку (str/int) в warning-коде если нормализация
                # не дала int — иначе теряется человекочитаемый ярлык subcategory.
                if sub is not None:
                    sub_label: int | str = sub
                elif sub_raw is not None:
                    sub_label = sub_raw
                else:
                    sub_label = "unknown"
                warnings.append(
                    Warning(
                        code=f"ZOUIT_SUB{sub_label}",
                        detail=f"ЗОУИТ {overlap.get('layer', '')}: {overlap.get('name', '')}",
                    )
                )

    # Check 3 — Engineering nearby (warning only, not a blocker)
    checks.append(f"Инженерные сети в радиусе {ENGINEERING_NEARBY_THRESHOLD_M}м")
    if not (nspd_engineering_nearby or []):
        warnings.append(
            Warning(
                code="NO_ENGINEERING_NEARBY",
                detail=f"Сетей не найдено в радиусе {ENGINEERING_NEARBY_THRESHOLD_M}м",
            )
        )

    # Final verdict.
    # Informational warnings (e.g. NO_ENGINEERING_NEARBY) appear in warnings[] for display
    # but do NOT by themselves downgrade a clean parcel to «С ограничениями» (#1617).
    verdict_relevant_warnings = [
        w for w in warnings if w["code"] not in INFORMATIONAL_WARNING_CODES
    ]
    can_build: bool | Literal["unknown"]
    if blockers:
        can_build = False
        label = "Нельзя"
    elif verdict_relevant_warnings:
        can_build = True
        label = "С ограничениями"
    else:
        can_build = True
        label = "Можно"

    return GateVerdict(
        can_build_mkd=can_build,
        verdict_label=label,
        blockers=blockers,
        warnings=warnings,
        checks_performed=checks,
        source=source,
    )
