"""Celery task: прогрев кэша ПЗЗ-регламента зон ЕКБ → ``zone_regulation_cache`` (#1059 / C8b).

Регламент ПЗЗ идентичен для всех участков одной терзоны (~100 зон на ЕКБ). C8b (#1099) умеет
резолвить регламент по точке (`zone_regulation_at` → urbanCard) и кэшировать по `zone_index`, но
кэш наполняется лениво — первый запрос участка в незакэшированной зоне платит за live-urbanCard.

Эта задача наполняет кэш ПРОАКТИВНО: WFS-перечисление всех терзон ЕКБ → по одной репрезентативной
точке на уникальный `urban_index` → `zone_regulation_at` → `upsert_zone_regulation`. После прогона
analyze отдаёт `ird.zone_regulation` из кэша (быстро), без live-геопортала в hot-пути.

Дедуп по `urban_index`: регламент одинаков для всех полигонов зоны → резолвим индекс один раз.
Репрезентативная точка (`shapely.representative_point`) гарантированно внутри полигона (в отличие
от centroid для вогнутых) → searchByGeom находит терзону.

Mirror conventions (planning_harvest.py / backend.md): SessionLocal()+try/finally; per-zone
try/except (сбой одной зоны не валит прогон); commit в конце. Расписание — ежемесячно (регламент
меняется только при правке ПЗЗ).
"""

from __future__ import annotations

import logging
from typing import Any

from shapely.geometry import shape

from app.core.db import SessionLocal
from app.services.scrapers.ekb_geoportal_client import EKBGeoportalClient
from app.services.site_finder.zone_regulation import upsert_zone_regulation
from app.workers.celery_app import celery_app

logger = logging.getLogger(__name__)

# bbox агломерации ЕКБ (minlon, minlat, maxlon, maxlat) EPSG:4326 — как в planning_harvest.
EKB_AGGLOMERATION_BBOX: tuple[float, float, float, float] = (60.0, 56.6, 61.1, 57.1)


def _representative_point(geometry: dict[str, Any] | None) -> tuple[float, float] | None:
    """Точка (lon, lat) гарантированно внутри полигона зоны. None при невалидной геометрии."""
    if not geometry:
        return None
    try:
        pt = shape(geometry).representative_point()
        return (float(pt.x), float(pt.y))
    except Exception as exc:  # битая геометрия фичи — пропускаем зону
        logger.warning("zone_regulation_refresh: bad geometry, skip: %s", exc)
        return None


@celery_app.task(name="tasks.zone_regulation_refresh.refresh_zone_regulations")
def refresh_zone_regulations(
    bbox: tuple[float, float, float, float] | None = None,
) -> dict[str, int]:
    """Прогрев кэша регламента по терзонам ЕКБ.

    Args:
        bbox: (minlon, minlat, maxlon, maxlat) EPSG:4326. None → bbox агломерации ЕКБ.

    Returns:
        {'zones': N, 'cached': M} — уникальных индексов зон / успешно закэшировано регламентов.
    """
    target_bbox = bbox if bbox is not None else EKB_AGGLOMERATION_BBOX
    client = EKBGeoportalClient()
    db = SessionLocal()
    seen: set[str] = set()
    n_cached = 0
    try:
        feats = client.features_in_bbox("territorial_zone", target_bbox)
        logger.info("zone_regulation_refresh: терзон в bbox=%d", len(feats))
        for feat in feats:
            idx = feat.properties.get("urban_index")
            if not idx or str(idx) in seen:
                continue
            seen.add(str(idx))
            point = _representative_point(feat.geometry)
            if point is None:
                continue
            lon, lat = point
            try:
                reg = client.zone_regulation_at(lon, lat)
            except Exception as exc:
                logger.warning("zone_regulation_refresh: resolve failed idx=%s: %s", idx, exc)
                continue
            if reg is None or not reg.zone_index:
                continue
            if upsert_zone_regulation(db, reg) is not None:
                n_cached += 1
        db.commit()
        logger.info("zone_regulation_refresh: зон=%d закэшировано=%d", len(seen), n_cached)
        return {"zones": len(seen), "cached": n_cached}
    finally:
        db.close()
