"""Кэш ПЗЗ-градрегламента по индексу зоны + числовой экстракт предельных параметров (#1067 C8b).

Регламент ПЗЗ идентичен для всех участков одной терзоны, а зон на город ~100 → резолвим из
ЕКБ-геопортала urbanCard ОДИН раз на (city, zone_index) и кэшируем в ``zone_regulation_cache``
(м.133). Предельные параметры urbanCard отдаёт СВОБОДНЫМ ТЕКСТОМ (``otherDescription``) — тут же
их числовой экстракт (regex) в ключевые поля застройки для capacity-оценки.

Слои:
  * :func:`parse_limit_params` — чистый экстрактор (regex, без IO; основной тест-таргет).
  * :func:`upsert_zone_regulation` / :func:`get_cached_zone_regulation` — запись/чтение кэша.
  * :func:`get_or_fetch_zone_regulation` — get-or-fetch (cache miss → urbanCard → upsert).

Текст предельных параметров (live-verified 2026-06-06, vault
research/EKB_Geoportal_PZZ_Content_API_Jun06) — пример (зона Ц-1):
    'минимальная площадь земельного участка - 1500 кв. м;\\n
     максимальный процент застройки ... – 100%;\\n
     коэффициент строительного использования земельного участка – 4;\\n
     процент застройки подземного пространства – 100%;\\n ...'
Каждый параметр часто повторяется per-ВРИ (среднеэтажная 1500 / многоэтажная 3000) и сплошь
встречается «не подлежат установлению» (числа нет → НЕ матчим). Поэтому params_numeric хранит
СПИСКИ значений, а headline-скаляры — most-permissive агрегат (capacity best-case).

Graceful: при отсутствии таблицы/ошибке БД чтение возвращает None, analyze не падает (wiring D9b).
"""

from __future__ import annotations

import json
import logging
import re
from typing import Any

from sqlalchemy import text
from sqlalchemy.exc import OperationalError, ProgrammingError
from sqlalchemy.orm import Session

from app.services.scrapers.ekb_geoportal_client import (
    EKBGeoportalClient,
    ZoneRegulation,
)

logger = logging.getLogger(__name__)

# ── Числовой экстракт предельных параметров (regex) ──────────────────────────
# Числовой токен: тысячные разделены пробелом/nbsp ('50 000'), десятичная — запятая ('2,4').
# Сначала grouped-тысячи, иначе простое число. `.` по умолчанию НЕ переходит \n — параметры
# разделены \n, поэтому non-greedy `[^\n]*?` не перепрыгнет на число следующего параметра.
_NUM = r"(\d{1,3}(?:[  ]\d{3})+(?:[.,]\d+)?|\d+(?:[.,]\d+)?)"
# Тире в реальном тексте варьируется: дефис, en-dash, em-dash.
_DASH = r"[-–—]"

# мин. площадь ЗУ — 'минимальная площадь земельного участка - 1500 кв. м'
_RE_MIN_AREA = re.compile(
    r"минимальн\w*\s+площад\w*\s+земельн\w*\s+участка\s*" + _DASH + r"\s*" + _NUM + r"\s*кв",
    re.IGNORECASE,
)
# макс. % застройки — 'максимальный процент застройки ... – 100%'
_RE_BUILD_PCT = re.compile(
    r"максимальн\w*\s+процент\s+застройки[^\n]*?" + _DASH + r"\s*" + _NUM + r"\s*%",
    re.IGNORECASE,
)
# КСИ — 'коэффициент строительного использования земельного участка ... – 4' / '– 2,4'
_RE_FAR = re.compile(
    r"коэффициент\s+строительного\s+использования\s+земельн\w*\s+участка[^\n]*?"
    + _DASH
    + r"\s*"
    + _NUM,
    re.IGNORECASE,
)
# подземная застройка — 'процент застройки подземного пространства – 100%'
_RE_UNDERGROUND = re.compile(
    r"процент\s+застройки\s+подземного\s+пространства\s*" + _DASH + r"\s*" + _NUM + r"\s*%",
    re.IGNORECASE,
)
# этажность — 'предельная этажность ... – 5 этажей' / 'предельное количество этажей ... – 5'
_RE_FLOORS = re.compile(
    r"(?:предельн\w*\s+этажност\w*|предельное\s+количество\s+этажей)[^\n]*?"
    + _DASH
    + r"\s*"
    + _NUM
    + r"(?:\s*этаж\w*)?",
    re.IGNORECASE,
)
# высота — 'предельная высота ... – 25 м' (в ЕКБ почти всегда «не подлежат», но для прочих МО)
_RE_HEIGHT = re.compile(
    r"предельн\w*\s+высот\w*[^\n]*?" + _DASH + r"\s*" + _NUM + r"\s*м",
    re.IGNORECASE,
)

