"""ГИСОГД-СО WFS-клиент — функциональные зоны генплана ЕКБ-2045 (#1058).

Источник: ``gisogd66.midural.ru`` — региональная ГИС органов государственной власти
Свердловской области. Открытый WFS без авторизации.

Два метода:
  ``resolve_functional_zone_layer()`` — резолвит актуальное typeName слоя «Функциональные
  зоны» из каталога слоёв. Суффикс-хеш в имени слоя меняется при обновлении генплана,
  поэтому хардкодить нельзя — резолвим динамически. Fallback-хардкод если каталог недоступен.

  ``fetch_functional_zones()`` — WFS GetFeature → список feature-dict'ов GeoJSON
  (FeatureCollection), ~6436 фич, Polygon, EPSG:4326.

TLS: midural.ru использует российский CA (по аналогии с #242 ekburg_permits). При SSLError
используем verify=False с explicit-комментом — без этого соединение рвётся на prod VPS.
"""

from __future__ import annotations

import logging
from typing import Any

import httpx

logger = logging.getLogger(__name__)

# ── Константы ─────────────────────────────────────────────────────────────────

GISOGD_BASE = "https://gisogd66.midural.ru"
CATALOG_URL = f"{GISOGD_BASE}/api/catalogs/agate_ekbgo"
WFS_URL = f"{GISOGD_BASE}/map/tiles/agate_ekbgo/wfs"

# Fallback-хардкод если каталог недоступен (последний известный хеш, 2026-06-07).
_LAYER_FALLBACK = "agate_ekbgo:agate_ekbgo_geo_urban_10_functional_zone_f6c5926"

# Имя узла в каталоге (case-insensitive поиск).
_CATALOG_NODE_NAME = "Функциональные зоны"

# Параметры запроса к WFS.
_WFS_OUTPUT_FORMAT = "application/json"
_WFS_VERSION = "2.0.0"

# Щедрый таймаут — ответ ~6436 фич (полный слой одним запросом, ~несколько МБ).
_WFS_TIMEOUT = 120
_CATALOG_TIMEOUT = 20

# Размер страницы для постраничного fallback (если целиком не забирается).
_PAGE_SIZE = 2000


# ── Сетевые helpers (module-level — мокаются в тестах) ───────────────────────


def _http_get_json(url: str, params: dict[str, str] | None = None, *, timeout: int) -> Any:
    """GET → JSON. verify=False: midural.ru — российский CA (аналог #242 ekburg_permits).

    Сначала пробуем verify=True; при SSLError fallback на verify=False с предупреждением.
    """
    try:
        resp = httpx.get(url, params=params, timeout=timeout, verify=True)
        resp.raise_for_status()
        return resp.json()
    except httpx.ConnectError as exc:
        # SSLError обёрнут в ConnectError у httpx — переключаемся на verify=False.
        logger.warning(
            "gisogd_so_client: SSL verify=True failed (%s), retry with verify=False", exc
        )
    resp = httpx.get(url, params=params, timeout=timeout, verify=False)
    resp.raise_for_status()
    return resp.json()


# ── Каталог → резолв слоя ────────────────────────────────────────────────────


def _find_layer_in_tree(node: Any) -> str | None:
    """Рекурсивный обход JSON-дерева каталога. Ищет узел с name==_CATALOG_NODE_NAME.

    Возвращает ``value`` найденного узла (typeName слоя) или None.
    """
    if not isinstance(node, dict):
        return None
    name = node.get("name") or ""
    if name.strip() == _CATALOG_NODE_NAME:
        value = node.get("value")
        return str(value) if value else None
    # Рекурсивно по children (разные ключи в каталоге: children / items / layers).
    for key in ("children", "items", "layers", "nodes"):
        children = node.get(key)
        if isinstance(children, list):
            for child in children:
                found = _find_layer_in_tree(child)
                if found:
                    return found
    return None


