"""Eurostat SDMX city codelist extractor — city_code → city_name mapping.

The Eurostat Urban Audit population dataset (urb_cpop1) uses coded city identifiers
(e.g. DE001C = Berlin) with no city name column. This extractor fetches the SDMX
codelist that maps those codes to human-readable names, enabling stg_city_labels to
join population data to dim_cities (which has names, not codes).

The codelist changes very rarely so ETag dedup means most runs produce a 304 skip.

Landing: {LANDING_DIR}/eurostat_city_labels/{year}/{month}/cities_codelist.json.gz
Output:  {"rows": [{"city_code": "DE001C", "city_name": "Berlin"}, ...], "count": N}
"""

import json
import sqlite3
from pathlib import Path

import niquests

from ._shared import HTTP_TIMEOUT_SECONDS, run_extractor, setup_logging
from .utils import landing_path, write_gzip_atomic

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

EXTRACTOR_NAME = "eurostat_city_labels"

# SDMX codelist endpoint — returns the full CITIES dimension codes with labels
# format=JSON gives a compact JSON-stat-like structure for the codelist
CODELIST_URL = (
    "https://ec.europa.eu/eurostat/api/dissemination/sdmx/2.1/codelist/ESTAT/CITIES"
    "?format=JSON&lang=EN"
)


def _parse_sdmx_codelist(data: dict) -> list[dict]:
    """Extract city_code → city_name pairs from Eurostat dimension JSON response.

    Eurostat's /codelist endpoint (format=JSON) returns a compact dimension
    object where data["category"]["label"] is a flat dict mapping code → name:
      {"BE001C": "Bruxelles/Brussel (greater city)", "DE001C": "Berlin", ...}

    Country-level entries (e.g. "BE") are included in the same dict; we filter
    them out by requiring a digit in the code (city codes look like DE001C).
    """
    try:
        label_map = data["category"]["label"]
    except (KeyError, TypeError) as e:
        raise ValueError(f"Unexpected SDMX structure — missing category.label: {e}") from e

    assert len(label_map) > 0, "SDMX category.label is empty — API response may have changed"

    rows: list[dict] = []
    for city_code, city_name in label_map.items():
        city_code = city_code.strip()
        city_name = str(city_name).strip() if city_name else ""
        # City codes contain digits (e.g. DE001C); country codes (e.g. DE) do not
        if not city_code or not city_name or not any(c.isdigit() for c in city_code):
            continue
        rows.append({"city_code": city_code, "city_name": city_name})

    return rows


def _etag_path(dest: Path) -> Path:
    return dest.parent / (dest.name + ".etag")


def extract(
    landing_dir: Path,
    year_month: str,
    conn: sqlite3.Connection,
    session: niquests.Session,
) -> dict:
    """Fetch Eurostat CITIES codelist with ETag dedup. Returns run metrics."""
    year, month = year_month.split("/")
    dest_dir = landing_path(landing_dir, "eurostat_city_labels", year, month)
    dest = dest_dir / "cities_codelist.json.gz"
    etag_file = _etag_path(dest)

    headers: dict[str, str] = {}
    if etag_file.exists():
        headers["If-None-Match"] = etag_file.read_text().strip()

    logger.info("GET CITIES codelist")
    resp = session.get(CODELIST_URL, headers=headers, timeout=HTTP_TIMEOUT_SECONDS)

    if resp.status_code == 304:
        logger.info("CITIES codelist not modified (304)")
        return {"files_written": 0, "files_skipped": 1, "bytes_written": 0}

    resp.raise_for_status()

    rows = _parse_sdmx_codelist(resp.json())
    assert len(rows) > 100, f"Expected >100 city codes, got {len(rows)} — parse may have failed"

    payload = json.dumps({"rows": rows, "count": len(rows)}).encode()
    bytes_written = write_gzip_atomic(dest, payload)
    logger.info("written %d city codes (%s bytes compressed)", len(rows), f"{bytes_written:,}")

    if etag := resp.headers.get("etag"):
        etag_file.parent.mkdir(parents=True, exist_ok=True)
        etag_file.write_text(etag)

    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()
