"""Загрузчик центров питания (ЦП) Россети из WFS — свободная мощность §3 (#2119).

No-B2B верифицированный источник: WFS геосервера портала-тп.рф отдаёт ~488
центров питания (ПС 35/110 кВ) по Свердловской области с координатами (WGS84,
округлены ~100 м), классом напряжения и индексом загрузки (открыт/ограничен/
закрыт для техприсоединения). UPSERT-ит в ``power_supply_centers``.

Источник ГЕО-БЛОКИРУЕТ non-RU IP → загрузчик РАБОТАЕТ НА ПРОДЕ (Celery weekly /
manual docker exec). Punycode-host ОБЯЗАТЕЛЕН — кириллический алиас портал-тп.рф
не резолвится корректно из curl/httpx.

Структура зеркалит utility_infrastructure_loader: httpx с явным таймаутом,
per-row SAVEPOINT при UPSERT (битая фича не валит weekly-sync).

Также экспортирует ``normalize_sc_name`` — общий нормализатор имён ЦП, которым
пользуется и ``rosseti_reserve_loader`` (матч xlsx-резерва по имени), и endpoint.
"""

import hashlib
import json
import logging
import re

import httpx
from sqlalchemy import text
from sqlalchemy.orm import Session

from app.core.db import SessionLocal

logger = logging.getLogger(__name__)

# Punycode-host ОБЯЗАТЕЛЕН (кириллический алиас портал-тп.рф не для curl/httpx).
# xn----7sb7akeedqd.xn--p1ai == портал-тп.рф.
WFS_BASE_URL = "https://xn----7sb7akeedqd.xn--p1ai/geoserver/wfs"

# Слой центров питания (полный вид с резервами/напряжением).
_WFS_TYPE_NAME = "gisosslabs:sc_points_fullview"

# CQL-фильтр по региону — только Свердловская область.
_WFS_CQL_FILTER = "rg_code='SverdlovskOblast'"

# Таймаут WFS-запроса (сек). ~488 фич — с запасом.
_WFS_TIMEOUT = 60

# sc_indexload_id (справочник Россети) → индекс загрузки ЦП. Неизвестный id →
# None (сырое значение остаётся в raw). Верифицированные id из справочника:
_INDEXLOAD_MAP: dict[int, str] = {
    256184: "open",  # открыт для техприсоединения (есть резерв)
    256186: "limited",  # ограниченно
    256188: "closed",  # закрыт (резерва нет)
}

# «ПС»/«подстанция» + класс напряжения-префикс — срезаем при нормализации имени.
# Оба компонента опциональны, но хотя бы один должен присутствовать (иначе матч
# пустой и sub ничего не делает). Примеры:
#   «ПС 110/35/10 Нижне-Исетская» → «нижне-исетская»
#   «ПС Уктус»                    → «уктус» (только «ПС», без напряжения)
#   «110/10 Уктус»                → «уктус» (только напряжение)
_VOLTAGE_PREFIX_RE = re.compile(
    r"^\s*(?:(?:пс|подстанци\w*)\s*)?(?:[\d/.,]+\s*(?:кв)?\s*)?",
    re.IGNORECASE,
)
# Класс напряжения внутри имени ЦП, напр. «110/35/10» или «110/10».
_VOLTAGE_CLASS_RE = re.compile(r"\b(\d{1,3}(?:[/.]\d{1,3}){1,3})\b")
_MULTISPACE_RE = re.compile(r"\s+")


def normalize_sc_name(name: str | None) -> str:
    """Нормализует имя ЦП для матча xlsx-резерва с WFS-фичей.

    Правила: lower, ё→е, срез «ПС»/подстанция + класс напряжения-префикса,
    удаление кавычек, схлопывание тире/пробелов. Пусто → ''.

    Examples:
        «ПС 110/35/10 Нижне-Исетская» → «нижне-исетская»
        «Нижне — Исетская»            → «нижне-исетская»
        «ПС "Уктус"»                  → «уктус»
    """
    if not name:
        return ""
    s = name.strip().lower().replace("ё", "е")
    s = s.replace("«", "").replace("»", "").replace('"', "").replace("'", "")
    # Срез voltage/«ПС»-префикса в начале имени (не трогаем цифры внутри имени).
    s = _VOLTAGE_PREFIX_RE.sub("", s, count=1)
    # Схлопываем все виды тире (—, –, -) в одиночный дефис, пробелы вокруг него убираем.
    s = re.sub(r"\s*[—–-]\s*", "-", s)
    s = _MULTISPACE_RE.sub(" ", s).strip()
    return s


def parse_voltage_class(name: str | None) -> str | None:
    """Извлекает класс напряжения из текста имени ЦП («ПС 110/35/10 …» → '110/35/10').

    Возвращает первую последовательность вида «NNN/NNN[/NNN…]». None — если нет.
    """
    if not name:
        return None
    m = _VOLTAGE_CLASS_RE.search(name)
    if not m:
        return None
    return m.group(1).replace(".", "/")


def _stable_external_id(feature: dict, props: dict) -> str:
    """Стабильный external_id фичи: feature['id'] или хэш ключевых полей.

    WFS обычно отдаёт стабильный ``feature['id']``; если его нет — детерминированный
    sha1 по (sc_name, координаты) чтобы UPSERT оставался идемпотентным.
    """
    fid = feature.get("id")
    if fid:
        return str(fid)
    geom = feature.get("geometry") or {}
    coords = geom.get("coordinates")
    seed = f"{props.get('sc_name', '')}|{coords}"
    # sha1 здесь — стабильный дедуп-id фичи, не криптография.
    return "h:" + hashlib.sha1(seed.encode("utf-8")).hexdigest()[:16]


