"""
OpenStreetMap utilities.

Handles geocoding, deterministic caching, and downloading of exact OSM regions
to serve as a Single Source of Truth for the routing engine and the topological graph.
"""

import hashlib
import logging
import os
from pathlib import Path
from typing import Optional, Tuple

import requests

logger = logging.getLogger(__name__)


def geocode_place_to_bbox(place: str) -> Tuple[float, float, float, float]:
    """Geocode a place name to a bounding box using Nominatim.

    Args:
        place: Free-text place name (e.g. ``"Zurich, Switzerland"``).

    Returns:
        ``(north, south, east, west)`` bounding box in decimal degrees.

    Raises:
        ValueError: If Nominatim returns no results for *place*.
    """
    logger.info("Geocoding place '%s' via Nominatim...", place)
    r = requests.get(
        "https://nominatim.openstreetmap.org/search",
        params={"q": place, "format": "json", "limit": 1},
        headers={"User-Agent": "walkingpandas-valhalla/1.0"},
        timeout=30,
    )
    r.raise_for_status()
    data = r.json()

    if not data:
        raise ValueError(f"Place not found: {place}")

    b = data[0].get("boundingbox")
    if not b or len(b) < 4:
        lat, lon = float(data[0]["lat"]), float(data[0]["lon"])
        delta = 0.01
        bbox = (lat + delta, lat - delta, lon + delta, lon - delta)
    else:
        # Nominatim returns [south, north, west, east] as strings
        south, north, west, east = (
            float(b[0]),
            float(b[1]),
            float(b[2]),
            float(b[3]),
        )
        bbox = (north, south, east, west)

    return bbox


def generate_cache_key(
    bbox: Optional[Tuple[float, float, float, float]] = None,
    place: Optional[str] = None,
    osm_pbf: Optional[os.PathLike] = None,
) -> str:
    """Generate a deterministic hash key for the given region or file.

    Exactly one of *bbox*, *place*, or *osm_pbf* must be provided.

    Args:
        bbox: ``(north, south, east, west)`` bounding box.
        place: Free-text place name to hash.
        osm_pbf: Path to a ``.osm.pbf`` file (hashes path + size + mtime).

    Returns:
        A 16-character hex digest.
    """
    if bbox is not None:
        raw = f"bbox:{bbox[0]:.6f},{bbox[1]:.6f},{bbox[2]:.6f},{bbox[3]:.6f}"
    elif place is not None:
        raw = f"place:{place}"
    elif osm_pbf is not None:
        p = Path(osm_pbf).resolve()
        if p.exists():
            stat = p.stat()
            raw = f"osm_pbf:{p!s}:{stat.st_size}:{stat.st_mtime}"
        else:
            raw = f"osm_pbf:{p!s}"
    else:
        raise ValueError("Exactly one of bbox, place, or osm_pbf must be provided.")

    return hashlib.sha256(raw.encode()).hexdigest()[:16]


def download_region(
    bbox: Tuple[float, float, float, float],
    output_xml: str = "map.osm",
    output_pbf: str = "map.osm.pbf",
) -> Tuple[str, str]:
    """Download OSM XML for *bbox* via Overpass and convert to PBF.

    Args:
        bbox: ``(north, south, east, west)`` bounding box.
        output_xml: Destination path for the raw OSM XML file.
        output_pbf: Destination path for the converted PBF file.

    Returns:
        ``(xml_path, pbf_path)`` as resolved strings.
    """
    north, south, east, west = bbox

    # Overpass API uses (south, west, north, east) order for bounding boxes.
    # We query for 'highway' to filter routing networks and use default ID sorting
    # (avoiding 'qt' geographical sorting) because Valhalla requires strictly sorted input!
    query = f"""
    [out:xml][timeout:180];
    (
      way["highway"]({south},{west},{north},{east});
    );
    (._;>;);
    out body;
    """

    xml_path = Path(output_xml).resolve()
    pbf_path = Path(output_pbf).resolve()

    xml_path.parent.mkdir(parents=True, exist_ok=True)

    logger.info("Downloading exact OSM region to %s ...", xml_path)
    response = requests.post(
        "https://overpass-api.de/api/interpreter", data={"data": query}
    )
    response.raise_for_status()

    with open(xml_path, "wb") as f:
        f.write(response.content)

    logger.info("Download complete: %.2f MB", xml_path.stat().st_size / 1024 / 1024)

    try:
        import osmium
    except ImportError as e:
        raise ImportError(
            "The 'osmium' package is required to convert OSM XML to PBF for Valhalla. "
            "Install it via: pip install walkingpandas[mapmatch]"
        ) from e

    logger.info("Converting OSM XML to PBF format at %s ...", pbf_path)

    with osmium.SimpleWriter(str(pbf_path), overwrite=True) as writer:
        for o in osmium.FileProcessor(str(xml_path)):
            writer.add(o)

    logger.info("Conversion complete.")
    return str(xml_path), str(pbf_path)
