"""Async Nominatim geocoding with bbox utilities."""

import logging

import httpx

from mcgibs.constants import NOMINATIM_BASE, USER_AGENT
from mcgibs.models import BBox, GeocodingResult

log = logging.getLogger(__name__)


async def geocode(
    client: httpx.AsyncClient,
    query: str,
    rate_limiter=None,
) -> GeocodingResult | None:
    """Geocode a place name via Nominatim.

    Caching is the caller's responsibility (GIBSClient.resolve_place manages
    an LRU cache). This function is a pure HTTP-call-and-parse layer.

    Args:
        client: Shared httpx async client.
        query: Free-form place name (e.g. "Tokyo", "Amazon River").
        rate_limiter: Object with async ``nominatim_rate_limit()`` method
            for enforcing Nominatim's 1 req/sec policy.  When called from
            GIBSClient, this is the client instance itself.

    Returns:
        GeocodingResult on success, None if no results found.
    """
    if rate_limiter is not None and hasattr(rate_limiter, "nominatim_rate_limit"):
        await rate_limiter.nominatim_rate_limit()

    params = {
        "q": query,
        "format": "json",
        "limit": "1",
        "bounded": "0",
    }
    headers = {"User-Agent": USER_AGENT}

    try:
        resp = await client.get(
            f"{NOMINATIM_BASE}/search",
            params=params,
            headers=headers,
        )
        resp.raise_for_status()
        data = resp.json()
    except httpx.HTTPError as exc:
        log.warning("Nominatim request failed for %r: %s", query, exc)
        return None

    if not data:
        log.debug("Nominatim returned no results for %r", query)
        return None

    hit = data[0]

    # Nominatim bbox order: [south_lat, north_lat, west_lon, east_lon]
    raw_bbox = hit.get("boundingbox", [])
    if len(raw_bbox) == 4:
        south, north, west, east = (float(v) for v in raw_bbox)
        bbox = BBox(west=west, south=south, east=east, north=north)
    else:
        # Fall back to a small bbox around the point.
        lat = float(hit["lat"])
        lon = float(hit["lon"])
        bbox = bbox_from_point(lat, lon, radius_deg=0.5)

    result = GeocodingResult(
        display_name=hit.get("display_name", query),
        bbox=bbox,
        lat=float(hit["lat"]),
        lon=float(hit["lon"]),
        osm_type=hit.get("osm_type", ""),
        importance=float(hit.get("importance", 0.0)),
    )

    log.debug("Geocoded %r -> %s", query, result.display_name)
    return result


def expand_bbox(bbox: BBox, factor: float = 0.1) -> BBox:
    """Expand a bounding box by a proportional factor in each direction.

    A factor of 0.1 adds 10% of the width/height as padding on each side.
    Results are clamped to valid geographic ranges.

    Args:
        bbox: Original bounding box.
        factor: Expansion ratio (0.1 = 10% padding per side).

    Returns:
        New BBox with expanded extents, clamped to [-180, 180] lon
        and [-90, 90] lat.
    """
    dlon = (bbox.east - bbox.west) * factor
    dlat = (bbox.north - bbox.south) * factor

    # Ensure a minimum expansion so zero-area boxes still grow.
    dlon = max(dlon, 0.001)
    dlat = max(dlat, 0.001)

    return BBox(
        west=max(-180.0, bbox.west - dlon),
        south=max(-90.0, bbox.south - dlat),
        east=min(180.0, bbox.east + dlon),
        north=min(90.0, bbox.north + dlat),
    )


def bbox_from_point(lat: float, lon: float, radius_deg: float = 0.5) -> BBox:
    """Create a bounding box centered on a geographic point.

    Args:
        lat: Latitude in decimal degrees.
        lon: Longitude in decimal degrees.
        radius_deg: Half-width/height of the box in degrees.

    Returns:
        BBox centered on (lat, lon), clamped to valid ranges.
    """
    return BBox(
        west=max(-180.0, lon - radius_deg),
        south=max(-90.0, lat - radius_deg),
        east=min(180.0, lon + radius_deg),
        north=min(90.0, lat + radius_deg),
    )
