"""Base client implementation with HTTP handling, auth, and retry logic."""

from __future__ import annotations

import os
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, TypeVar

import httpx
from pydantic import BaseModel
from tenacity import (
    retry,
    retry_if_exception_type,
    stop_after_attempt,
    wait_exponential_jitter,
)

from ticksupply.exceptions import (
    NetworkError,
    RateLimitError,
    raise_for_error_response,
)
from ticksupply.models.common import ErrorResponse

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

# Default configuration
DEFAULT_BASE_URL = "https://api.ticksupply.com/v1"
DEFAULT_TIMEOUT = 30.0
DEFAULT_MAX_RETRIES = 3
ENV_API_KEY = "TICKSUPPLY_API_KEY"


@dataclass
class ClientConfig:
    """Configuration for the Ticksupply client.

    Attributes:
        api_key: API key for authentication. If not provided, reads from
            TICKSUPPLY_API_KEY environment variable.
        base_url: Base URL for the API. Defaults to https://api.ticksupply.com/v1.
        timeout: Request timeout in seconds. Defaults to 30.
        max_retries: Maximum number of retries for failed requests. Defaults to 3.
            Set to 0 to disable retries.
    """

    api_key: str | None = None
    base_url: str = DEFAULT_BASE_URL
    timeout: float = DEFAULT_TIMEOUT
    max_retries: int = DEFAULT_MAX_RETRIES

    def __post_init__(self) -> None:
        # Read API key from environment if not provided
        if self.api_key is None:
            self.api_key = os.environ.get(ENV_API_KEY)

        if not self.api_key:
            raise ValueError(
                f"API key is required. Provide it directly or set {ENV_API_KEY} environment variable."
            )


def datetime_to_nanoseconds(dt: datetime) -> int:
    """Convert datetime to nanoseconds since epoch.

    Args:
        dt: Datetime object. If naive, assumed to be UTC.

    Returns:
        Nanoseconds since Unix epoch.
    """
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=timezone.utc)
    return int(dt.timestamp() * 1_000_000_000)


class BaseHTTPClient:
    """Base HTTP client with common functionality for sync and async clients."""

    def __init__(self, config: ClientConfig) -> None:
        self._config = config

    def _get_headers(self) -> dict[str, str]:
        """Get default headers for API requests."""
        return {
            "X-Api-Key": self._config.api_key or "",
            "Content-Type": "application/json",
            "Accept": "application/json",
        }

    def _handle_response_error(self, response: httpx.Response) -> None:
        """Handle error responses from the API.

        Args:
            response: HTTP response object.

        Raises:
            APIError: Appropriate subclass based on the error.
        """
        if response.is_success:
            return

        # Try to parse error response
        try:
            error_data = response.json()
            error_response = ErrorResponse.model_validate(error_data)
            error = error_response.error

            # Get retry-after header for rate limiting
            retry_after = None
            if response.status_code == 429:
                retry_after_header = response.headers.get("Retry-After")
                if retry_after_header:
                    try:
                        retry_after = int(retry_after_header)
                    except ValueError:
                        pass

            raise_for_error_response(
                http_status=response.status_code,
                code=error.code,
                message=error.message,
                trace_id=error.trace_id,
                retry_after=retry_after,
            )
        except (ValueError, KeyError):
            # Could not parse error response, create generic error
            raise_for_error_response(
                http_status=response.status_code,
                code="unknown",
                message=response.text or f"HTTP {response.status_code}",
            )

    def _build_url(self, path: str) -> str:
        """Build full URL from path.

        Args:
            path: API path (e.g., "/exchanges").

        Returns:
            Full URL.
        """
        base = self._config.base_url.rstrip("/")
        path = path.lstrip("/")
        return f"{base}/{path}"


