"""Полный NSPD scrape для всех cad-кварталов ЕКБ с ДДУ-сделками.

Запускать с машины пользователя (РФ-IP, иначе WAF).
SSH-tunnel к prod БД на localhost:15432.

Что делает:
1. Берёт список cad-номеров из rosreestr_deals (region=66, ЕКБ, ДДУ, квартиры).
2. Пропускает уже скрейпнутые (есть в cad_quarters_geom).
3. Для каждого cad-квартала:
   a. fetch /api/geoportal/v2/search/geoportal?thematicSearchId=2 → polygon
   b. fetch /api/geoportal/v2/search/geoportal?thematicSearchId=1 → 20 зданий + земля
4. INSERT в cad_quarters_geom + cad_buildings (фильтр cad_num LIKE '66:41:%').
5. После каждого квартала commit + лог. Можно прервать и перезапустить.

Usage:
    cd backend && uv run python ../data/sql/62_scrape_nspd_full.py [--limit N] [--rate-ms 600]
"""

from __future__ import annotations

import argparse
import json
import os
import ssl
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
from typing import Any

import psycopg

PG_DSN = os.environ.get(
    "TUNNEL_DSN",
    "postgresql://gendesign:2J2SBPMKuS998fiwhtQqDhMI@localhost:15432/gendesign",
)

NSPD_BASE = "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",
}
SSL_CTX = ssl._create_unverified_context()


def nspd_fetch(thematic_search_id: int, query: str, *, retries: int = 5, timeout: int = 30) -> dict | None:
    """Fetch NSPD with backoff. WAF banит burst-запросы — на 403 ждём долго."""
    qs = urllib.parse.urlencode({"thematicSearchId": thematic_search_id, "query": query})
    url = f"{NSPD_BASE}?{qs}"
    req = urllib.request.Request(url, headers=HEADERS)
    last_err = None
    for attempt in range(retries):
        try:
            with urllib.request.urlopen(req, timeout=timeout, context=SSL_CTX) as r:
                return json.loads(r.read().decode("utf-8"))
        except urllib.error.HTTPError as e:
            if e.code == 404:
                return None
            last_err = e
            if e.code == 403:
                # WAF rate-limit — ждём дольше (60 сек)
                wait = 60 + 30 * attempt
                print(f"  WAF 403 for {query} (attempt {attempt+1}/{retries}) — sleeping {wait}s", flush=True)
                time.sleep(wait)
                continue
            print(f"  HTTP {e.code} for {query} (attempt {attempt+1})", flush=True)
        except (urllib.error.URLError, TimeoutError, OSError) as e:
            last_err = e
            print(f"  Network err for {query}: {e} (attempt {attempt+1})", flush=True)
        time.sleep(min(2 ** attempt, 30))
    print(f"  FAILED {query}: {last_err}", flush=True)
    return None


def poly_to_wkt(geom: dict | None) -> str | None:
    if not geom:
        return None
    def ring(r):
        return "(" + ",".join(f"{p[0]} {p[1]}" for p in r) + ")"
    t = geom.get("type")
    if t == "Polygon":
        return "POLYGON(" + ",".join(ring(r) for r in geom["coordinates"]) + ")"
    if t == "MultiPolygon":
        return "MULTIPOLYGON(" + ",".join("(" + ",".join(ring(r) for r in p) + ")" for p in geom["coordinates"]) + ")"
    if t == "Point":
        return f"POINT({geom['coordinates'][0]} {geom['coordinates'][1]})"
    return None


def get_pending_cads(cur) -> list[str]:
    """Все cad-номера с ДДУ-сделками в ЕКБ — минус уже scraped."""
    cur.execute("""
        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 <> ''
           AND quarter_cad_number <> '66:41:0000000'
    """)
    all_cads = {r[0] for r in cur.fetchall()}
    cur.execute("SELECT cad_number FROM cad_quarters_geom")
    done = {r[0] for r in cur.fetchall()}
    pending = sorted(all_cads - done)
    return pending


def insert_quarter(cur, cad: str, wkt: str) -> None:
    cur.execute(
        """
        INSERT INTO cad_quarters_geom (cad_number, geom, source)
        VALUES (
            %s,
            ST_Multi(ST_Transform(ST_SetSRID(ST_GeomFromText(%s), 3857), 4326))::geometry(MultiPolygon, 4326),
            'nspd'
        )
        ON CONFLICT (cad_number) DO UPDATE SET geom = EXCLUDED.geom, fetched_at = NOW()
        """,
        (cad, wkt),
    )


