"""Client for the AccuWeather Locations API.

Every weather endpoint needs an AccuWeather *location key*. This module
resolves human-friendly inputs - a city name, a point of interest, a
lat/lon pair, a postal code, or an IP address - into such a key.

Example:
    >>> from accuweather_client.clients import LocationClient
    >>> locations = LocationClient(token=API_KEY)
    >>> sydney = locations.search_city("sydney").first
    >>> sydney.Key
    '22889'
    >>> locations.search_city("sydney", country="canada").first
    Sydney, Canada (key=...)
"""

import warnings
from typing import Any

from accuweather_client._http import (
    DEFAULT_RETRIES,
    DEFAULT_TIMEOUT,
    Transport,
)
from accuweather_client.exceptions import LocationNotFoundError
from accuweather_client.models import LocationModel, LocationModelItem


class LocationClient:
    """Resolves cities, POIs, coordinates, postal codes and IPs to locations.

    Args:
        token: A valid AccuWeather API key.
        language: BCP-47 language code for localized location names.
        timeout: Per-request timeout in seconds.
        retries: Automatic retries for transient failures.

    Attributes:
        transport: The underlying HTTP transport (shared with
            :class:`~accuweather_client.clients.WeatherClient` when created
            through it, so both use one connection pool).
    """

    def __init__(
        self,
        token: str,
        language: str | None = None,
        timeout: float = DEFAULT_TIMEOUT,
        retries: int = DEFAULT_RETRIES,
        transport: Transport | None = None,
    ):
        self.transport = transport or Transport(
            token, timeout=timeout, retries=retries
        )
        self.language = language

    # -- internal helpers ------------------------------------------------

    def _get(self, endpoint: str, **extra: Any) -> Any:
        """Perform a Locations API request with common parameters."""
        return self.transport.get(
            endpoint,
            language=self.language,
            extra=extra or None,
        )

    @staticmethod
    def _wrap(payload: Any, query: str) -> LocationModel:
        """Wrap a raw payload in a LocationModel, or raise if empty.

        Raises:
            LocationNotFoundError: If the API found no matching location.
        """
        if payload is None or payload == [] or payload == {}:
            raise LocationNotFoundError(
                f"No AccuWeather location matched {query!r}. Try a "
                "different spelling, or add a country name."
            )
        return LocationModel(response=payload)

    # -- public search methods -------------------------------------------

    def search_city(
        self, city: str, country: str | None = None
    ) -> LocationModel:
        """Find locations by city name, optionally narrowed by country.

        Args:
            city: The city name, e.g. ``"sydney"``.
            country: Optional country name to disambiguate cities that
                exist in several countries.

        Returns:
            A :class:`LocationModel`; ``.first`` is the best-ranked match.

        Raises:
            LocationNotFoundError: If no location matched.
        """
        if country:
            payload = self._get("locations/v1/search", q=f"{city} {country}")
        else:
            payload = self._get("locations/v1/cities/search", q=city)
        return self._wrap(
            payload, f"{city}{', ' + country if country else ''}"
        )

    def search_poi(self, poi: str) -> LocationModel:
        """Find locations by point of interest, e.g. ``"Eiffel tower"``.

        Raises:
            LocationNotFoundError: If no location matched.
        """
        payload = self._get("locations/v1/poi/search", q=poi)
        return self._wrap(payload, poi)

    def search_geoposition(self, lat: float, lon: float) -> LocationModel:
        """Find the location at a latitude/longitude pair.

        Args:
            lat: Latitude in decimal degrees (-90 to 90).
            lon: Longitude in decimal degrees (-180 to 180).

        Raises:
            ValueError: If the coordinates are out of range.
            LocationNotFoundError: If no location matched.
        """
        if not -90 <= lat <= 90:
            raise ValueError("Latitude must be between -90 and 90.")
        if not -180 <= lon <= 180:
            raise ValueError("Longitude must be between -180 and 180.")
        payload = self._get(
            "locations/v1/cities/geoposition/search", q=f"{lat},{lon}"
        )
        return self._wrap(payload, f"({lat}, {lon})")

    def search_postal_code(
        self, code: str, country: str | None = None
    ) -> LocationModel:
        """Find locations by postal code.

        Args:
            code: The postal code, e.g. ``"1012"`` or ``"90210"``.
            country: Optional ISO country code to scope the search, e.g.
                ``"NL"``.

        Raises:
            LocationNotFoundError: If no location matched.
        """
        endpoint = (
            f"locations/v1/postalcodes/{country}/search"
            if country
            else "locations/v1/postalcodes/search"
        )
        payload = self._get(endpoint, q=code)
        return self._wrap(payload, code)

    def search_ip(self, ip: str | None = None) -> LocationModel:
        """Find the location of an IP address.

        Args:
            ip: The IP address to look up. When omitted, the API uses the
                caller's own IP address.

        Raises:
            LocationNotFoundError: If no location matched.
        """
        extra = {"q": ip} if ip else {}
        payload = self._get("locations/v1/cities/ipaddress", **extra)
        return self._wrap(payload, ip or "your IP address")

    def autocomplete(self, query: str) -> list[LocationModelItem]:
        """Return autocomplete suggestions for a partial city name.

        Args:
            query: The partial name, e.g. ``"syd"``.

        Returns:
            A (possibly empty) list of matching locations.
        """
        payload = self.transport.get(
            "locations/v1/cities/autocomplete",
            details=False,
            language=self.language,
            extra={"q": query},
        )
        return [LocationModelItem(**item) for item in payload or []]

    def resolve(
        self,
        city: str | None = None,
        country: str | None = None,
        poi: str | None = None,
        lat: float | None = None,
        lon: float | None = None,
    ) -> LocationModel:
        """Resolve whichever location input was given to a location.

        Exactly one of ``city``, ``poi`` or the ``lat``/``lon`` pair must
        be provided.

        Raises:
            ValueError: If the combination of arguments is invalid.
            LocationNotFoundError: If no location matched.
        """
        if city and poi:
            raise ValueError(
                'Only one of "city" or "poi" can be provided, not both.'
            )
        if city:
            return self.search_city(city, country=country)
        if poi:
            return self.search_poi(poi)
        if lat is not None and lon is not None:
            return self.search_geoposition(lat, lon)
        if lat is not None or lon is not None:
            raise ValueError('You must provide both "lat" and "lon".')
        raise ValueError(
            'You must provide either a "city", a "poi" or a '
            '"lat lon" combination.'
        )


#: Deprecated alias kept for v1 imports; use :class:`LocationClient`.
LocationBaseClient = LocationClient


class _ResolvedLocation:
    """Return type of the deprecated :func:`get_location_model` factory."""

    def __init__(self, location: LocationModel):
        self.location = location


def get_location_model(
    city: str | None = None,
    poi: str | None = None,
    lat: float | None = None,
    lon: float | None = None,
    country: str | None = None,
    token: str = "",
    **kwargs: Any,
) -> _ResolvedLocation:
    """Resolve a location the v1 way. Deprecated.

    .. deprecated:: 2.0
        Use :class:`LocationClient` and its ``resolve``/``search_*``
        methods instead. This wrapper only exists so v1 code keeps
        working; it returns an object with a ``.location`` attribute
        holding the :class:`LocationModel`.
    """
    warnings.warn(
        "get_location_model() is deprecated; use LocationClient instead.",
        DeprecationWarning,
        stacklevel=2,
    )
    client = LocationClient(token=token)
    model = client.resolve(
        city=city, country=country, poi=poi, lat=lat, lon=lon
    )
    return _ResolvedLocation(model)