class SyncHTTPClient(BaseHTTPClient):
    """Synchronous HTTP client."""

    def __init__(self, config: ClientConfig) -> None:
        super().__init__(config)
        self._client: httpx.Client | None = None

    def _get_client(self) -> httpx.Client:
        """Get or create the HTTP client."""
        if self._client is None:
            self._client = httpx.Client(
                headers=self._get_headers(),
                timeout=self._config.timeout,
            )
        return self._client

    def close(self) -> None:
        """Close the HTTP client."""
        if self._client is not None:
            self._client.close()
            self._client = None

    def __enter__(self) -> SyncHTTPClient:
        return self

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

    def _make_request(
        self,
        method: str,
        path: str,
        params: dict[str, Any] | None = None,
        json_data: dict[str, Any] | None = None,
    ) -> httpx.Response:
        """Make an HTTP request without retry logic."""
        client = self._get_client()
        url = self._build_url(path)

        # Filter out None values from params
        if params:
            params = {k: v for k, v in params.items() if v is not None}

        try:
            response = client.request(
                method=method,
                url=url,
                params=params,
                json=json_data,
            )
            self._handle_response_error(response)
            return response
        except httpx.RequestError as e:
            raise NetworkError(f"Request failed: {e}", cause=e) from e

    def request(
        self,
        method: str,
        path: str,
        params: dict[str, Any] | None = None,
        json_data: dict[str, Any] | None = None,
    ) -> httpx.Response:
        """Make an HTTP request with retry logic.

        Args:
            method: HTTP method (GET, POST, etc.).
            path: API path.
            params: Query parameters.
            json_data: JSON body data.

        Returns:
            HTTP response.

        Raises:
            APIError: On API errors.
            NetworkError: On network errors.
        """
        if self._config.max_retries > 0:
            return self._request_with_retry(method, path, params, json_data)
        return self._make_request(method, path, params, json_data)

    def _request_with_retry(
        self,
        method: str,
        path: str,
        params: dict[str, Any] | None = None,
        json_data: dict[str, Any] | None = None,
    ) -> httpx.Response:
        """Make request with retry logic using tenacity."""

        @retry(
            stop=stop_after_attempt(self._config.max_retries + 1),
            wait=wait_exponential_jitter(initial=1, max=60, jitter=5),
            retry=retry_if_exception_type((NetworkError, RateLimitError)),
            reraise=True,
        )
        def _do_request() -> httpx.Response:
            return self._make_request(method, path, params, json_data)

        return _do_request()

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

    def post(
        self,
        path: str,
        json_data: dict[str, Any] | None = None,
        params: dict[str, Any] | None = None,
    ) -> httpx.Response:
        """Make a POST request."""
        return self.request("POST", path, params=params, json_data=json_data)

    def delete(
        self,
        path: str,
        params: dict[str, Any] | None = None,
    ) -> httpx.Response:
        """Make a DELETE request."""
        return self.request("DELETE", path, params=params)


class AsyncHTTPClient(BaseHTTPClient):
    """Asynchronous HTTP client."""

    def __init__(self, config: ClientConfig) -> None:
        super().__init__(config)
        self._client: httpx.AsyncClient | None = None

    def _get_client(self) -> httpx.AsyncClient:
        """Get or create the async HTTP client."""
        if self._client is None:
            self._client = httpx.AsyncClient(
                headers=self._get_headers(),
                timeout=self._config.timeout,
            )
        return self._client

    async def close(self) -> None:
        """Close the HTTP client."""
        if self._client is not None:
            await self._client.aclose()
            self._client = None

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

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

    async def _make_request(
        self,
        method: str,
        path: str,
        params: dict[str, Any] | None = None,
        json_data: dict[str, Any] | None = None,
    ) -> httpx.Response:
        """Make an HTTP request without retry logic."""
        client = self._get_client()
        url = self._build_url(path)

        # Filter out None values from params
        if params:
            params = {k: v for k, v in params.items() if v is not None}

        try:
            response = await client.request(
                method=method,
                url=url,
                params=params,
                json=json_data,
            )
            self._handle_response_error(response)
            return response
        except httpx.RequestError as e:
            raise NetworkError(f"Request failed: {e}", cause=e) from e

    async def request(
        self,
        method: str,
        path: str,
        params: dict[str, Any] | None = None,
        json_data: dict[str, Any] | None = None,
    ) -> httpx.Response:
        """Make an HTTP request with retry logic.

        Args:
            method: HTTP method (GET, POST, etc.).
            path: API path.
            params: Query parameters.
            json_data: JSON body data.

        Returns:
            HTTP response.

        Raises:
            APIError: On API errors.
            NetworkError: On network errors.
        """
        if self._config.max_retries > 0:
            return await self._request_with_retry(method, path, params, json_data)
        return await self._make_request(method, path, params, json_data)

    async def _request_with_retry(
        self,
        method: str,
        path: str,
        params: dict[str, Any] | None = None,
        json_data: dict[str, Any] | None = None,
    ) -> httpx.Response:
        """Make request with retry logic using tenacity."""

        @retry(
            stop=stop_after_attempt(self._config.max_retries + 1),
            wait=wait_exponential_jitter(initial=1, max=60, jitter=5),
            retry=retry_if_exception_type((NetworkError, RateLimitError)),
            reraise=True,
        )
        async def _do_request() -> httpx.Response:
            return await self._make_request(method, path, params, json_data)

        return await _do_request()

    async def get(
        self,
        path: str,
        params: dict[str, Any] | None = None,
    ) -> httpx.Response:
        """Make a GET request."""
        return await self.request("GET", path, params=params)

    async def post(
        self,
        path: str,
        json_data: dict[str, Any] | None = None,
        params: dict[str, Any] | None = None,
    ) -> httpx.Response:
        """Make a POST request."""
        return await self.request("POST", path, params=params, json_data=json_data)

    async def delete(
        self,
        path: str,
        params: dict[str, Any] | None = None,
    ) -> httpx.Response:
        """Make a DELETE request."""
        return await self.request("DELETE", path, params=params)
