"""Celery task: harvest opportunity-ЗУ (future-supply) из НСПД → ``ird_overlays`` (#1086).

Future-supply / land opportunity — свободная и выставляемая под застройку земля. Источник —
НСПД (федеральный, единообразный, vault research/NSPD_IRD_Layers_Automation_Recon_Jun06). В центре
города такие ЗУ редки (точечный probe даёт 0), но по агломерации ЕКБ присутствуют → нужен
СПЛОШНОЙ grid-walk по кварталам, как в ИРД-harvest (#1067 B5).

DIRECT-сигналы «земля под застройку» (ID в nspd_client.LAYERS):
  37299 ЗУ на аукционе · 37294 ЗУ по схеме расположения · 37298 ЗУ свободные от прав ·
  36473 ЗУ по проекту межевания · 37430 территории ККР (комплексное развитие).

Переиспользует ДВИЖОК ИРД-harvest (один grid-walk + UPSERT-engine, не дублируем): тот же
``ird_overlays`` (м.132), тот же ``_upsert_feature`` (ST_Transform 3857→4326, SAVEPOINT per-row,
ON CONFLICT по (source_layer_id, geom_data_id)), те же кварталы ЕКБ (``_ekb_quarters``). Отличие —
набор слоёв (opportunity вместо ограничений) и ``layer_kind='opportunity_*'`` для фильтра в analyze.

Расписание — ежемесячно (beat_schedule.py: opportunity-harvest-monthly): opportunity-земля
меняется медленнее ограничений, еженедельный прогон избыточен.
"""

from __future__ import annotations

import logging
from datetime import UTC, datetime

from app.core.db import SessionLocal
from app.services.scrapers.nspd_client import LAYERS, NSPDClient, _geojson_bbox_3857
from app.workers.celery_app import celery_app
from app.workers.tasks.ird_harvest import _ekb_quarters, _upsert_feature

logger = logging.getLogger(__name__)

# Opportunity-слои: ключ nspd_client.LAYERS → layer_kind в ird_overlays ('opportunity_*' для
# фильтра в analyze «рядом свободная/аукционная земля»). НЕ ограничения — отдельный бакет.
OPPORTUNITY_LAYER_KINDS: dict[str, str] = {
    "auction_parcels": "opportunity_auction",
    "scheme_parcels": "opportunity_scheme",
    "free_parcels": "opportunity_free",
    "future_parcels": "opportunity_future",
    "krt_territories": "opportunity_krt",
}


@celery_app.task(name="tasks.opportunity_harvest.harvest_opportunity_overlays")
def harvest_opportunity_overlays(quarters: list[str] | None = None) -> dict[str, int]:
    """Harvest opportunity-ЗУ НСПД по кварталам ЕКБ → ird_overlays (layer_kind='opportunity_*').

    Args:
        quarters: явный список cad-номеров кварталов ('66:41:NNNNNNN'). None → distinct
            кварталы ЕКБ из cad_parcels (``_ekb_quarters``).

    Returns:
        {'quarters': N, 'features': M} — обработано кварталов / записано opportunity-фич.
    """
    fetched_at = datetime.now(UTC).isoformat()
    client = NSPDClient()
    db = SessionLocal()
    n_quarters = 0
    n_features = 0
    try:
        quarter_list = quarters if quarters is not None else _ekb_quarters(db)
        logger.info("opportunity_harvest: старт, кварталов=%d", len(quarter_list))
        for quarter in quarter_list:
            try:
                qres = client.search_by_cad(quarter, thematic_id=2)
                qfeat = qres.first
                if not qfeat or not qfeat.geometry:
                    logger.info("opportunity_harvest: квартал %s пуст в НСПД, скип", quarter)
                    continue
                bbox = _geojson_bbox_3857(qfeat.geometry)
                if bbox is None:
                    continue
                # Staging-счётчик квартала: прибавляем к итогам ТОЛЬКО после успешного
                # commit (строка ниже). Иначе при сбое commit + rollback откатятся
                # незакоммиченные UPSERT'ы, а отчёт завысит число persisted фич (#1624).
                q_features = 0
                for layer_key, layer_kind in OPPORTUNITY_LAYER_KINDS.items():
                    layer_id = LAYERS.get(layer_key)
                    if layer_id is None:
                        logger.warning("opportunity_harvest: нет layer_id для %s", layer_key)
                        continue
                    try:
                        feats = client.get_features_in_bbox_grid(layer_id, bbox)
                    except Exception as exc:
                        logger.warning(
                            "opportunity_harvest: grid-walk failed quarter=%s layer=%s: %s",
                            quarter,
                            layer_key,
                            exc,
                        )
                        continue
                    for feat in feats:
                        if _upsert_feature(
                            db,
                            layer_id=layer_id,
                            layer_kind=layer_kind,
                            feature=feat,
                            fetched_at=fetched_at,
                        ):
                            q_features += 1
                # Durable per-quarter commit: длинный grid-walk не теряет прогресс при
                # краше/таймауте середины прогона (commit раз-в-конце терял ВСЁ).
                db.commit()
                # Commit прошёл → фичи квартала реально persisted, учитываем в отчёте.
                n_quarters += 1
                n_features += q_features
            except Exception as exc:
                logger.warning("opportunity_harvest: квартал %s failed: %s", quarter, exc)
                db.rollback()  # сбросить незакоммиченный tx квартала перед следующим
                continue
        db.commit()  # финальный — на случай пустого списка / подстраховка
        logger.info("opportunity_harvest: готово, кварталов=%d фич=%d", n_quarters, n_features)
        return {"quarters": n_quarters, "features": n_features}
    finally:
        db.close()