def insert_buildings(cur, qcad: str, features: list[dict]) -> int:
    """Insert 'Здания' filtered by cad LIKE '66:41:%'."""
    n = 0
    for f in features:
        if f.get("properties", {}).get("categoryName") != "Здания":
            continue
        opts = f["properties"].get("options", {}) or {}
        cn = opts.get("cad_num")
        if not cn or not cn.startswith("66:41:"):
            continue
        wkt = poly_to_wkt(f.get("geometry"))
        if not wkt:
            continue
        cur.execute(
            """
            INSERT INTO cad_buildings (
                cad_num, quarter_cad_num, geom, purpose, building_name,
                readable_address, area, floors, year_built, year_commisioning,
                cost_value, registration_date
            )
            VALUES (
                %s, %s, ST_Transform(ST_SetSRID(ST_GeomFromText(%s), 3857), 4326),
                %s, %s, %s, %s, %s, %s, %s, %s, %s
            )
            ON CONFLICT (cad_num) DO UPDATE SET
                cost_value = EXCLUDED.cost_value,
                area = EXCLUDED.area,
                fetched_at = NOW()
            """,
            (
                cn, qcad, wkt,
                opts.get("purpose"), opts.get("building_name"),
                opts.get("readable_address"), opts.get("area"), opts.get("floors"),
                int(opts["year_built"]) if opts.get("year_built") else None,
                int(opts["year_commisioning"]) if opts.get("year_commisioning") else None,
                opts.get("cost_value"),
                opts.get("registration_date", "").split(" ")[0] if opts.get("registration_date") else None,
            ),
        )
        n += 1
    return n


def main() -> int:
    p = argparse.ArgumentParser()
    p.add_argument("--limit", type=int, default=None, help="Limit N quarters (smoke)")
    p.add_argument("--rate-ms", type=int, default=600, help="Pause between cads (ms)")
    p.add_argument("--commit-every", type=int, default=10, help="Commit every N quarters")
    args = p.parse_args()

    conn = psycopg.connect(PG_DSN, connect_timeout=10)
    cur = conn.cursor()
    pending = get_pending_cads(cur)
    if args.limit:
        pending = pending[: args.limit]
    print(f"Pending: {len(pending)} cad-quarters")
    if not pending:
        print("Nothing to do.")
        return 0

    started = time.time()
    ok = 0
    n_buildings = 0
    failed = 0
    for i, cn in enumerate(pending, 1):
        try:
            j2 = nspd_fetch(2, cn)
            qf = None
            if j2:
                features = j2.get("data", {}).get("features") or []
                for f in features:
                    if f.get("properties", {}).get("label") == cn:
                        qf = f
                        break
            wkt = poly_to_wkt(qf.get("geometry") if qf else None) if qf else None
            if wkt:
                insert_quarter(cur, cn, wkt)

            j1 = nspd_fetch(1, cn)
            if j1:
                features = j1.get("data", {}).get("features") or []
                n_buildings += insert_buildings(cur, cn, features)

            if wkt:
                ok += 1
            else:
                failed += 1

            if i % args.commit_every == 0:
                conn.commit()
                elapsed = time.time() - started
                rate = ok / elapsed * 60 if elapsed > 0 else 0
                eta = (len(pending) - i) / max(rate, 1) if rate > 0 else 0
                print(
                    f"[{i}/{len(pending)}] {cn} "
                    f"ok={ok} fail={failed} buildings={n_buildings} "
                    f"rate={rate:.1f}/min eta={eta:.0f}min",
                    flush=True,
                )
        except Exception as e:  # noqa: BLE001
            failed += 1
            conn.rollback()
            print(f"  ERR {cn}: {e}", flush=True)

        time.sleep(args.rate_ms / 1000.0)

    conn.commit()
    elapsed = time.time() - started
    cur.execute("SELECT COUNT(*) FROM cad_quarters_geom")
    db_q = cur.fetchone()[0]
    cur.execute("SELECT COUNT(*) FROM cad_buildings")
    db_b = cur.fetchone()[0]
    print(f"\nDONE in {elapsed:.0f}s. ok={ok} failed={failed} buildings_inserted={n_buildings}")
    print(f"DB totals: cad_quarters_geom={db_q}, cad_buildings={db_b}")
    conn.close()
    return 0


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