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

Загружает ~6436 полигонов функциональных зон из WFS ГИСОГД-СО и UPSERT-ит их в
``ekb_genplan_functional_zone`` (м.139) по ключу ``globalid`` (UUID объекта).

Зеркалит конвенции ird_harvest.py / supply_layers_refresh.py:
  • SessionLocal() + try/finally close; logger (не print).
  • SAVEPOINT per-row (``with db.begin_nested():``) — одна битая фича не валит батч.
  • CAST(:x AS type) — НИКОГДА :x::type (psycopg v3).
  • Геометрия: ST_MakeValid(ST_SetSRID(ST_GeomFromGeoJSON, 4326)) — без transform (уже 4326);
    ST_MakeValid исправляет self-intersecting/ring-order дефекты геопортала (#1156).
  • ON CONFLICT (globalid) DO UPDATE: обновляет все поля кроме id/fetched_at (latest wins).
  • commit один раз в конце.

Beat: «genplan-zones-sync-weekly» (понедельник 06:30 МСК) — после supply-layers-refresh.
Admin trigger: при необходимости ручного запуска можно сделать POST-эндпоинт.
"""

from __future__ import annotations

import json
import logging
from datetime import UTC, datetime
from typing import Any

from sqlalchemy import text
from sqlalchemy.orm import Session

from app.core.db import SessionLocal
from app.services.scrapers.gisogd_so_client import fetch_functional_zones
from app.workers.celery_app import celery_app

logger = logging.getLogger(__name__)

# UPSERT одной фичи. geom = ST_MakeValid(ST_SetSRID(ST_GeomFromGeoJSON, 4326)) —
# ST_MakeValid оборачивает результат чтобы исправить self-intersecting/ring-order
# дефекты из источника геопортала (881/6436 полигонов имели ST_IsValid=false, #1156).
# ON CONFLICT обновляет все аналитические поля; fetched_at = excluded (latest wins).
# CAST(:x AS type) — psycopg v3 (НИКОГДА :x::type).
_UPSERT_SQL = text(
    """
    INSERT INTO ekb_genplan_functional_zone (
        globalid, zone_type, zone_class_code, status,
        area_ha, doc_ref, geom, raw_props, fetched_at
    ) VALUES (
        CAST(:globalid AS text),
        CAST(:zone_type AS text),
        CAST(:zone_class_code AS text),
        CAST(:status AS text),
        CAST(:area_ha AS numeric),
        CAST(:doc_ref AS text),
        ST_MakeValid(ST_SetSRID(ST_GeomFromGeoJSON(CAST(:geojson AS text)), 4326)),
        CAST(:raw_props AS jsonb),
        CAST(:fetched_at AS timestamptz)
    )
    ON CONFLICT (globalid) DO UPDATE SET
        zone_type       = EXCLUDED.zone_type,
        zone_class_code = EXCLUDED.zone_class_code,
        status          = EXCLUDED.status,
        area_ha         = EXCLUDED.area_ha,
        doc_ref         = EXCLUDED.doc_ref,
        geom            = EXCLUDED.geom,
        raw_props       = EXCLUDED.raw_props,
        fetched_at      = EXCLUDED.fetched_at
    """
)


def _extract_params(feature: dict[str, Any], fetched_at: str) -> dict[str, Any] | None:
    """Извлекает параметры UPSERT из GeoJSON-фичи.

    Возвращает None если фича не пригодна для записи (нет geometry или globalid).
    """
    geom = feature.get("geometry")
    props: dict[str, Any] = feature.get("properties") or {}

    globalid = props.get("urban10_globalid") or props.get("globalid")
    if not geom or not globalid:
        return None

    # zone_type: приоритет full_name, fallback urban10_classid_v.
    zone_type = props.get("full_name") or props.get("urban10_classid_v")
    zone_class_code_raw = props.get("urban10_classid")
    zone_class_code = str(zone_class_code_raw) if zone_class_code_raw is not None else None

    area_raw = props.get("urban10_area")
    try:
        area_ha = float(area_raw) if area_raw is not None else None
    except (TypeError, ValueError):
        area_ha = None

    return {
        "globalid": str(globalid),
        "zone_type": str(zone_type) if zone_type is not None else None,
        "zone_class_code": zone_class_code,
        "status": props.get("urban10_status_v"),
        "area_ha": area_ha,
        "doc_ref": props.get("info_set_key_ref"),
        "geojson": json.dumps(geom),
        "raw_props": json.dumps(props, ensure_ascii=False),
        "fetched_at": fetched_at,
    }


def _upsert_feature(db: Session, params: dict[str, Any]) -> bool:
    """UPSERT одной фичи под SAVEPOINT. True если записана, False при ошибке."""
    try:
        with db.begin_nested():
            db.execute(_UPSERT_SQL, params)
        return True
    except Exception as exc:
        logger.warning(
            "genplan_zones_sync: upsert failed globalid=%s: %s",
            params.get("globalid"),
            exc,
        )
        return False


@celery_app.task(name="tasks.genplan_zones_sync.sync_functional_zones")
def sync_functional_zones() -> dict[str, int]:
    """Загружает функц.зоны из ГИСОГД-СО WFS → UPSERT ekb_genplan_functional_zone.

    Returns:
        ``{"zones": N}`` — количество успешно записанных зон.
    """
    fetched_at = datetime.now(UTC).isoformat()
    db = SessionLocal()
    n_zones = 0
    n_skip = 0
    try:
        logger.info("genplan_zones_sync: старт загрузки функциональных зон ГИСОГД-СО")
        features = fetch_functional_zones()
        logger.info("genplan_zones_sync: получено фич=%d", len(features))

        for feat in features:
            params = _extract_params(feat, fetched_at)
            if params is None:
                n_skip += 1
                continue
            if _upsert_feature(db, params):
                n_zones += 1
            else:
                n_skip += 1

        db.commit()
        logger.info("genplan_zones_sync: готово, zones=%d skip=%d", n_zones, n_skip)
        return {"zones": n_zones}
    finally:
        db.close()
