from __future__ import annotations

from dataclasses import dataclass, field
from typing import (
    Any,
    Dict,
    Generic,
    List,
    Optional,
    TypeVar,
)

import httpx

from .._version import __version__
from ._auth import build_auth_headers, resolve_api_key
from ._errors import (
    ConnectError,
    RequestTimeoutError,
    create_api_error,
)
from ._retry import RetryConfig, async_with_retry, with_retry

T = TypeVar("T")

DEFAULT_BASE_URL = "https://api.gunspec.io"
DEFAULT_TIMEOUT = 30.0
SDK_USER_AGENT = f"gunspec-sdk/python/{__version__}"


# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------


@dataclass
class HttpClientConfig:
    """Configuration for the HTTP client."""

    base_url: str = DEFAULT_BASE_URL
    timeout: float = DEFAULT_TIMEOUT
    headers: Dict[str, str] = field(default_factory=dict)
    api_key: Optional[str] = None
    retry: RetryConfig = field(default_factory=RetryConfig)


@dataclass
class RateLimitInfo:
    """Parsed rate limit information from response headers."""

    limit: Optional[int] = None
    remaining: Optional[int] = None
    reset: Optional[int] = None


@dataclass
class PaginationMeta:
    """Pagination metadata from list responses."""

    page: int = 1
    limit: int = 25
    total: Optional[int] = None
    total_pages: Optional[int] = None


@dataclass
class APIResponse(Generic[T]):
    """Unwrapped API response for a single resource."""

    data: T
    status: int
    headers: Dict[str, str]
    request_id: str
    rate_limit: RateLimitInfo


@dataclass
class PaginatedResponse(Generic[T]):
    """Unwrapped API response for a paginated list."""

    data: List[T]
    pagination: PaginationMeta
    status: int
    headers: Dict[str, str]
    request_id: str
    rate_limit: RateLimitInfo


# ---------------------------------------------------------------------------
# Query serialization
# ---------------------------------------------------------------------------


def serialize_query(
    params: Optional[Dict[str, Any]],
) -> Dict[str, Any]:
    """Serialize query parameters for httpx.

    - Skips ``None`` values.
    - Converts booleans to ``"true"``/``"false"`` strings.
    - Lists produce repeated keys via httpx's native handling.
    """
    if params is None:
        return {}

    out: Dict[str, Any] = {}
    for key, value in params.items():
        if value is None:
            continue
        if isinstance(value, bool):
            out[key] = "true" if value else "false"
        elif isinstance(value, list):
            out[key] = value
        else:
            out[key] = value
    return out


# ---------------------------------------------------------------------------
# Header parsing helpers
# ---------------------------------------------------------------------------


def _parse_rate_limit(headers: httpx.Headers) -> RateLimitInfo:
    """Parse X-RateLimit-* headers."""

    def _int_or_none(name: str) -> Optional[int]:
        raw = headers.get(name)
        if raw is None:
            return None
        try:
            return int(raw)
        except (ValueError, TypeError):
            return None

    return RateLimitInfo(
        limit=_int_or_none("X-RateLimit-Limit"),
        remaining=_int_or_none("X-RateLimit-Remaining"),
        reset=_int_or_none("X-RateLimit-Reset"),
    )


def _extract_request_id(headers: httpx.Headers) -> str:
    return headers.get("X-Request-Id", "")


def _headers_to_dict(headers: httpx.Headers) -> Dict[str, str]:
    return dict(headers.items())


# ---------------------------------------------------------------------------
# Sync HTTP client
# ---------------------------------------------------------------------------


