"""Internal HTTP transport for the AccuWeather API.

This module centralizes everything HTTP: the base URL, authentication
parameters, timeouts, automatic retries with backoff, the ``User-Agent``
header, and the mapping from HTTP status codes to the typed exceptions in
:mod:`accuweather_client.exceptions`.

Both the synchronous client (``requests``) and the asynchronous client
(``httpx``) build on the helpers here so URL construction and error
handling have a single source of truth.
"""

import re
from collections.abc import Mapping
from typing import Any

from requests import Session
from requests.adapters import HTTPAdapter
from requests.exceptions import RequestException
from urllib3.util.retry import Retry

from accuweather_client.exceptions import (
    ApiRequestError,
    ApiServerError,
    InvalidTokenError,
    RateLimitExceededError,
)

BASE_URL = "https://dataservice.accuweather.com/"
DEFAULT_TIMEOUT = 10.0
DEFAULT_RETRIES = 3

#: AccuWeather API keys: classic keys are 32 alphanumeric characters, but
#: keys issued by the newer developer portal are longer and may contain
#: ``_``, ``-`` or percent-encoded characters. Validate the shape loosely;
#: the API itself is the final authority (HTTP 401 -> InvalidTokenError).
VALID_TOKEN_REGEX = re.compile(r"^[A-Za-z0-9%_-]{20,64}$")


def validate_token(token: str) -> str:
    """Check that ``token`` looks like a valid AccuWeather API key.

    Args:
        token: The API key to validate.

    Returns:
        The token, unchanged, when it matches the expected format.

    Raises:
        InvalidTokenError: If the token does not look like an AccuWeather
            API key. The token itself is never included in the message.
    """
    if not isinstance(token, str) or not VALID_TOKEN_REGEX.match(token):
        raise InvalidTokenError(
            "The provided API token is not valid. AccuWeather tokens are "
            "20-64 characters (letters, digits, '_', '-' or '%') - create "
            "one at https://developer.accuweather.com."
        )
    return token


def build_params(
    token: str,
    *,
    details: bool = True,
    metric: bool | None = None,
    language: str | None = None,
    extra: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
    """Build the query-parameter dictionary for an API request.

    Args:
        token: The AccuWeather API key.
        details: Whether to request the detailed response payload.
        metric: When not ``None``, request metric (``True``) or imperial
            (``False``) units. Only some endpoints accept this.
        language: BCP-47 language code such as ``"en-us"`` or ``"nl-nl"``.
        extra: Additional endpoint-specific query parameters.

    Returns:
        A dict suitable for the ``params`` argument of a request.
    """
    params: dict[str, Any] = {"apikey": token, "details": str(details).lower()}
    if metric is not None:
        params["metric"] = str(metric).lower()
    if language is not None:
        params["language"] = language
    if extra:
        params.update(extra)
    return params


def raise_for_response(
    status_code: int, body: str, headers: Mapping[str, str], url: str
) -> None:
    """Translate an HTTP error response into a typed exception.

    Args:
        status_code: HTTP status code of the response.
        body: Response body text (used to detect quota messages).
        headers: Response headers (``RateLimit-*`` headers are attached to
            rate-limit errors).
        url: The requested URL, included in error messages for debugging.

    Raises:
        InvalidTokenError: On HTTP 401.
        RateLimitExceededError: On HTTP 429, or a 503 whose body mentions
            the exhausted quota (AccuWeather signals quota exhaustion both
            ways).
        ApiRequestError: On any other 4xx.
        ApiServerError: On any 5xx.
    """
    if status_code < 400:
        return
    rate_headers = {
        k: v for k, v in headers.items() if k.lower().startswith("ratelimit")
    }
    if status_code == 401:
        raise InvalidTokenError(
            "The AccuWeather API rejected the token (HTTP 401). Check that "
            "the key is active and belongs to the right app."
        )
    if status_code == 429 or (
        status_code == 503 and "exceeded" in body.lower()
    ):
        raise RateLimitExceededError(
            "The request quota for this API key is exhausted "
            f"(HTTP {status_code}). The free tier allows 50 calls/day.",
            status_code=status_code,
            rate_limit_headers=rate_headers,
        )
    if status_code == 403:
        raise ApiRequestError(
            f"The AccuWeather API refused the request to {url} (HTTP 403). "
            "This usually means the endpoint is not included in your "
            "AccuWeather plan (e.g. alerts, indices, historical data and "
            "10/15-day forecasts require a paid tier).",
            status_code=status_code,
        )
    if status_code < 500:
        raise ApiRequestError(
            f"The AccuWeather API rejected the request to {url} "
            f"(HTTP {status_code}): {body[:200]}",
            status_code=status_code,
        )
    raise ApiServerError(
        f"The AccuWeather API returned a server error for {url} "
        f"(HTTP {status_code}).",
        status_code=status_code,
    )


class Transport:
    """Synchronous HTTP transport built on a pooled ``requests.Session``.

    Args:
        token: A valid AccuWeather API key.
        base_url: Root URL of the API; override for testing/proxying.
        timeout: Per-request timeout in seconds.
        retries: Number of automatic retries (with exponential backoff)
            for connection errors and HTTP 500/502/504 responses. HTTP 503
            is deliberately not retried so quota errors surface instantly.
    """

    def __init__(
        self,
        token: str,
        base_url: str = BASE_URL,
        timeout: float = DEFAULT_TIMEOUT,
        retries: int = DEFAULT_RETRIES,
    ):
        self.token = validate_token(token)
        self.base_url = base_url if base_url.endswith("/") else base_url + "/"
        self.timeout = timeout
        self.session = Session()
        self.session.headers["User-Agent"] = _user_agent()
        retry = Retry(
            total=retries,
            backoff_factor=0.5,
            status_forcelist=(500, 502, 504),
            allowed_methods=("GET",),
        )
        adapter = HTTPAdapter(max_retries=retry)
        self.session.mount("https://", adapter)
        self.session.mount("http://", adapter)

    def get(
        self,
        endpoint: str,
        *,
        details: bool = True,
        metric: bool | None = None,
        language: str | None = None,
        extra: Mapping[str, Any] | None = None,
    ) -> Any:
        """Perform a GET request against the API and return parsed JSON.

        Args:
            endpoint: Path relative to the base URL, e.g.
                ``"forecasts/v1/daily/5day/12345"``.
            details: Whether to request detailed payloads.
            metric: Metric (``True``) or imperial (``False``) units.
            language: BCP-47 language code for localized text.
            extra: Additional endpoint-specific query parameters.

        Returns:
            The decoded JSON response (dict or list).

        Raises:
            ApiRequestError: If the request could not be sent at all, or
                the API answered with a 4xx status.
            ApiServerError: If the API answered with a 5xx status.
            InvalidTokenError: If the API answered with 401.
            RateLimitExceededError: If the daily quota is exhausted.
        """
        url = self.base_url + endpoint.lstrip("/")
        params = build_params(
            self.token,
            details=details,
            metric=metric,
            language=language,
            extra=extra,
        )
        try:
            response = self.session.get(
                url, params=params, timeout=self.timeout
            )
        except RequestException as exc:
            raise ApiRequestError(
                f"Failed to reach the AccuWeather API at {url}: {exc}"
            ) from exc
        raise_for_response(
            response.status_code, response.text, response.headers, url
        )
        return response.json()


def _user_agent() -> str:
    """Return the ``User-Agent`` string identifying this client."""
    from accuweather_client import __version__

    return f"accuweather-client/{__version__}"
