"""
Georavity Python SDK — HTTP client for the Georavity Geospatial API.

Usage::

    from georavity import Georavity

    geo = Georavity("gr_your_api_key")
    route = geo.route(
        locations=[
            {"lon": 13.388860, "lat": 52.517037},
            {"lon": 13.397634, "lat": 52.529407},
        ],
        costing="auto",
    )
    print(f"Distance: {route['trip']['summary']['length']} km")
"""

from __future__ import annotations

from typing import Any, Dict, List, Optional
from urllib.parse import urlencode

import requests

__all__ = ["Georavity", "GeoravityError"]

SDK_VERSION = "1.0.0"
DEFAULT_BASE_URL = "https://api.georavity.com"
DEFAULT_TIMEOUT = 30


class GeoravityError(Exception):
    """Raised when the Georavity API returns an error response."""

    def __init__(self, status: int, code: str, message: str, details: Optional[str] = None):
        super().__init__(message)
        self.status = status
        self.code = code
        self.details = details

    def __repr__(self) -> str:
        return f"GeoravityError(status={self.status}, code='{self.code}', message='{self}')"


class Georavity:
    """
    Client for the Georavity Geospatial API.

    Args:
        api_key: Your Georavity API key (starts with ``gr_``).
        base_url: API base URL. Defaults to ``https://api.georavity.com``.
        timeout: Request timeout in seconds. Defaults to 30.

    Example::

        geo = Georavity("gr_your_api_key")

        # Route
        route = geo.route(
            locations=[{"lat": 52.52, "lon": 13.40}, {"lat": 48.85, "lon": 2.35}],
            costing="auto",
        )

        # Geocode
        results = geo.geocode("Brandenburg Gate, Berlin")

        # Reverse geocode
        place = geo.reverse(lat=52.5163, lon=13.3777)
    """

    def __init__(
        self,
        api_key: str,
        base_url: str = DEFAULT_BASE_URL,
        timeout: int = DEFAULT_TIMEOUT,
    ):
        if not api_key or not api_key.startswith("gr_"):
            raise ValueError('Invalid API key. Georavity keys start with "gr_".')

        self._api_key = api_key
        self._base_url = base_url.rstrip("/")
        self._timeout = timeout
        self._session = requests.Session()
        self._session.headers.update(
            {
                "X-API-Key": self._api_key,
                "User-Agent": f"georavity-python/{SDK_VERSION}",
            }
        )

    # ── Routing ──────────────────────────────────────────────────────────

    def route(
        self,
        locations: List[Dict[str, float]],
        costing: str = "auto",
        units: str = "kilometers",
        language: str = "en",
        costing_options: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """
        Calculate a route between two or more locations.

        Args:
            locations: List of ``{"lat": float, "lon": float}`` dicts.
            costing: Routing profile — ``"auto"``, ``"bicycle"``, ``"pedestrian"``, ``"truck"``.
            units: ``"kilometers"`` or ``"miles"``.
            language: Language code for instructions (e.g. ``"en"``, ``"de"``, ``"ar"``).
            costing_options: Advanced costing parameters.

        Returns:
            Route response dict with ``trip.legs``, ``trip.summary``, etc.
        """
        body: Dict[str, Any] = {
            "locations": locations,
            "costing": costing,
            "directions_options": {"units": units, "language": language},
        }
        if costing_options:
            body["costing_options"] = costing_options
        return self._post("/v1/route", body)

    def optimized_route(
        self,
        locations: List[Dict[str, float]],
        costing: str = "auto",
        units: str = "kilometers",
        language: str = "en",
    ) -> Dict[str, Any]:
        """
        Calculate an optimized route (TSP) visiting all locations in optimal order.
        """
        return self._post(
            "/v1/optimized_route",
            {
                "locations": locations,
                "costing": costing,
                "directions_options": {"units": units, "language": language},
            },
        )

    def matrix(
        self,
        sources: List[Dict[str, float]],
        targets: List[Dict[str, float]],
        costing: str = "auto",
    ) -> Dict[str, Any]:
        """Calculate a distance/time matrix between sources and targets."""
        return self._post(
            "/v1/sources_to_targets",
            {"sources": sources, "targets": targets, "costing": costing},
        )

    def isochrone(
        self,
        locations: List[Dict[str, float]],
        costing: str = "auto",
        contours: Optional[List[Dict[str, float]]] = None,
        polygons: bool = True,
    ) -> Dict[str, Any]:
        """
        Calculate isochrone/isodistance polygons.

        Args:
            locations: Origin point(s).
            costing: Routing profile.
            contours: List of ``{"time": minutes}`` or ``{"distance": km}`` dicts.
            polygons: If True, return polygons. If False, return linestrings.
        """
        if contours is None:
            contours = [{"time": 15}]
        return self._post(
            "/v1/isochrone",
            {
                "locations": locations,
                "costing": costing,
                "contours": contours,
                "polygons": polygons,
            },
        )

    # ── Geocoding ────────────────────────────────────────────────────────

    def geocode(
        self,
        text: str,
        size: int = 10,
        lang: Optional[str] = None,
        country: Optional[str] = None,
        focus_lat: Optional[float] = None,
        focus_lon: Optional[float] = None,
    ) -> Dict[str, Any]:
        """
        Forward geocode — search for places by name or address.

        Returns:
            GeoJSON FeatureCollection with matching places.
        """
        params: Dict[str, Any] = {"text": text, "size": size}
        if lang:
            params["lang"] = lang
        if country:
            params["boundary.country"] = country
        if focus_lat is not None and focus_lon is not None:
            params["focus.point.lat"] = focus_lat
            params["focus.point.lon"] = focus_lon
        return self._get("/v1/geocode", params)

    def reverse(
        self,
        lat: float,
        lon: float,
        size: int = 1,
        lang: Optional[str] = None,
    ) -> Dict[str, Any]:
        """Reverse geocode — find the nearest address for coordinates."""
        params: Dict[str, Any] = {"point.lat": lat, "point.lon": lon, "size": size}
        if lang:
            params["lang"] = lang
        return self._get("/v1/reverse", params)

    def autocomplete(
        self,
        text: str,
        size: int = 5,
        focus_lat: Optional[float] = None,
        focus_lon: Optional[float] = None,
    ) -> Dict[str, Any]:
        """Autocomplete — real-time search suggestions as the user types."""
        params: Dict[str, Any] = {"text": text, "size": size}
        if focus_lat is not None and focus_lon is not None:
            params["focus.point.lat"] = focus_lat
            params["focus.point.lon"] = focus_lon
        return self._get("/v1/autocomplete", params)

    # ── System ───────────────────────────────────────────────────────────

    def health(self) -> Dict[str, Any]:
        """Check API health and uptime."""
        return self._get("/health")

    # ── Internal ─────────────────────────────────────────────────────────

    def _get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
        url = f"{self._base_url}{path}"
        if params:
            url += "?" + urlencode(params)

        resp = self._session.get(url, timeout=self._timeout)
        return self._handle_response(resp)

    def _post(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]:
        resp = self._session.post(
            f"{self._base_url}{path}",
            json=body,
            timeout=self._timeout,
        )
        return self._handle_response(resp)

    def _handle_response(self, resp: requests.Response) -> Dict[str, Any]:
        if not resp.ok:
            try:
                data = resp.json()
            except ValueError:
                data = {}

            raise GeoravityError(
                status=resp.status_code,
                code=data.get("code", f"HTTP_{resp.status_code}"),
                message=data.get("error", f"Request failed with status {resp.status_code}"),
                details=data.get("details"),
            )

        try:
            return resp.json()
        except ValueError:
            raise GeoravityError(500, "PARSE_ERROR", "Failed to parse API response")