class SyncHttpClient:
    """Synchronous HTTP client using ``httpx.Client``."""

    def __init__(self, config: Optional[HttpClientConfig] = None) -> None:
        cfg = config or HttpClientConfig()
        self._base_url = cfg.base_url.rstrip("/")
        self._timeout = cfg.timeout
        self._api_key = resolve_api_key(cfg.api_key)
        self._retry = cfg.retry

        default_headers = {
            "Accept": "application/json",
            "User-Agent": SDK_USER_AGENT,
            **cfg.headers,
            **build_auth_headers(self._api_key),
        }

        self._client = httpx.Client(
            base_url=self._base_url,
            headers=default_headers,
            timeout=self._timeout,
        )

    # -- Public API ---------------------------------------------------------

    def request(
        self,
        method: str,
        path: str,
        *,
        query: Optional[Dict[str, Any]] = None,
        body: Optional[Any] = None,
        headers: Optional[Dict[str, str]] = None,
        timeout: Optional[float] = None,
    ) -> APIResponse[Any]:
        """Execute an HTTP request and return the unwrapped response."""

        def _do() -> APIResponse[Any]:
            return self._execute_request(
                method, path, query=query, body=body, headers=headers, timeout=timeout
            )

        return with_retry(_do, method, self._retry)

    def request_paginated(
        self,
        method: str,
        path: str,
        *,
        query: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
        timeout: Optional[float] = None,
    ) -> PaginatedResponse[Any]:
        """Execute an HTTP request and return a paginated response."""

        def _do() -> PaginatedResponse[Any]:
            return self._execute_paginated_request(
                method, path, query=query, headers=headers, timeout=timeout
            )

        return with_retry(_do, method, self._retry)

    def get(
        self,
        path: str,
        query: Optional[Dict[str, Any]] = None,
    ) -> APIResponse[Any]:
        return self.request("GET", path, query=query)

    def get_paginated(
        self,
        path: str,
        query: Optional[Dict[str, Any]] = None,
    ) -> PaginatedResponse[Any]:
        return self.request_paginated("GET", path, query=query)

    def post(
        self,
        path: str,
        body: Optional[Any] = None,
        query: Optional[Dict[str, Any]] = None,
    ) -> APIResponse[Any]:
        return self.request("POST", path, body=body, query=query)

    def put(
        self,
        path: str,
        body: Optional[Any] = None,
        query: Optional[Dict[str, Any]] = None,
    ) -> APIResponse[Any]:
        return self.request("PUT", path, body=body, query=query)

    def delete(
        self,
        path: str,
        query: Optional[Dict[str, Any]] = None,
    ) -> APIResponse[Any]:
        return self.request("DELETE", path, query=query)

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

    def __enter__(self) -> SyncHttpClient:
        return self

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

    # -- Internal -----------------------------------------------------------

    def _execute_request(
        self,
        method: str,
        path: str,
        *,
        query: Optional[Dict[str, Any]] = None,
        body: Optional[Any] = None,
        headers: Optional[Dict[str, str]] = None,
        timeout: Optional[float] = None,
    ) -> APIResponse[Any]:
        response = self._do_fetch(method, path, query=query, body=body, headers=headers, timeout=timeout)
        request_id = _extract_request_id(response.headers)
        rate_limit = _parse_rate_limit(response.headers)

        if not response.is_success:
            self._raise_api_error(response, request_id)

        json_body = response.json()
        return APIResponse(
            data=json_body.get("data"),
            status=response.status_code,
            headers=_headers_to_dict(response.headers),
            request_id=request_id,
            rate_limit=rate_limit,
        )

    def _execute_paginated_request(
        self,
        method: str,
        path: str,
        *,
        query: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
        timeout: Optional[float] = None,
    ) -> PaginatedResponse[Any]:
        response = self._do_fetch(method, path, query=query, headers=headers, timeout=timeout)
        request_id = _extract_request_id(response.headers)
        rate_limit = _parse_rate_limit(response.headers)

        if not response.is_success:
            self._raise_api_error(response, request_id)

        json_body = response.json()
        pagination_raw = json_body.get("pagination", {})
        pagination = PaginationMeta(
            page=pagination_raw.get("page", 1),
            limit=pagination_raw.get("limit", 25),
            total=pagination_raw.get("total"),
            total_pages=pagination_raw.get("totalPages"),
        )
        return PaginatedResponse(
            data=json_body.get("data", []),
            pagination=pagination,
            status=response.status_code,
            headers=_headers_to_dict(response.headers),
            request_id=request_id,
            rate_limit=rate_limit,
        )

    def _do_fetch(
        self,
        method: str,
        path: str,
        *,
        query: Optional[Dict[str, Any]] = None,
        body: Optional[Any] = None,
        headers: Optional[Dict[str, str]] = None,
        timeout: Optional[float] = None,
    ) -> httpx.Response:
        params = serialize_query(query)
        req_headers: Dict[str, str] = {}
        if headers:
            req_headers.update(headers)
        if body is not None:
            req_headers["Content-Type"] = "application/json"

        try:
            return self._client.request(
                method,
                path,
                params=params or None,
                json=body,
                headers=req_headers if req_headers else None,
                timeout=timeout or self._timeout,
            )
        except httpx.TimeoutException as exc:
            raise RequestTimeoutError(timeout or self._timeout) from exc
        except httpx.ConnectError as exc:
            raise ConnectError(str(exc), exc) from exc

    def _raise_api_error(self, response: httpx.Response, request_id: str) -> None:
        body: Optional[Dict[str, Any]] = None
        try:
            body = response.json()
        except Exception:
            pass
        raise create_api_error(
            response.status_code, body, request_id, _headers_to_dict(response.headers)
        )


# ---------------------------------------------------------------------------
# Async HTTP client
# ---------------------------------------------------------------------------