# param-ключ → (compiled regex, агрегатор headline-скаляра по списку значений).
# min_parcel_area — НАИМЕНЬШИЙ минимум (самый разрешительный); прочие — max (capacity best-case).
_PARAM_SPECS: tuple[tuple[str, re.Pattern[str], Any], ...] = (
    ("max_far", _RE_FAR, max),
    ("max_building_pct", _RE_BUILD_PCT, max),
    ("underground_pct", _RE_UNDERGROUND, max),
    ("min_parcel_area_m2", _RE_MIN_AREA, min),
    ("max_floors", _RE_FLOORS, max),
    ("max_height_m", _RE_HEIGHT, max),
)


def _to_number(raw: str) -> float:
    """Нормализовать захваченный токен в float: убрать пробелы/nbsp тысяч, запятая → точка."""
    s = raw.replace(" ", "").replace(" ", "").replace(",", ".")
    return float(s)


def parse_limit_params(texts: list[str] | None) -> dict[str, list[float]]:
    """Числовой экстракт предельных параметров из сырых ``otherDescription``-строк.

    Args:
        texts: список свободно-текстовых блоков предельных параметров (limit_params).

    Returns:
        ``{param_key: [значения...]}`` только для параметров, где найдено ≥1 число.
        Значения в порядке появления, дубликаты сохраняются (per-ВРИ повторы осмысленны).
        Параметры: ``max_far`` (КСИ), ``max_building_pct``, ``underground_pct``,
        ``min_parcel_area_m2``, ``max_floors``, ``max_height_m``.
    """
    out: dict[str, list[float]] = {}
    blob = "\n".join(t for t in (texts or []) if t)
    if not blob:
        return out
    for key, rx, _agg in _PARAM_SPECS:
        vals = [_to_number(m) for m in rx.findall(blob)]
        if vals:
            out[key] = vals
    return out


def _headline_scalars(params_numeric: dict[str, list[float]]) -> dict[str, float | int | None]:
    """Агрегировать списки значений в headline-скаляры (most-permissive для capacity)."""
    scalars: dict[str, float | int | None] = {}
    for key, _rx, agg in _PARAM_SPECS:
        vals = params_numeric.get(key) or []
        if not vals:
            scalars[key] = None
            continue
        v = agg(vals)
        scalars[key] = int(v) if key == "max_floors" else float(v)
    return scalars


# ── Кэш-таблица zone_regulation_cache (м.133) ────────────────────────────────

_UPSERT_SQL = text(
    """
    INSERT INTO zone_regulation_cache (
        city, zone_index, zone_full_name,
        main_vri, conditional_vri, auxiliary_vri,
        limit_params_raw, params_numeric,
        max_far, max_building_pct, underground_pct,
        min_parcel_area_m2, max_floors, max_height_m,
        source, fetched_at
    ) VALUES (
        :city, :zone_index, :zone_full_name,
        CAST(:main_vri AS jsonb), CAST(:conditional_vri AS jsonb), CAST(:auxiliary_vri AS jsonb),
        CAST(:limit_params_raw AS jsonb), CAST(:params_numeric AS jsonb),
        :max_far, :max_building_pct, :underground_pct,
        :min_parcel_area_m2, :max_floors, :max_height_m,
        :source, now()
    )
    ON CONFLICT (city, zone_index) DO UPDATE SET
        zone_full_name     = EXCLUDED.zone_full_name,
        main_vri           = EXCLUDED.main_vri,
        conditional_vri    = EXCLUDED.conditional_vri,
        auxiliary_vri      = EXCLUDED.auxiliary_vri,
        limit_params_raw   = EXCLUDED.limit_params_raw,
        params_numeric     = EXCLUDED.params_numeric,
        max_far            = EXCLUDED.max_far,
        max_building_pct   = EXCLUDED.max_building_pct,
        underground_pct    = EXCLUDED.underground_pct,
        min_parcel_area_m2 = EXCLUDED.min_parcel_area_m2,
        max_floors         = EXCLUDED.max_floors,
        max_height_m       = EXCLUDED.max_height_m,
        source             = EXCLUDED.source,
        fetched_at         = now()
    """
)

