"""Internal utilities for walkingpandas."""

import hashlib
import os
from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional, Tuple, Union


@dataclass(frozen=True)
class BoundingBox:
    """Immutable bounding box with named fields.

    Stores coordinates as *north*, *south*, *east*, *west* and provides
    convenience accessors for the different ordering conventions used
    across the codebase and external tools.

    Examples:
        >>> bb = BoundingBox.from_nsew(47.4, 47.3, 8.6, 8.5)
        >>> bb.contains(lon=8.55, lat=47.35)
        True
        >>> bb = BoundingBox.from_min_max(min_lon=8.5, min_lat=47.3, max_lon=8.6, max_lat=47.4)
        >>> bb.as_osmium()
        '8.5,47.3,8.6,47.4'
    """

    north: float
    south: float
    east: float
    west: float

    @classmethod
    def from_nsew(
        cls, north: float, south: float, east: float, west: float
    ) -> "BoundingBox":
        """Create from ``(north, south, east, west)`` convention.

        This matches :meth:`GPSDataset.estimate_bbox` and ``from_osmnx``.
        """
        return cls(north=north, south=south, east=east, west=west)

    @classmethod
    def from_min_max(
        cls,
        min_lon: float,
        min_lat: float,
        max_lon: float,
        max_lat: float,
    ) -> "BoundingBox":
        """Create from ``(min_lon, min_lat, max_lon, max_lat)`` convention.

        This matches the Valhalla matcher and the osmium CLI.
        """
        return cls(north=max_lat, south=min_lat, east=max_lon, west=min_lon)

    def as_nsew(self) -> Tuple[float, float, float, float]:
        """Return ``(north, south, east, west)``."""
        return (self.north, self.south, self.east, self.west)

    def as_osmium(self) -> str:
        """Return ``west,south,east,north`` string for the osmium CLI ``-b`` flag."""
        return f"{self.west},{self.south},{self.east},{self.north}"

    def as_overpass(self) -> str:
        """Return ``south,west,north,east`` string for Overpass QL."""
        return f"{self.south},{self.west},{self.north},{self.east}"

    def contains(self, lon: float, lat: float) -> bool:
        """Test whether a point lies within (or on the boundary of) this box."""
        return self.west <= lon <= self.east and self.south <= lat <= self.north


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 _escape_sql_string(s: str) -> str:
    """Escape a string for safe use inside SQL single-quoted literals."""
    return s.replace("'", "''")


def _format_id_list_sql(values: List[Union[int, str]]) -> str:
    """Format a list of IDs for a SQL IN clause.

    Integer values are rendered bare (``1, 2, 3``); string values are
    single-quoted with escaping.
    """
    if not values:
        return ""
    if all(isinstance(v, (int, float)) and not isinstance(v, bool) for v in values):
        return ", ".join(str(int(v)) for v in values)
    return ", ".join(f"'{_escape_sql_string(str(v))}'" for v in values)