class AsyncHttpClient:
    """Asynchronous HTTP client using ``httpx.AsyncClient``."""

    def __init__(self, config: Optional[HttpClientConfig] = None) -> None:
        cfg = config or HttpClientConfig()
        self._base_url = cfg.base_url.rstrip("/")
        self._timeout = cfg.timeout
        self._api_key = resolve_api_key(cfg.api_key)
        self._retry = cfg.retry

        default_headers = {
            "Accept": "application/json",
            "User-Agent": SDK_USER_AGENT,
            **cfg.headers,
            **build_auth_headers(self._api_key),
        }

        self._client = httpx.AsyncClient(
            base_url=self._base_url,
            headers=default_headers,
            timeout=self._timeout,
        )

    # -- Public API ---------------------------------------------------------

    async def request(
        self,
        method: str,
        path: str,
        *,
        query: Optional[Dict[str, Any]] = None,
        body: Optional[Any] = None,
        headers: Optional[Dict[str, str]] = None,
        timeout: Optional[float] = None,
    ) -> APIResponse[Any]:
        """Execute an HTTP request and return the unwrapped response."""

        async def _do() -> APIResponse[Any]:
            return await self._execute_request(
                method, path, query=query, body=body, headers=headers, timeout=timeout
            )

        return await async_with_retry(_do, method, self._retry)

    async def request_paginated(
        self,
        method: str,
        path: str,
        *,
        query: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
        timeout: Optional[float] = None,
    ) -> PaginatedResponse[Any]:
        """Execute an HTTP request and return a paginated response."""

        async def _do() -> PaginatedResponse[Any]:
            return await self._execute_paginated_request(
                method, path, query=query, headers=headers, timeout=timeout
            )

        return await async_with_retry(_do, method, self._retry)

    async def get(
        self,
        path: str,
        query: Optional[Dict[str, Any]] = None,
    ) -> APIResponse[Any]:
        return await self.request("GET", path, query=query)

    async def get_paginated(
        self,
        path: str,
        query: Optional[Dict[str, Any]] = None,
    ) -> PaginatedResponse[Any]:
        return await self.request_paginated("GET", path, query=query)

    async def post(
        self,
        path: str,
        body: Optional[Any] = None,
        query: Optional[Dict[str, Any]] = None,
    ) -> APIResponse[Any]:
        return await self.request("POST", path, body=body, query=query)

    async def put(
        self,
        path: str,
        body: Optional[Any] = None,
        query: Optional[Dict[str, Any]] = None,
    ) -> APIResponse[Any]:
        return await self.request("PUT", path, body=body, query=query)

    async def delete(
        self,
        path: str,
        query: Optional[Dict[str, Any]] = None,
    ) -> APIResponse[Any]:
        return await self.request("DELETE", path, query=query)

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

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

    async def __aexit__(self, *_: Any) -> None:
        await self.aclose()

    # -- Internal -----------------------------------------------------------

    async def _execute_request(
        self,
        method: str,
        path: str,
        *,
        query: Optional[Dict[str, Any]] = None,
        body: Optional[Any] = None,
        headers: Optional[Dict[str, str]] = None,
        timeout: Optional[float] = None,
    ) -> APIResponse[Any]:
        response = await self._do_fetch(method, path, query=query, body=body, headers=headers, timeout=timeout)
        request_id = _extract_request_id(response.headers)
        rate_limit = _parse_rate_limit(response.headers)

        if not response.is_success:
            self._raise_api_error(response, request_id)

        json_body = response.json()
        return APIResponse(
            data=json_body.get("data"),
            status=response.status_code,
            headers=_headers_to_dict(response.headers),
            request_id=request_id,
            rate_limit=rate_limit,
        )

    async def _execute_paginated_request(
        self,
        method: str,
        path: str,
        *,
        query: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
        timeout: Optional[float] = None,
    ) -> PaginatedResponse[Any]:
        response = await self._do_fetch(method, path, query=query, headers=headers, timeout=timeout)
        request_id = _extract_request_id(response.headers)
        rate_limit = _parse_rate_limit(response.headers)

        if not response.is_success:
            self._raise_api_error(response, request_id)

        json_body = response.json()
        pagination_raw = json_body.get("pagination", {})
        pagination = PaginationMeta(
            page=pagination_raw.get("page", 1),
            limit=pagination_raw.get("limit", 25),
            total=pagination_raw.get("total"),
            total_pages=pagination_raw.get("totalPages"),
        )
        return PaginatedResponse(
            data=json_body.get("data", []),
            pagination=pagination,
            status=response.status_code,
            headers=_headers_to_dict(response.headers),
            request_id=request_id,
            rate_limit=rate_limit,
        )

    async def _do_fetch(
        self,
        method: str,
        path: str,
        *,
        query: Optional[Dict[str, Any]] = None,
        body: Optional[Any] = None,
        headers: Optional[Dict[str, str]] = None,
        timeout: Optional[float] = None,
    ) -> httpx.Response:
        params = serialize_query(query)
        req_headers: Dict[str, str] = {}
        if headers:
            req_headers.update(headers)
        if body is not None:
            req_headers["Content-Type"] = "application/json"

        try:
            return await self._client.request(
                method,
                path,
                params=params or None,
                json=body,
                headers=req_headers if req_headers else None,
                timeout=timeout or self._timeout,
            )
        except httpx.TimeoutException as exc:
            raise RequestTimeoutError(timeout or self._timeout) from exc
        except httpx.ConnectError as exc:
            raise ConnectError(str(exc), exc) from exc

    def _raise_api_error(self, response: httpx.Response, request_id: str) -> None:
        body: Optional[Dict[str, Any]] = None
        try:
            body = response.json()
        except Exception:
            pass
        raise create_api_error(
            response.status_code, body, request_id, _headers_to_dict(response.headers)
        )
