"""HTTP client wrapper for API requests."""

from collections.abc import Iterable
from typing import Any, Union, cast

import httpx

from kamimind_storage._internal.auth import AuthHandler
from kamimind_storage.constants import API_BASE_PATH, DEFAULT_TIMEOUT
from kamimind_storage.exceptions import (
    AuthenticationError,
    AuthorizationError,
    NotFoundError,
    QuotaExceededError,
    RetryableError,
    StorageError,
)
from kamimind_storage.utils.retry import is_retryable_status_code


class HttpClient:
    """HTTP client for making API requests."""

    def __init__(
        self,
        base_url: str,
        auth_handler: AuthHandler,
        timeout: float = DEFAULT_TIMEOUT,
    ) -> None:
        """Initialize the HTTP client.

        Args:
            base_url: Base URL of the storage service.
            auth_handler: Authentication handler.
            timeout: Request timeout in seconds.
        """
        self._base_url = base_url.rstrip("/")
        self._auth_handler = auth_handler
        self._timeout = timeout
        self._client = httpx.Client(timeout=timeout)

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

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

        Args:
            path: API path (without base path).

        Returns:
            Full URL.
        """
        if path.startswith("/"):
            path = path[1:]
        return f"{self._base_url}{API_BASE_PATH}/{path}"

    def _get_headers(self, extra_headers: Union[dict[str, str], None] = None) -> dict[str, str]:
        """Get headers for request.

        Args:
            extra_headers: Additional headers to include.

        Returns:
            Complete headers dictionary.
        """
        headers = {
            "Content-Type": "application/json",
            **self._auth_handler.get_auth_headers(),
        }
        if extra_headers:
            headers.update(extra_headers)
        return headers

    def _handle_error_response(self, response: httpx.Response) -> None:
        """Handle error responses.

        Args:
            response: HTTP response object.

        Raises:
            Appropriate exception based on status code.
        """
        status_code = response.status_code

        # Try to parse error from response body
        try:
            data = response.json()
            error_message = data.get("error", response.text)
        except Exception:
            error_message = response.text or f"HTTP {status_code}"

        if status_code == 401:
            raise AuthenticationError(error_message)
        elif status_code == 403:
            raise AuthorizationError(error_message)
        elif status_code == 404:
            raise NotFoundError(error_message)
        elif status_code == 413:
            raise QuotaExceededError(error_message)
        elif is_retryable_status_code(status_code):
            retry_after = response.headers.get("Retry-After")
            raise RetryableError(
                error_message,
                retry_after=int(retry_after) if retry_after else None,
            )
        else:
            raise StorageError(
                error_message,
                status_code=status_code,
            )

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

        Args:
            path: API path.
            params: Query parameters.
            headers: Additional headers.

        Returns:
            Response JSON data.
        """
        response = self._client.get(
            self._get_url(path),
            params=params,
            headers=self._get_headers(headers),
        )

        if not response.is_success:
            self._handle_error_response(response)

        return cast("dict[str, Any]", response.json())

    def post(
        self,
        path: str,
        json: Union[dict[str, Any], None] = None,
        headers: Union[dict[str, str], None] = None,
    ) -> dict[str, Any]:
        """Make a POST request.

        Args:
            path: API path.
            json: JSON body.
            headers: Additional headers.

        Returns:
            Response JSON data.
        """
        response = self._client.post(
            self._get_url(path),
            json=json,
            headers=self._get_headers(headers),
        )

        if not response.is_success:
            self._handle_error_response(response)

        return cast("dict[str, Any]", response.json())

    def patch(
        self,
        path: str,
        json: Union[dict[str, Any], None] = None,
        headers: Union[dict[str, str], None] = None,
    ) -> dict[str, Any]:
        """Make a PATCH request.

        Args:
            path: API path.
            json: JSON body.
            headers: Additional headers.

        Returns:
            Response JSON data.
        """
        response = self._client.patch(
            self._get_url(path),
            json=json,
            headers=self._get_headers(headers),
        )

        if not response.is_success:
            self._handle_error_response(response)

        return cast("dict[str, Any]", response.json())

    def put_presigned(
        self,
        url: str,
        content: bytes,
        content_type: str = "application/octet-stream",
    ) -> httpx.Response:
        """Upload content to a presigned URL.

        Args:
            url: Presigned URL.
            content: Content to upload.
            content_type: Content type.

        Returns:
            HTTP response.
        """
        response = self._client.put(
            url,
            content=content,
            headers={"Content-Type": content_type},
        )

        if not response.is_success:
            self._handle_error_response(response)

        return response

    def get_download(
        self,
        url: str,
        stream: bool = False,
    ) -> Union[bytes, Iterable[bytes]]:
        """Download content from a URL.

        Args:
            url: Download URL.
            stream: Whether to stream the response.

        Returns:
            HTTP response.
        """
        response = self._client.get(url)
        if not response.is_success:
            self._handle_error_response(response)
        if stream:
            return iter(response.iter_bytes())
        return response.content