_SELECT_SQL = text(
    """
    SELECT city, zone_index, zone_full_name,
           main_vri, conditional_vri, auxiliary_vri,
           limit_params_raw, params_numeric,
           max_far, max_building_pct, underground_pct,
           min_parcel_area_m2, max_floors, max_height_m,
           source, fetched_at
    FROM zone_regulation_cache
    WHERE city = :city AND zone_index = :zone_index
    """
)


def upsert_zone_regulation(
    db: Session, reg: ZoneRegulation, *, city: str = "ekb", source: str = "ekb_geoportal"
) -> dict[str, list[float]] | None:
    """Записать регламент зоны в кэш (UPSERT по (city, zone_index)) + числовой экстракт.

    Returns:
        params_numeric (числовой экстракт) при успехе; None если у регламента нет zone_index
        или таблица недоступна (graceful).
    """
    if not reg.zone_index:
        return None
    params_numeric = parse_limit_params(reg.limit_params)
    scalars = _headline_scalars(params_numeric)
    payload = {
        "city": city,
        "zone_index": reg.zone_index,
        "zone_full_name": reg.zone_full_name,
        "main_vri": json.dumps(reg.main_vri, ensure_ascii=False),
        "conditional_vri": json.dumps(reg.conditional_vri, ensure_ascii=False),
        "auxiliary_vri": json.dumps(reg.auxiliary_vri, ensure_ascii=False),
        "limit_params_raw": json.dumps(reg.limit_params, ensure_ascii=False),
        "params_numeric": json.dumps(params_numeric, ensure_ascii=False),
        "source": source,
        **scalars,
    }
    try:
        with db.begin_nested():
            db.execute(_UPSERT_SQL, payload)
    except (OperationalError, ProgrammingError) as exc:
        logger.warning("upsert_zone_regulation: zone_regulation_cache недоступна, skip: %s", exc)
        return None
    return params_numeric


def get_cached_zone_regulation(
    db: Session, zone_index: str | None, *, city: str = "ekb"
) -> dict[str, Any] | None:
    """Прочитать кэшированный регламент зоны. None при отсутствии/недоступности таблицы."""
    if not zone_index:
        return None
    try:
        row = db.execute(_SELECT_SQL, {"city": city, "zone_index": zone_index}).mappings().first()
    except (OperationalError, ProgrammingError) as exc:
        logger.warning("get_cached_zone_regulation: cache недоступна, skip: %s", exc)
        return None
    return dict(row) if row else None


def get_or_fetch_zone_regulation(
    db: Session,
    lon: float,
    lat: float,
    *,
    city: str = "ekb",
    client: EKBGeoportalClient | None = None,
) -> dict[str, Any] | None:
    """Get-or-fetch регламент зоны в точке: кэш → (miss) urbanCard → upsert → кэш.

    Сначала индекс зоны по точке (WFS, дёшево); если он уже в кэше — отдаём кэш без urbanCard.
    Иначе полный резолв через urbanCard + запись. None если в точке нет терзоны/регламента.
    """
    client = client or EKBGeoportalClient()
    try:
        zone_index = client.zone_index_at(lon, lat)
    except Exception as exc:
        logger.warning("get_or_fetch_zone_regulation: zone_index_at failed: %s", exc)
        return None
    if zone_index:
        cached = get_cached_zone_regulation(db, zone_index, city=city)
        if cached is not None:
            return cached
    try:
        reg = client.zone_regulation_at(lon, lat)
    except Exception as exc:
        logger.warning("get_or_fetch_zone_regulation: zone_regulation_at failed: %s", exc)
        return None
    if reg is None or not reg.zone_index:
        return None
    upsert_zone_regulation(db, reg, city=city)
    db.commit()
    return get_cached_zone_regulation(db, reg.zone_index, city=city)


__all__ = [
    "get_cached_zone_regulation",
    "get_or_fetch_zone_regulation",
    "parse_limit_params",
    "upsert_zone_regulation",
]