def resolve_functional_zone_layer() -> str:
    """Резолвит actуальное typeName слоя «Функциональные зоны» из каталога ГИСОГД-СО.

    Суффикс-хеш в имени слоя меняется при обновлении генплана → не хардкодить.
    При недоступности каталога возвращает fallback-хардкод.

    Returns:
        typeName вида ``agate_ekbgo:agate_ekbgo_geo_urban_10_functional_zone_<hash>``.
    """
    try:
        catalog = _http_get_json(CATALOG_URL, timeout=_CATALOG_TIMEOUT)
    except Exception as exc:
        logger.warning(
            "gisogd_so_client: каталог недоступен (%s), использую fallback layer=%s",
            exc,
            _LAYER_FALLBACK,
        )
        return _LAYER_FALLBACK

    layer = _find_layer_in_tree(catalog)
    if layer:
        logger.info("gisogd_so_client: резолвлен слой из каталога: %s", layer)
        return layer

    logger.warning(
        "gisogd_so_client: узел '%s' не найден в каталоге, использую fallback layer=%s",
        _CATALOG_NODE_NAME,
        _LAYER_FALLBACK,
    )
    return _LAYER_FALLBACK


# ── WFS GetFeature → список фич ──────────────────────────────────────────────


def _wfs_params(type_name: str, start_index: int = 0, count: int | None = None) -> dict[str, str]:
    """Строит параметры WFS GetFeature-запроса."""
    params: dict[str, str] = {
        "service": "WFS",
        "version": _WFS_VERSION,
        "request": "GetFeature",
        "typeNames": type_name,
        "outputFormat": _WFS_OUTPUT_FORMAT,
    }
    if count is not None:
        params["count"] = str(count)
    if start_index > 0:
        params["startIndex"] = str(start_index)
    return params


def fetch_functional_zones(type_name: str | None = None) -> list[dict[str, Any]]:
    """Загружает все фичи слоя функциональных зон из ГИСОГД-СО WFS.

    Сначала пробует забрать весь слой одним запросом (~6436 фич). При таймауте
    автоматически переключается на постраничный режим (count=_PAGE_SIZE).

    Args:
        type_name: typeName WFS-слоя. None → вызывает resolve_functional_zone_layer().

    Returns:
        Список feature-dict'ов (GeoJSON Feature): ``{"geometry": ..., "properties": ...}``.
        Пусто если WFS недоступен или вернул 0 фич.
    """
    if type_name is None:
        type_name = resolve_functional_zone_layer()

    # Попытка 1: весь слой одним запросом.
    try:
        data = _http_get_json(WFS_URL, _wfs_params(type_name), timeout=_WFS_TIMEOUT)
        features: list[dict[str, Any]] = (data or {}).get("features") or []
        if features:
            logger.info("gisogd_so_client: WFS загружено фич=%d (один запрос)", len(features))
            return features
    except httpx.TimeoutException:
        logger.warning("gisogd_so_client: WFS таймаут одним запросом, переключаюсь на страницы")
    except Exception as exc:
        logger.error("gisogd_so_client: WFS ошибка одним запросом: %s", exc)
        return []

    # Попытка 2: постраничная загрузка.
    features = []
    start = 0
    while True:
        try:
            data = _http_get_json(
                WFS_URL,
                _wfs_params(type_name, start_index=start, count=_PAGE_SIZE),
                timeout=_WFS_TIMEOUT,
            )
        except Exception as exc:
            logger.error("gisogd_so_client: WFS постраничная ошибка start=%d: %s", start, exc)
            break
        page: list[dict[str, Any]] = (data or {}).get("features") or []
        if not page:
            break
        features.extend(page)
        logger.info(
            "gisogd_so_client: WFS страница start=%d, фич на странице=%d, итого=%d",
            start,
            len(page),
            len(features),
        )
        if len(page) < _PAGE_SIZE:
            break
        start += _PAGE_SIZE

    logger.info("gisogd_so_client: WFS итого загружено фич=%d (постраничная)", len(features))
    return features


__all__ = [
    "CATALOG_URL",
    "WFS_URL",
    "fetch_functional_zones",
    "resolve_functional_zone_layer",
]