class AsyncHttpClient:
    """Async HTTP client for making API requests."""

    def __init__(
        self,
        base_url: str,
        auth_handler: AuthHandler,
        timeout: float = DEFAULT_TIMEOUT,
    ) -> None:
        """Initialize the async HTTP client.

        Args:
            base_url: Base URL of the storage service.
            auth_handler: Authentication handler.
            timeout: Request timeout in seconds.
        """
        self._base_url = base_url.rstrip("/")
        self._auth_handler = auth_handler
        self._timeout = timeout
        self._client: Union[httpx.AsyncClient, None] = None

    async def __aenter__(self) -> "AsyncHttpClient":
        """Enter async context."""
        self._client = httpx.AsyncClient(timeout=self._timeout)
        return self

    async def __aexit__(self, *args: Any) -> None:
        """Exit async context."""
        if self._client:
            await self._client.aclose()

    def _get_url(self, path: str) -> str:
        """Build full URL from path."""
        if path.startswith("/"):
            path = path[1:]
        return f"{self._base_url}{API_BASE_PATH}/{path}"

    def _get_headers(self, extra_headers: Union[dict[str, str], None] = None) -> dict[str, str]:
        """Get headers for request."""
        headers = {
            "Content-Type": "application/json",
            **self._auth_handler.get_auth_headers(),
        }
        if extra_headers:
            headers.update(extra_headers)
        return headers

    def _handle_error_response(self, response: httpx.Response) -> None:
        """Handle error responses."""
        status_code = response.status_code

        try:
            data = response.json()
            error_message = data.get("error", response.text)
        except Exception:
            error_message = response.text or f"HTTP {status_code}"

        if status_code == 401:
            raise AuthenticationError(error_message)
        elif status_code == 403:
            raise AuthorizationError(error_message)
        elif status_code == 404:
            raise NotFoundError(error_message)
        elif status_code == 413:
            raise QuotaExceededError(error_message)
        elif is_retryable_status_code(status_code):
            retry_after = response.headers.get("Retry-After")
            raise RetryableError(
                error_message,
                retry_after=int(retry_after) if retry_after else None,
            )
        else:
            raise StorageError(error_message, status_code=status_code)

    async def get(
        self,
        path: str,
        params: Union[dict[str, Any], None] = None,
        headers: Union[dict[str, str], None] = None,
    ) -> dict[str, Any]:
        """Make an async GET request."""
        if not self._client:
            raise RuntimeError("Client not initialized. Use 'async with' context.")

        response = await self._client.get(
            self._get_url(path),
            params=params,
            headers=self._get_headers(headers),
        )

        if not response.is_success:
            self._handle_error_response(response)

        return cast("dict[str, Any]", response.json())

    async def post(
        self,
        path: str,
        json: Union[dict[str, Any], None] = None,
        headers: Union[dict[str, str], None] = None,
    ) -> dict[str, Any]:
        """Make an async POST request."""
        if not self._client:
            raise RuntimeError("Client not initialized. Use 'async with' context.")

        response = await self._client.post(
            self._get_url(path),
            json=json,
            headers=self._get_headers(headers),
        )

        if not response.is_success:
            self._handle_error_response(response)

        return cast("dict[str, Any]", response.json())

    async def patch(
        self,
        path: str,
        json: Union[dict[str, Any], None] = None,
        headers: Union[dict[str, str], None] = None,
    ) -> dict[str, Any]:
        """Make an async PATCH request."""
        if not self._client:
            raise RuntimeError("Client not initialized. Use 'async with' context.")

        response = await self._client.patch(
            self._get_url(path),
            json=json,
            headers=self._get_headers(headers),
        )

        if not response.is_success:
            self._handle_error_response(response)

        return cast("dict[str, Any]", response.json())

    async def put_presigned(
        self,
        url: str,
        content: bytes,
        content_type: str = "application/octet-stream",
    ) -> httpx.Response:
        """Upload content to a presigned URL."""
        if not self._client:
            raise RuntimeError("Client not initialized. Use 'async with' context.")

        response = await self._client.put(
            url,
            content=content,
            headers={"Content-Type": content_type},
        )

        if not response.is_success:
            self._handle_error_response(response)

        return response
