from __future__ import annotations

import time
from typing import Any, Dict, Optional, Type, TypeVar, Union

import httpx
from pydantic import BaseModel

from cardog._constants import DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT
from cardog._exceptions import (
    APIError,
    AuthenticationError,
    ConnectionError,
    NotFoundError,
    PermissionError,
    RateLimitError,
    ServerError,
    TimeoutError,
)

T = TypeVar("T", bound=BaseModel)

_RETRYABLE_STATUS_CODES = {408, 429, 500, 502, 503, 504}


def _build_headers(api_key: str) -> dict[str, str]:
    return {
        "x-api-key": api_key,
        "Content-Type": "application/json",
        "User-Agent": "cardog-python/0.1.0",
    }


def _raise_for_status(response: httpx.Response) -> None:
    if response.is_success:
        return

    status = response.status_code
    try:
        body = response.json()
    except Exception:
        body = {"message": response.text}

    message = body.get("message") or body.get("error") or response.reason_phrase or "Unknown error"
    code = body.get("code")

    if status == 401:
        raise AuthenticationError(message, status, code, body)
    elif status == 403:
        raise PermissionError(message, status, code, body)
    elif status == 404:
        raise NotFoundError(message, status, code, body)
    elif status == 429:
        retry_after = response.headers.get("retry-after")
        raise RateLimitError(
            message,
            status,
            code,
            body,
            retry_after=float(retry_after) if retry_after else None,
        )
    elif status >= 500:
        raise ServerError(message, status, code, body)
    else:
        raise APIError(message, status, code, body)


def _parse_response(response: httpx.Response, model: Optional[Type[T]] = None) -> Any:
    _raise_for_status(response)
    data = response.json()
    if model is not None:
        return model.model_validate(data)
    return data


class SyncAPIClient:
    """Synchronous HTTP client for the Cardog API."""

    def __init__(
        self,
        api_key: str,
        base_url: str = DEFAULT_BASE_URL,
        timeout: float = DEFAULT_TIMEOUT,
        max_retries: int = DEFAULT_MAX_RETRIES,
    ) -> None:
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.max_retries = max_retries
        self._client = httpx.Client(
            base_url=self.base_url,
            headers=_build_headers(api_key),
            timeout=timeout,
        )

    def _request(
        self,
        method: str,
        path: str,
        *,
        params: Optional[Dict[str, Any]] = None,
        json: Optional[Any] = None,
        model: Optional[Type[T]] = None,
    ) -> Any:
        # Strip None values from params
        if params:
            params = {k: v for k, v in params.items() if v is not None}

        last_exc: Optional[Exception] = None
        for attempt in range(self.max_retries + 1):
            try:
                response = self._client.request(method, path, params=params, json=json)
                if response.status_code in _RETRYABLE_STATUS_CODES and attempt < self.max_retries:
                    delay = min(2**attempt * 0.5, 8.0)
                    if response.status_code == 429:
                        retry_after = response.headers.get("retry-after")
                        if retry_after:
                            delay = float(retry_after)
                    time.sleep(delay)
                    continue
                return _parse_response(response, model)
            except httpx.TimeoutException as e:
                last_exc = TimeoutError(str(e))
                if attempt < self.max_retries:
                    time.sleep(min(2**attempt * 0.5, 8.0))
                    continue
                raise last_exc from e
            except httpx.ConnectError as e:
                last_exc = ConnectionError(str(e))
                if attempt < self.max_retries:
                    time.sleep(min(2**attempt * 0.5, 8.0))
                    continue
                raise last_exc from e

        # Should not reach here, but just in case
        if last_exc:
            raise last_exc
        raise CardogError("Request failed after retries")  # type: ignore[name-defined]

    def _get(
        self,
        path: str,
        *,
        params: Optional[Dict[str, Any]] = None,
        model: Optional[Type[T]] = None,
    ) -> Any:
        return self._request("GET", path, params=params, model=model)

    def _post(
        self,
        path: str,
        *,
        json: Optional[Any] = None,
        params: Optional[Dict[str, Any]] = None,
        model: Optional[Type[T]] = None,
    ) -> Any:
        return self._request("POST", path, json=json, params=params, model=model)

    def close(self) -> None:
        self._client.close()

    def __enter__(self) -> SyncAPIClient:
        return self

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


class AsyncAPIClient:
    """Asynchronous HTTP client for the Cardog API."""

    def __init__(
        self,
        api_key: str,
        base_url: str = DEFAULT_BASE_URL,
        timeout: float = DEFAULT_TIMEOUT,
        max_retries: int = DEFAULT_MAX_RETRIES,
    ) -> None:
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.max_retries = max_retries
        self._client = httpx.AsyncClient(
            base_url=self.base_url,
            headers=_build_headers(api_key),
            timeout=timeout,
        )

    async def _request(
        self,
        method: str,
        path: str,
        *,
        params: Optional[Dict[str, Any]] = None,
        json: Optional[Any] = None,
        model: Optional[Type[T]] = None,
    ) -> Any:
        import asyncio

        if params:
            params = {k: v for k, v in params.items() if v is not None}

        last_exc: Optional[Exception] = None
        for attempt in range(self.max_retries + 1):
            try:
                response = await self._client.request(method, path, params=params, json=json)
                if response.status_code in _RETRYABLE_STATUS_CODES and attempt < self.max_retries:
                    delay = min(2**attempt * 0.5, 8.0)
                    if response.status_code == 429:
                        retry_after = response.headers.get("retry-after")
                        if retry_after:
                            delay = float(retry_after)
                    await asyncio.sleep(delay)
                    continue
                return _parse_response(response, model)
            except httpx.TimeoutException as e:
                last_exc = TimeoutError(str(e))
                if attempt < self.max_retries:
                    await asyncio.sleep(min(2**attempt * 0.5, 8.0))
                    continue
                raise last_exc from e
            except httpx.ConnectError as e:
                last_exc = ConnectionError(str(e))
                if attempt < self.max_retries:
                    await asyncio.sleep(min(2**attempt * 0.5, 8.0))
                    continue
                raise last_exc from e

        if last_exc:
            raise last_exc
        raise CardogError("Request failed after retries")  # type: ignore[name-defined]

    async def _get(
        self,
        path: str,
        *,
        params: Optional[Dict[str, Any]] = None,
        model: Optional[Type[T]] = None,
    ) -> Any:
        return await self._request("GET", path, params=params, model=model)

    async def _post(
        self,
        path: str,
        *,
        json: Optional[Any] = None,
        params: Optional[Dict[str, Any]] = None,
        model: Optional[Type[T]] = None,
    ) -> Any:
        return await self._request("POST", path, json=json, params=params, model=model)

    async def close(self) -> None:
        await self._client.aclose()

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

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