"""Overpass API extractor — global tennis court locations from OpenStreetMap.

Queries the Overpass API for all nodes/ways/relations tagged sport=tennis,
split across 10 geographic regions to avoid timeout on the ~150K+ global result.

Regional strategy:
  - Each region is a bounding box covering a continent or sub-continent
  - Each region is queried independently (POST with [bbox:...])
  - Overlapping bboxes are deduped on OSM element id
  - One region per POST (~10-40K elements each, well within Overpass limits)
  - Crash recovery: working JSONL accumulates completed regions; on restart
    already-written IDs are skipped, completed regions produce 0 new elements

Landing: {LANDING_DIR}/overpass_tennis/{year}/{month}/courts.jsonl.gz
"""

import json
import sqlite3
import time
from pathlib import Path

import niquests

from ._shared import post_overpass, run_extractor, setup_logging
from .utils import compress_jsonl_atomic, landing_path, load_partial_results

logger = setup_logging("padelnomics.extract.overpass_tennis")

EXTRACTOR_NAME = "overpass_tennis"

# Each region is [south, west, north, east] — Overpass bbox format
REGIONS = [
    {"name": "europe_west",    "bbox": "35.0,-11.0,61.0,8.0"},      # FR ES GB PT IE BE NL
    {"name": "europe_central", "bbox": "42.0,8.0,55.5,24.0"},       # DE IT AT CH CZ PL HU
    {"name": "europe_east",    "bbox": "35.0,24.0,72.0,60.0"},      # Nordics Baltics GR TR RO
    {"name": "north_america",  "bbox": "15.0,-170.0,72.0,-50.0"},   # US CA MX
    {"name": "south_america",  "bbox": "-56.0,-82.0,15.0,-34.0"},   # BR AR CL
    {"name": "asia_east",      "bbox": "18.0,73.0,54.0,150.0"},     # JP KR CN
    {"name": "asia_west",      "bbox": "-11.0,24.0,42.0,73.0"},     # Middle East India
    {"name": "oceania",        "bbox": "-50.0,110.0,5.0,180.0"},    # AU NZ
    {"name": "africa",         "bbox": "-35.0,-18.0,37.0,52.0"},    # ZA EG MA
    {"name": "asia_north",     "bbox": "42.0,60.0,82.0,180.0"},     # RU-east KZ
]

MAX_RETRIES_PER_REGION = 2
RETRY_DELAY_SECONDS = 30       # Overpass cooldown between retries
REGION_TIMEOUT_SECONDS = 180   # Client-side per-region timeout (server uses 150s)
INTER_REGION_DELAY_SECONDS = 5 # Polite delay between regions


def _region_query(bbox: str) -> str:
    """Build an Overpass QL query for tennis courts within a bounding box."""
    return (
        f"[out:json][timeout:150][bbox:{bbox}];\n"
        "(\n"
        "  node[\"sport\"=\"tennis\"];\n"
        "  way[\"sport\"=\"tennis\"];\n"
        "  rel[\"sport\"=\"tennis\"];\n"
        ");\n"
        "out center;"
    )


def _query_region(session: niquests.Session, region: dict) -> list[dict]:
    """POST one regional Overpass query, with mirror fallback. Returns OSM elements."""
    query = _region_query(region["bbox"])
    resp = post_overpass(session, query, REGION_TIMEOUT_SECONDS)
    return resp.json().get("elements", [])


def extract(
    landing_dir: Path,
    year_month: str,
    conn: sqlite3.Connection,
    session: niquests.Session,
) -> dict:
    """Query Overpass for global tennis courts using regional bbox splitting.

    Splits the global query into REGIONS to avoid Overpass timeout.
    Writes one OSM element per line to courts.jsonl.gz.
    Crash-safe: working.jsonl accumulates results; on restart already-written
    element IDs are skipped so completed regions produce 0 new elements.
    """
    assert landing_dir.is_dir(), f"landing_dir must exist: {landing_dir}"
    assert "/" in year_month and len(year_month) == 7, f"year_month must be YYYY/MM: {year_month!r}"

    year, month = year_month.split("/")
    dest_dir = landing_path(landing_dir, "overpass_tennis", year, month)
    dest = dest_dir / "courts.jsonl.gz"
    old_blob = dest_dir / "courts.json.gz"

    if dest.exists() or old_blob.exists():
        logger.info("Already have courts for %s — skipping", year_month)
        return {"files_written": 0, "files_skipped": 1, "bytes_written": 0}

    # Crash recovery: load already-written elements from the working file
    working_path = dest_dir / "courts.working.jsonl"
    prior_records, already_seen_ids = load_partial_results(working_path, id_key="id")
    if already_seen_ids:
        logger.info("Resuming: %d elements already in working file", len(already_seen_ids))

    total_new = 0
    regions_succeeded: list[str] = []
    regions_failed: list[str] = []

    working_file = open(working_path, "a")  # noqa: SIM115
    try:
        for i, region in enumerate(REGIONS):
            for attempt in range(MAX_RETRIES_PER_REGION + 1):
                try:
                    elements = _query_region(session, region)
                    new_elements = [e for e in elements if str(e.get("id", "")) not in already_seen_ids]
                    for elem in new_elements:
                        working_file.write(json.dumps(elem, separators=(",", ":")) + "\n")
                        already_seen_ids.add(str(elem["id"]))
                    working_file.flush()
                    total_new += len(new_elements)
                    regions_succeeded.append(region["name"])
                    logger.info(
                        "Region %s: %d elements (%d new, %d total)",
                        region["name"], len(elements), len(new_elements), len(already_seen_ids),
                    )
                    break
                except niquests.exceptions.RequestException as exc:
                    if attempt < MAX_RETRIES_PER_REGION:
                        logger.warning(
                            "Region %s attempt %d failed: %s — retrying in %ds",
                            region["name"], attempt + 1, exc, RETRY_DELAY_SECONDS,
                        )
                        time.sleep(RETRY_DELAY_SECONDS)
                    else:
                        regions_failed.append(region["name"])
                        logger.error(
                            "Region %s failed after %d attempts: %s",
                            region["name"], MAX_RETRIES_PER_REGION + 1, exc,
                        )

            if i < len(REGIONS) - 1:
                time.sleep(INTER_REGION_DELAY_SECONDS)
    finally:
        working_file.close()

    total_elements = len(prior_records) + total_new
    if total_elements == 0:
        raise RuntimeError(f"All regions failed, no elements written: {regions_failed}")

    if regions_failed:
        logger.warning("Completed with %d failed regions: %s", len(regions_failed), regions_failed)

    bytes_written = compress_jsonl_atomic(working_path, dest)
    logger.info(
        "%d total elements (%d regions, %d failed) -> %s (%s bytes)",
        total_elements, len(regions_succeeded), len(regions_failed), dest, f"{bytes_written:,}",
    )

    return {
        "files_written": 1,
        "files_skipped": 0,
        "bytes_written": bytes_written,
        "cursor_value": year_month,
    }


def main() -> None:
    run_extractor(EXTRACTOR_NAME, extract)


if __name__ == "__main__":
    main()
