"""
HTTP client with retry logic, timeout handling, and error parsing.

Provides both synchronous (HttpClient) and asynchronous (AsyncHttpClient)
implementations built on httpx.
"""

from __future__ import annotations

import time
from typing import Any
from urllib.parse import urlencode

import httpx

from .errors import ARPError, TimeoutError as ARPTimeoutError, from_response


class HttpClient:
    """Synchronous HTTP client wrapping httpx with retry, timeout, and error handling."""

    def __init__(
        self,
        base_url: str,
        headers: dict[str, str],
        timeout: float = 30.0,
        max_retries: int = 3,
    ) -> None:
        self._base_url = base_url.rstrip("/")
        self._headers = dict(headers)
        self._timeout = timeout
        self._max_retries = max_retries
        self._client = httpx.Client(
            base_url=self._base_url,
            headers=self._headers,
            timeout=httpx.Timeout(timeout),
        )

    def close(self) -> None:
        """Close the underlying httpx client."""
        self._client.close()

    def __enter__(self) -> HttpClient:
        return self

    def __exit__(self, *args: Any) -> None:
        self.close()

    def get(
        self,
        path: str,
        params: dict[str, Any] | None = None,
    ) -> Any:
        """Perform a GET request."""
        return self._request("GET", path, params=params)

    def post(
        self,
        path: str,
        body: dict[str, Any] | None = None,
    ) -> Any:
        """Perform a POST request."""
        return self._request("POST", path, json_body=body)

    def patch(
        self,
        path: str,
        body: dict[str, Any] | None = None,
    ) -> Any:
        """Perform a PATCH request."""
        return self._request("PATCH", path, json_body=body)

    def delete(self, path: str) -> None:
        """Perform a DELETE request."""
        self._request("DELETE", path)

    def _request(
        self,
        method: str,
        path: str,
        params: dict[str, Any] | None = None,
        json_body: dict[str, Any] | None = None,
    ) -> Any:
        """Core request method with retry logic."""
        clean_params = _strip_none(params) if params else None
        last_error: Exception | None = None

        for attempt in range(self._max_retries + 1):
            try:
                response = self._client.request(
                    method,
                    path,
                    params=clean_params,
                    json=json_body,
                )

                if response.status_code >= 400:
                    body = _try_parse_response(response)
                    error = from_response(response.status_code, body)

                    if _is_retryable_status(response.status_code) and attempt < self._max_retries:
                        last_error = error
                        retry_delay = _get_retry_delay(attempt, response)
                        time.sleep(retry_delay)
                        continue

                    raise error

                if response.status_code == 204:
                    return None

                return response.json()

            except ARPError:
                raise
            except httpx.TimeoutException:
                err = ARPTimeoutError(f"Request to {path} timed out after {self._timeout}s")
                if attempt >= self._max_retries:
                    raise err from None
                last_error = err
            except httpx.HTTPError as exc:
                err_net = ARPError("NETWORK_ERROR", f"Network error: {exc}")
                if attempt >= self._max_retries:
                    raise err_net from exc
                last_error = err_net

            retry_delay = _get_retry_delay(attempt)
            time.sleep(retry_delay)

        if last_error is not None:
            raise last_error
        raise ARPError("UNKNOWN_ERROR", "Request failed after all retries")