def _map_load_index(props: dict) -> str | None:
    """sc_indexload_id → 'open'|'limited'|'closed'|None (неизвестный → None)."""
    raw = props.get("sc_indexload_id")
    if raw is None:
        return None
    try:
        return _INDEXLOAD_MAP.get(int(raw))
    except (TypeError, ValueError):
        return None


def fetch_power_supply_centers() -> list[dict]:
    """Тянет WFS-фичи центров питания Свердловской области (GeoJSON Feature-list).

    RUN-ON-PROD: источник гео-блокирует non-RU IP. httpx с явным таймаутом,
    verify по умолчанию (валидный TLS у геосервера). Возвращает list feature-dict'ов
    ``{"id", "geometry": {...}, "properties": {...}}``.
    """
    params = {
        "service": "WFS",
        "version": "1.1.0",
        "request": "GetFeature",
        "typeName": _WFS_TYPE_NAME,
        "outputFormat": "json",
        "CQL_FILTER": _WFS_CQL_FILTER,
    }
    resp = httpx.get(WFS_BASE_URL, params=params, timeout=_WFS_TIMEOUT)
    resp.raise_for_status()
    data = resp.json()
    features: list[dict] = (data or {}).get("features") or []
    logger.info("rosseti_wfs: загружено центров питания=%d", len(features))
    return features


def _point_geom_sql(feature: dict) -> tuple[str, dict] | None:
    """SQL-выражение + params для geom из GeoJSON Point. None — если не Point/нет коорд."""
    geom = feature.get("geometry") or {}
    if geom.get("type") != "Point":
        return None
    coords = geom.get("coordinates")
    if not coords or len(coords) < 2:
        return None
    lon, lat = coords[0], coords[1]
    if lon is None or lat is None:
        return None
    return "ST_SetSRID(ST_MakePoint(:lon, :lat), 4326)", {"lon": lon, "lat": lat}


def load_power_supply_centers(db: Session | None = None) -> dict[str, int]:
    """Тянет WFS + UPSERT-ит центры питания в power_supply_centers.

    UPSERT по UNIQUE(source, external_id). Per-row SAVEPOINT — битая фича не валит
    weekly-sync. ``db`` принимается для совместимости сигнатуры; если None — своя
    SessionLocal (WFS-fetch + запись в БД sync, как utility_infrastructure_loader).

    Returns: счётчики fetched/inserted/updated/skipped.
    """
    features = fetch_power_supply_centers()
    owns_session = db is None
    if db is None:
        db = SessionLocal()

    inserted = 0
    updated = 0
    skipped = 0
    fetched = len(features)
    try:
        for feature in features:
            props: dict = feature.get("properties") or {}
            sc_name = props.get("sc_name")
            if not sc_name:
                skipped += 1
                continue

            geom_pair = _point_geom_sql(feature)
            geom_sql = geom_pair[0] if geom_pair else "NULL"
            geom_params = geom_pair[1] if geom_pair else {}

            params: dict = {
                "source": "rosseti_wfs",
                "external_id": _stable_external_id(feature, props),
                "sc_name": sc_name,
                "sc_name_norm": normalize_sc_name(sc_name),
                "dzo_name": props.get("dzo_name"),
                "org_name": props.get("org_name"),
                "branch_name": props.get("br_name"),
                "voltage_class": parse_voltage_class(sc_name),
                "load_index": _map_load_index(props),
                "municipality": props.get("municipality"),
                "place_city": props.get("sc_placecity"),
                "raw": json.dumps(feature, ensure_ascii=False),
                **geom_params,
            }

            try:
                with db.begin_nested():  # SAVEPOINT — откат только этой фичи
                    result = db.execute(
                        text(f"""
                            INSERT INTO power_supply_centers
                                (source, external_id, sc_name, sc_name_norm, dzo_name,
                                 org_name, branch_name, voltage_class, load_index,
                                 municipality, place_city, geom, raw, fetched_at)
                            VALUES (
                                :source, :external_id, :sc_name, :sc_name_norm, :dzo_name,
                                :org_name, :branch_name, :voltage_class, :load_index,
                                :municipality, :place_city, {geom_sql},
                                CAST(:raw AS jsonb), NOW()
                            )
                            ON CONFLICT (source, external_id) DO UPDATE
                               SET sc_name       = EXCLUDED.sc_name,
                                   sc_name_norm  = EXCLUDED.sc_name_norm,
                                   dzo_name      = EXCLUDED.dzo_name,
                                   org_name      = EXCLUDED.org_name,
                                   branch_name   = EXCLUDED.branch_name,
                                   voltage_class = EXCLUDED.voltage_class,
                                   load_index    = EXCLUDED.load_index,
                                   municipality  = EXCLUDED.municipality,
                                   place_city    = EXCLUDED.place_city,
                                   geom          = EXCLUDED.geom,
                                   raw           = EXCLUDED.raw,
                                   fetched_at    = NOW()
                            RETURNING (xmax = 0) AS is_insert
                        """),
                        params,
                    ).scalar()
                if result:
                    inserted += 1
                else:
                    updated += 1
            except Exception as e:
                logger.warning("rosseti_wfs upsert failed for %r: %s", sc_name, e)
                skipped += 1

        db.commit()
    except Exception as e:
        db.rollback()
        logger.exception("rosseti_wfs: unexpected error, outer tx rolled back: %s", e)
        raise
    finally:
        if owns_session:
            db.close()

    result_dict = {
        "fetched": fetched,
        "inserted": inserted,
        "updated": updated,
        "skipped": skipped,
    }
    logger.info("rosseti_wfs load done: %s", result_dict)
    return result_dict
