"""Fetch кадастровые квартал-полигоны из NSPD и загрузить в PostGIS.

Usage:
    cd backend && uv run python ../data/sql/58_fetch_cad_quarters_nspd.py [options]

Options:
    --scope ekb|region   ekb (default): только cad-кварталы с ДДУ-сделками
                         в ЕКБ (~674 кварталов). region: вся Свердл (~10 853).
    --batch-size N       commit каждые N кварталов (default 25).
    --rate-limit-ms N    пауза между запросами в мс (default 700).
    --resume             пропустить cad-кварталы уже в cad_quarters_geom.
    --max N              ограничить N запросами (для smoke-теста).
    --dry-run            fetch+parse only, skip DB upsert.

ВАЖНО: NSPD блокирует запросы с не-российских IP (WAF). Скрипт нужно запускать
с машины в РФ. Если получаешь HTTP 403 + 'Client IP: ...' — это блок гео.

Endpoint: GET https://nspd.gov.ru/api/geoportal/v2/search/geoportal
          ?thematicSearchId=1&query={cad_number}
Returns: GeoJSON FeatureCollection. Геометрия MultiPolygon в EPSG:3857 (web
         mercator). Скрипт переводит в EPSG:4326 через ST_Transform на стороне
         БД при INSERT.
"""

from __future__ import annotations

import argparse
import json
import logging
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path

# Add backend to path so we can import app.* in standalone CLI mode.
ROOT = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(ROOT / "backend"))

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger(__name__)

NSPD_URL = "https://nspd.gov.ru/api/geoportal/v2/search/geoportal"
HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
        "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
    ),
    "Accept": "application/json",
    "Accept-Language": "ru-RU,ru;q=0.9",
    "Referer": "https://nspd.gov.ru/map",
}


def fetch_cad_quarter(cad_number: str, *, retries: int = 3, timeout: int = 30) -> dict | None:
    """Запрос NSPD API для одного cad-номера.

    Returns: GeoJSON FeatureCollection or None если квартал не найден / ошибка.
    """
    qs = urllib.parse.urlencode(
        {
            "thematicSearchId": 1,  # 1 = кадастровые квартал
            "query": cad_number,
        }
    )
    url = f"{NSPD_URL}?{qs}"
    req = urllib.request.Request(url, headers=HEADERS)
    last_err: Exception | None = None
    for attempt in range(retries):
        try:
            with urllib.request.urlopen(req, timeout=timeout) as resp:
                return json.loads(resp.read().decode("utf-8"))
        except urllib.error.HTTPError as e:
            if e.code == 403:
                # WAF: гео-блок не лечится retry. Бросаем выше — оператор решит.
                body = e.read().decode("utf-8", errors="ignore")[:300]
                log.error("HTTP 403 from NSPD (likely geo-WAF). Body: %s", body)
                raise
            if e.code == 404:
                return None  # cad-квартал не найден — норма для глухих номеров.
            last_err = e
            log.warning("HTTP %d for %s (attempt %d/%d)", e.code, cad_number, attempt + 1, retries)
        except (urllib.error.URLError, TimeoutError, OSError) as e:
            last_err = e
            log.warning("Network err for %s: %s (attempt %d/%d)", cad_number, e, attempt + 1, retries)
        time.sleep(2 ** attempt)
    log.error("Failed %s after %d retries: %s", cad_number, retries, last_err)
    return None


def extract_geometry(geojson: dict | None, cad_number: str) -> tuple[dict | None, dict | None]:
    """Из GeoJSON выбрать первый MultiPolygon (или Polygon) под точный
    cad-номер. Returns (geometry_dict, raw_properties)."""
    if not geojson or "features" not in geojson:
        return None, None
    for feat in geojson["features"]:
        props = feat.get("properties") or {}
        # NSPD возвращает кадастровый номер в одном из этих полей.
        cn = (
            props.get("cn")
            or props.get("cadastralNumber")
            or props.get("cad_num")
            or props.get("CAD_NUMBER")
        )
        if cn and str(cn).strip() == cad_number:
            geom = feat.get("geometry")
            if not geom:
                continue
            # Принимаем Polygon → MultiPolygon (для unification).
            if geom.get("type") == "Polygon":
                geom = {"type": "MultiPolygon", "coordinates": [geom["coordinates"]]}
            return geom, props
    # Fallback: если features=1 и cn совпадает по prefix — берём первую.
    if len(geojson["features"]) == 1:
        feat = geojson["features"][0]
        geom = feat.get("geometry")
        if geom:
            if geom.get("type") == "Polygon":
                geom = {"type": "MultiPolygon", "coordinates": [geom["coordinates"]]}
            return geom, feat.get("properties") or {}
    return None, None