class AsyncHttpClient:
    """Asynchronous HTTP client wrapping httpx.AsyncClient with retry, timeout, and error handling."""

    def __init__(
        self,
        base_url: str,
        headers: dict[str, str],
        timeout: float = 30.0,
        max_retries: int = 3,
    ) -> None:
        self._base_url = base_url.rstrip("/")
        self._headers = dict(headers)
        self._timeout = timeout
        self._max_retries = max_retries
        self._client = httpx.AsyncClient(
            base_url=self._base_url,
            headers=self._headers,
            timeout=httpx.Timeout(timeout),
        )

    async def close(self) -> None:
        """Close the underlying async httpx client."""
        await self._client.aclose()

    async def __aenter__(self) -> AsyncHttpClient:
        return self

    async def __aexit__(self, *args: Any) -> None:
        await self.close()

    async def get(
        self,
        path: str,
        params: dict[str, Any] | None = None,
    ) -> Any:
        """Perform an async GET request."""
        return await self._request("GET", path, params=params)

    async def post(
        self,
        path: str,
        body: dict[str, Any] | None = None,
    ) -> Any:
        """Perform an async POST request."""
        return await self._request("POST", path, json_body=body)

    async def patch(
        self,
        path: str,
        body: dict[str, Any] | None = None,
    ) -> Any:
        """Perform an async PATCH request."""
        return await self._request("PATCH", path, json_body=body)

    async def delete(self, path: str) -> None:
        """Perform an async DELETE request."""
        await self._request("DELETE", path)

    async def _request(
        self,
        method: str,
        path: str,
        params: dict[str, Any] | None = None,
        json_body: dict[str, Any] | None = None,
    ) -> Any:
        """Core async request method with retry logic."""
        import asyncio

        clean_params = _strip_none(params) if params else None
        last_error: Exception | None = None

        for attempt in range(self._max_retries + 1):
            try:
                response = await self._client.request(
                    method,
                    path,
                    params=clean_params,
                    json=json_body,
                )

                if response.status_code >= 400:
                    body = _try_parse_response(response)
                    error = from_response(response.status_code, body)

                    if _is_retryable_status(response.status_code) and attempt < self._max_retries:
                        last_error = error
                        retry_delay = _get_retry_delay(attempt, response)
                        await asyncio.sleep(retry_delay)
                        continue

                    raise error

                if response.status_code == 204:
                    return None

                return response.json()

            except ARPError:
                raise
            except httpx.TimeoutException:
                err = ARPTimeoutError(f"Request to {path} timed out after {self._timeout}s")
                if attempt >= self._max_retries:
                    raise err from None
                last_error = err
            except httpx.HTTPError as exc:
                err_net = ARPError("NETWORK_ERROR", f"Network error: {exc}")
                if attempt >= self._max_retries:
                    raise err_net from exc
                last_error = err_net

            retry_delay = _get_retry_delay(attempt)
            await asyncio.sleep(retry_delay)

        if last_error is not None:
            raise last_error
        raise ARPError("UNKNOWN_ERROR", "Request failed after all retries")


# ──────────────────────────────────────────────
# Shared helpers
# ──────────────────────────────────────────────


def _strip_none(params: dict[str, Any]) -> dict[str, Any]:
    """Remove keys with None values from a dict."""
    return {k: v for k, v in params.items() if v is not None}


def _try_parse_response(response: httpx.Response) -> dict[str, Any] | str | None:
    """Try to parse the response body as JSON, falling back to text."""
    content_type = response.headers.get("content-type", "")
    if "application/json" in content_type:
        try:
            result: dict[str, Any] | str | None = response.json()
            return result
        except Exception:
            return response.text
    text = response.text
    if text:
        try:
            import json
            parsed: dict[str, Any] | str | None = json.loads(text)
            return parsed
        except (ValueError, json.JSONDecodeError):
            return text
    return None


def _is_retryable_status(status: int) -> bool:
    """Determine if a status code is retryable."""
    return status == 429 or status >= 500


def _get_retry_delay(
    attempt: int,
    response: httpx.Response | None = None,
) -> float:
    """
    Calculate the retry delay using exponential backoff.
    Base delays: attempt 0 = 1s, attempt 1 = 2s, attempt 2 = 4s.
    Respects Retry-After header when present.
    """
    if response is not None:
        retry_after = response.headers.get("retry-after")
        if retry_after is not None:
            try:
                return float(retry_after)
            except ValueError:
                pass

    return float(2 ** attempt)
