"""Base API Client for common HTTP operations.

This module provides a base class for API clients with common functionality
like authentication, header management, and HTTP client lifecycle.
"""

import logging
from pathlib import Path
from typing import Self

import httpx

from hezor_common.transfer.base_sdk.constants import (
    ANONYMOUS_HEADER_PRIVATE_KEY,
    ANONYMOUS_HEADER_PRIVATE_KEY_PASSWORD,
    ANONYMOUS_HEADER_PUBLIC_KEY,
    REQ_HEADER_APP_NAME_KEY,
    REQ_HEADER_META_INFO_KEY,
)
from hezor_common.transfer.base_sdk.meta_info import MetaInfo

logger = logging.getLogger(__name__)


class BaseAPIClient:
    """Base API client with common functionality.

    This class provides common functionality for API clients including:
    - HTTP client lifecycle management (async context manager)
    - Authentication header management (API key, MetaInfo JWT)
    - Request header building

    Attributes
    ----------
    base_url : str
        API base URL (without trailing slash)
    timeout : float
        Request timeout in seconds
    api_key : str | None
        API key for Bearer token authentication
    meta_info : MetaInfo | None
        Meta information for JWT header generation
    private_key_path : str | Path | None
        Path to private key file for JWT signing
    password : bytes | None
        Password for encrypted private key
    meta_info_expires_in : int
        JWT token expiration time in seconds
    """

    def __init__(
        self,
        base_url: str,
        timeout: float = 120.0,
        api_key: str | None = None,
        meta_info: MetaInfo | None = None,
        private_key_path: str | Path | None = None,
        password: bytes | None = None,
        meta_info_expires_in: int = 3600,
        app_name: str | None = None,
    ):
        """Initialize base API client.

        Parameters
        ----------
        base_url : str
            API base URL
        timeout : float, optional
            Request timeout in seconds (default: 120.0)
        api_key : str | None, optional
            API key for Bearer token authentication (default: None)
        meta_info : MetaInfo | None, optional
            Meta information for JWT header (default: None)
        private_key_path : str | Path | None, optional
            Path to private key file for JWT signing (default: None)
        password : bytes | None, optional
            Password for encrypted private key (default: None)
        meta_info_expires_in : int, optional
            JWT token expiration time in seconds (default: 3600)
        app_name : str | None, optional
            Application name sent as X-APP-NAME header for
            server-side certificate resolution (default: None)
        """
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self.api_key = api_key
        self.meta_info = meta_info
        self.private_key_path = private_key_path
        self.password = password
        self.meta_info_expires_in = meta_info_expires_in
        self.app_name = app_name
        self._client: httpx.AsyncClient | None = None

    async def __aenter__(self) -> Self:
        """Async context manager entry.

        Returns
        -------
        Self
            Self reference
        """
        self._client = httpx.AsyncClient(base_url=self.base_url, timeout=self.timeout)
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
        """Async context manager exit.

        Parameters
        ----------
        exc_type : type | None
            Exception type if raised
        exc_val : Exception | None
            Exception value if raised
        exc_tb : traceback | None
            Exception traceback if raised
        """
        if self._client:
            await self._client.aclose()
            self._client = None

    @property
    def client(self) -> httpx.AsyncClient:
        """Get HTTP client instance.

        Returns
        -------
        httpx.AsyncClient
            HTTP client instance

        Raises
        ------
        RuntimeError
            If client not initialized (must use async context manager)
        """
        if self._client is None:
            msg = "Client not initialized. Use 'async with' context manager."
            raise RuntimeError(msg)
        return self._client

    def _get_headers(self) -> dict[str, str]:
        """Build request headers.

        Generates headers including:
        - Content-Type: application/json
        - Authorization: Bearer {api_key} (if api_key provided)
        - X-META-INFO: {jwt_token} (if meta_info provided)
        - X-APP-NAME: {app_name} (if app_name provided)

        When meta_info is provided but private_key_path is None,
        anonymous private key will be used with a warning.

        Returns
        -------
        dict[str, str]
            Request headers dictionary

        Examples
        --------
        >>> client = BaseAPIClient(base_url="http://api.example.com")
        >>> headers = client._get_headers()
        >>> headers["Content-Type"]
        'application/json'
        """
        # Default headers
        headers = {
            "Content-Type": "application/json",
        }

        # Add Application name header
        if self.app_name is not None:
            headers[REQ_HEADER_APP_NAME_KEY] = self.app_name

        # Add Authorization header
        if self.api_key is not None:
            headers["Authorization"] = f"Bearer {self.api_key}"
            logger.info(f"Added Authorization header, api_key: {self.api_key[:6]}***")

        # Add MetaInfo JWT header
        if self.meta_info is not None:
            if self.private_key_path is None:
                # Use anonymous key with warning
                logger.warning("*" * 20)
                logger.warning(
                    "private_key_path is not provided, using anonymous private key. "
                    "The corresponding public key is: \n"
                    f"{ANONYMOUS_HEADER_PUBLIC_KEY}"
                )
                logger.warning("*" * 20)
                meta_header = self.meta_info.to_request_header(
                    private_key_pem=ANONYMOUS_HEADER_PRIVATE_KEY,
                    password=ANONYMOUS_HEADER_PRIVATE_KEY_PASSWORD,
                    expires_in=self.meta_info_expires_in,
                )
            else:
                # Use provided private key file path
                meta_header = self.meta_info.to_request_header(
                    private_key_path=self.private_key_path,
                    password=self.password,
                    expires_in=self.meta_info_expires_in,
                )

            headers.update(meta_header)
            logger.info(
                f"Added MetaInfo JWT header, meta_info: {meta_header[REQ_HEADER_META_INFO_KEY][:10]}***"
            )

        return headers

    async def get(
        self,
        path: str,
        **kwargs,
    ) -> httpx.Response:
        """Execute GET request with automatic header injection.

        Parameters
        ----------
        path : str
            Request path (e.g., "/endpoint" or "/api/users")
        **kwargs
            Additional arguments to pass to httpx.AsyncClient.get()

        Returns
        -------
        httpx.Response
            HTTP response

        Examples
        --------
        >>> async with BaseAPIClient(base_url="http://api.example.com") as client:
        ...     response = await client.get("/endpoint", params={"key": "value"})
        ...     data = response.json()
        """
        headers = self._get_headers()
        # Merge with any headers provided in kwargs
        if "headers" in kwargs:
            headers.update(kwargs["headers"])
        kwargs["headers"] = headers
        return await self.client.get(path, **kwargs)

    async def post(
        self,
        path: str,
        **kwargs,
    ) -> httpx.Response:
        """Execute POST request with automatic header injection.

        Parameters
        ----------
        path : str
            Request path (e.g., "/endpoint" or "/api/users")
        **kwargs
            Additional arguments to pass to httpx.AsyncClient.post()

        Returns
        -------
        httpx.Response
            HTTP response

        Examples
        --------
        >>> async with BaseAPIClient(base_url="http://api.example.com") as client:
        ...     response = await client.post("/endpoint", json={"key": "value"})
        ...     data = response.json()
        """
        headers = self._get_headers()
        # Merge with any headers provided in kwargs
        if "headers" in kwargs:
            headers.update(kwargs["headers"])
        kwargs["headers"] = headers
        return await self.client.post(path, **kwargs)

    async def put(
        self,
        path: str,
        **kwargs,
    ) -> httpx.Response:
        """Execute PUT request with automatic header injection.

        Parameters
        ----------
        path : str
            Request path (e.g., "/endpoint" or "/api/users")
        **kwargs
            Additional arguments to pass to httpx.AsyncClient.put()

        Returns
        -------
        httpx.Response
            HTTP response

        Examples
        --------
        >>> async with BaseAPIClient(base_url="http://api.example.com") as client:
        ...     response = await client.put("/endpoint", json={"key": "value"})
        ...     data = response.json()
        """
        headers = self._get_headers()
        # Merge with any headers provided in kwargs
        if "headers" in kwargs:
            headers.update(kwargs["headers"])
        kwargs["headers"] = headers
        return await self.client.put(path, **kwargs)

    async def patch(
        self,
        path: str,
        **kwargs,
    ) -> httpx.Response:
        """Execute PATCH request with automatic header injection.

        Parameters
        ----------
        path : str
            Request path (e.g., "/endpoint" or "/api/users")
        **kwargs
            Additional arguments to pass to httpx.AsyncClient.patch()

        Returns
        -------
        httpx.Response
            HTTP response

        Examples
        --------
        >>> async with BaseAPIClient(base_url="http://api.example.com") as client:
        ...     response = await client.patch("/endpoint", json={"key": "value"})
        ...     data = response.json()
        """
        headers = self._get_headers()
        # Merge with any headers provided in kwargs
        if "headers" in kwargs:
            headers.update(kwargs["headers"])
        kwargs["headers"] = headers
        return await self.client.patch(path, **kwargs)

    async def delete(
        self,
        path: str,
        **kwargs,
    ) -> httpx.Response:
        """Execute DELETE request with automatic header injection.

        Parameters
        ----------
        path : str
            Request path (e.g., "/endpoint" or "/api/users")
        **kwargs
            Additional arguments to pass to httpx.AsyncClient.delete()

        Returns
        -------
        httpx.Response
            HTTP response

        Examples
        --------
        >>> async with BaseAPIClient(base_url="http://api.example.com") as client:
        ...     response = await client.delete("/endpoint")
        ...     data = response.json()
        """
        headers = self._get_headers()
        # Merge with any headers provided in kwargs
        if "headers" in kwargs:
            headers.update(kwargs["headers"])
        kwargs["headers"] = headers
        return await self.client.delete(path, **kwargs)