def candidates_to_fetch(scope: str, resume: bool) -> list[str]:
    """Возвращает список cad-номеров для скрейпа из rosreestr_deals."""
    from sqlalchemy import text

    from app.core.db import SessionLocal

    db = SessionLocal()
    try:
        if scope == "ekb":
            rows = db.execute(
                text(
                    """
                    SELECT DISTINCT quarter_cad_number
                      FROM rosreestr_deals
                     WHERE region_code = 66
                       AND doc_type = 'ДДУ'
                       AND realestate_type_code = '002001003000'
                       AND (district ILIKE '%Екатеринбург%' OR city ILIKE '%Екатеринбург%')
                       AND quarter_cad_number IS NOT NULL
                       AND quarter_cad_number <> ''
                    """
                )
            ).all()
        else:  # region
            rows = db.execute(
                text(
                    """
                    SELECT DISTINCT quarter_cad_number
                      FROM rosreestr_deals
                     WHERE region_code = 66
                       AND quarter_cad_number IS NOT NULL
                       AND quarter_cad_number <> ''
                    """
                )
            ).all()
        cads = sorted({r[0] for r in rows if r[0]})

        if resume:
            done = {
                r[0]
                for r in db.execute(
                    text("SELECT cad_number FROM cad_quarters_geom")
                ).all()
            }
            cads = [c for c in cads if c not in done]
            log.info("Resume: skipping %d already-loaded quarters", len(done))

        return cads
    finally:
        db.close()


def upsert_quarter(db, cad_number: str, geom_dict: dict, props: dict) -> None:
    """INSERT или UPDATE в cad_quarters_geom. Полигон приходит в EPSG:3857
    (web mercator); конвертируем в 4326 на стороне БД через ST_Transform."""
    from sqlalchemy import text

    db.execute(
        text(
            """
            INSERT INTO cad_quarters_geom (cad_number, geom, raw_props, fetched_at, source)
            VALUES (
                :cn,
                ST_Multi(
                    ST_Transform(
                        ST_SetSRID(ST_GeomFromGeoJSON(:gj), 3857),
                        4326
                    )
                )::geometry(MultiPolygon, 4326),
                CAST(:props AS jsonb),
                NOW(),
                'nspd'
            )
            ON CONFLICT (cad_number) DO UPDATE SET
                geom = EXCLUDED.geom,
                raw_props = EXCLUDED.raw_props,
                fetched_at = NOW()
            """
        ),
        {
            "cn": cad_number,
            "gj": json.dumps(geom_dict),
            "props": json.dumps(props, ensure_ascii=False),
        },
    )


def main() -> int:
    p = argparse.ArgumentParser(description="Fetch+load NSPD cad-quarter polygons")
    p.add_argument("--scope", choices=["ekb", "region"], default="ekb")
    p.add_argument("--batch-size", type=int, default=25)
    p.add_argument("--rate-limit-ms", type=int, default=700)
    p.add_argument("--resume", action="store_true", help="Skip already-loaded cad-numbers")
    p.add_argument("--max", type=int, default=None, help="Limit N requests (smoke test)")
    p.add_argument("--dry-run", action="store_true")
    args = p.parse_args()

    cads = candidates_to_fetch(args.scope, resume=args.resume)
    if args.max:
        cads = cads[: args.max]
    log.info("Plan to fetch %d cad-quarters (scope=%s)", len(cads), args.scope)
    if not cads:
        log.info("Nothing to do.")
        return 0

    if args.dry_run:
        log.info("Dry-run: would fetch %s", cads[:5])
        return 0

    from sqlalchemy import text  # noqa: F401

    from app.core.db import SessionLocal

    db = SessionLocal()
    ok = 0
    skipped = 0
    failed = 0
    try:
        for i, cn in enumerate(cads, 1):
            try:
                gj = fetch_cad_quarter(cn)
                geom, props = extract_geometry(gj, cn)
                if geom is None:
                    log.warning("[%d/%d] %s — no geometry returned", i, len(cads), cn)
                    skipped += 1
                else:
                    upsert_quarter(db, cn, geom, props or {})
                    ok += 1
                    if i % 10 == 0 or i == len(cads):
                        log.info("[%d/%d] %s ✓ (ok=%d skip=%d fail=%d)",
                                 i, len(cads), cn, ok, skipped, failed)
            except urllib.error.HTTPError as e:
                if e.code == 403:
                    db.commit()
                    log.error(
                        "Aborting: NSPD WAF блокирует запросы (HTTP 403). "
                        "Скрипт нужно запускать с российского IP. Saved %d so far.",
                        ok,
                    )
                    return 2
                failed += 1
            except Exception as e:  # noqa: BLE001
                failed += 1
                log.exception("[%d/%d] %s ✗ %s", i, len(cads), cn, e)
            if i % args.batch_size == 0:
                db.commit()
                log.info("  → committed batch (i=%d, ok=%d)", i, ok)
            time.sleep(args.rate_limit_ms / 1000.0)
        db.commit()
        log.info("Done: ok=%d, skipped=%d, failed=%d", ok, skipped, failed)
        return 0
    finally:
        db.close()


if __name__ == "__main__":
    sys.exit(main())
