from __future__ import annotations

from typing import Any, Mapping, Protocol, cast, Type
from types import TracebackType
from curl_cffi import requests as curl

from ._base_client import BaseClient
from ._models import (
    AccountInfo,
    APIEnvelope,
    LoginData,
    ClientConfig,
    HttpVersion,
    Status,
)
from ._constants import BASE_URL, USER_AGENT


class _Resp(Protocol):
    status_code: int
    content: bytes
    headers: Mapping[str, str] | None = None


class _SyncSession(Protocol):
    def request(
        self,
        method: str,
        url: str,
        *,
        headers: Mapping[str, str],
        params: Mapping[str, Any] | None,
        data: bytes | None,
        json: Any | None,
        timeout: float,
    ) -> _Resp: ...
    def close(self) -> None: ...


class _AsyncSession(Protocol):
    async def request(
        self,
        method: str,
        url: str,
        *,
        headers: Mapping[str, str],
        params: Mapping[str, Any] | None,
        data: bytes | None,
        json: Any | None,
        timeout: float,
    ) -> _Resp: ...
    async def close(self) -> None: ...


class SyncClient(BaseClient):
    """Synchronous API client.

    Provides blocking methods to communicate with the API.

    Args:
        base_url: Base API endpoint URL.
        timeout: Request timeout in seconds.
        user_agent: Custom User-Agent header for requests.
        http_version: HTTP protocol version (e.g., "v2", "v3").
        token: Optional authentication token to include in headers.

    Example:
        ```python
        with SyncClient(token="abc123") as client:
            account = client.get_my_account()
        ```
    """

    def __init__(
        self,
        base_url: str = BASE_URL,
        timeout: float = 30.0,
        user_agent: str = USER_AGENT,
        http_version: HttpVersion = "v2",
        token: str | None = None,
    ) -> None:
        config = ClientConfig(
            base_url=base_url,
            timeout=timeout,
            user_agent=user_agent,
            http_version=http_version,
            token=token,
        )
        super().__init__(config=config)
        self._session: _SyncSession = cast(
            _SyncSession, curl.Session(http_version=http_version)
        )

    def __enter__(self) -> SyncClient:
        return self

    def __exit__(
        self,
        exc_type: Type[BaseException] | None,
        exc: BaseException | None,
        tb: TracebackType | None,
    ) -> None:
        self.close()

    def close(self) -> None:
        try:
            self._session.close()
        except Exception:
            pass

    def request(
        self,
        method: str,
        path: str,
        *,
        params: Mapping[str, Any] | None = None,
        headers: Mapping[str, str] | None = None,
        json_body: Any = None,
        data: bytes | None = None,
    ) -> APIEnvelope:
        """Make an HTTP request to the API server.

        Builds the absolute URL and default headers, delegates the call to the
        underlying session, and normalizes the response into an `APIEnvelope`.

        Args:
            method: HTTP method (e.g., "GET", "POST").
            path: API endpoint path (e.g., "account/login-in").
            params: Optional query parameters to include in the URL.
            headers: Additional headers to merge with defaults.
            json_body: JSON-serializable body to send with the request.
            data: Raw request body (bytes) to send with the request.

        Returns:
            APIEnvelope: Normalized API response envelope.

        Raises:
            APIError: If the API returns a non-success status or an invalid body.

        Notes:
            Exactly one of `json_body` or `data` should be provided. If both are
            given, the transport will typically prefer the raw `data`.
        """
        url = self._build_url(path)
        hdrs = self._build_headers(headers)
        r = self._session.request(
            method,
            url,
            params=params,
            headers=hdrs,
            json=json_body,
            data=data,
            timeout=self._cfg.timeout,
        )
        return self._handle_response(r.status_code, r.content)

    def login(self, email: str, password: str) -> LoginData:
        """Log in with email and password.

        Sends a POST request to the API and updates the client's authentication
        token upon success.

        Args:
            email: Account email.
            password: Account password.

        Returns:
            LoginData: Authentication token and related information.

        Raises:
            APIError: If the API returns an error or an invalid response.
            AuthError: If authentication fails.
            AccountNotFoundError: If the account does not exist.
            InvalidPasswordError: If the password is incorrect.

        Notes:
            On success, this method updates the client configuration with the
            received token so subsequent requests are authenticated.
        """
        env = self.request(
            "POST",
            "account/login-in",
            json_body={"email": email, "password": password},
        )

        login = LoginData.model_validate(env.data or {})
        token: str = login.token
        self._cfg = self._cfg.model_copy(update={"token": token})
        return login

    def get_my_account(self) -> AccountInfo:
        """Retrieve information about the authenticated account.

        Returns:
            AccountInfo: Details of the current account.

        Raises:
            APIError: If the API returns an error or an invalid response.
            AuthTokenInvalidError: If the authentication token is invalid.
        """
        env = self.request("POST", "account/my")
        return AccountInfo.model_validate(env.data or {})

    def ping(self) -> Status:
        """Ping the API server.

        Returns:
            Status: Server health/status response.

        Raises:
            APIError: If the API returns an error or an invalid response.
        """
        env = self.request("POST", "ping")
        return Status.model_validate(env.status)


class AsyncClient(BaseClient):
    """Asynchronous API client.

    Provides async methods for interacting with the API.

    Args:
        base_url: Base API URL (defaults to the built-in constant `BASE_URL`).
        timeout: Request timeout in seconds.
        user_agent: Custom User-Agent header.
        http_version: HTTP protocol version (e.g., "v2", "v3").
        token: Optional authentication token to include in requests.

    Example:
        ```python
        async with AsyncClient(token="abc123") as client:
            info = await client.get_my_account()
        ```
    """

    def __init__(
        self,
        base_url: str = BASE_URL,
        timeout: float = 30.0,
        user_agent: str = USER_AGENT,
        http_version: HttpVersion = "v2",
        token: str | None = None,
    ) -> None:
        config = ClientConfig(
            base_url=base_url,
            timeout=timeout,
            user_agent=user_agent,
            http_version=http_version,
            token=token,
        )
        super().__init__(config=config)
        self._asession: _AsyncSession = cast(
            _AsyncSession, curl.AsyncSession(http_version=http_version)
        )

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

    async def __aexit__(
        self,
        exc_type: Type[BaseException] | None,
        exc: BaseException | None,
        tb: TracebackType | None,
    ) -> None:
        await self.close()

    async def close(self) -> None:
        try:
            await self._asession.close()
        except Exception:
            pass

    async def request(
        self,
        method: str,
        path: str,
        *,
        params: Mapping[str, Any] | None = None,
        headers: Mapping[str, str] | None = None,
        json_body: Any = None,
        data: bytes | None = None,
    ) -> APIEnvelope:
        """Make an HTTP request to the API server.

        Builds the absolute URL and default headers, delegates the call to the
        underlying session, and normalizes the response into an `APIEnvelope`.

        Args:
            method: HTTP method (e.g., "GET", "POST").
            path: API endpoint path (e.g., "account/login-in").
            params: Optional query parameters to include in the URL.
            headers: Additional headers to merge with defaults.
            json_body: JSON-serializable body to send with the request.
            data: Raw request body (bytes) to send with the request.

        Returns:
            APIEnvelope: Normalized API response envelope.

        Raises:
            APIError: If the API returns a non-success status or an invalid body.

        Notes:
            Exactly one of `json_body` or `data` should be provided. If both are
            given, the transport will typically prefer the raw `data`.
        """
        url = self._build_url(path)
        hdrs = self._build_headers(headers)
        r = await self._asession.request(
            method,
            url,
            params=params,
            headers=hdrs,
            json=json_body,
            data=data,
            timeout=self._cfg.timeout,
        )
        return self._handle_response(r.status_code, r.content)

    async def login(self, email: str, password: str) -> LoginData:
        """Log in with email and password.

        Sends a POST request to the API and updates the client's authentication
        token upon success.

        Args:
            email: Account email.
            password: Account password.

        Returns:
            LoginData: Authentication token and related information.

        Raises:
            APIError: If the API returns an error or an invalid response.
            AuthError: If authentication fails.
            AccountNotFoundError: If the account does not exist.
            InvalidPasswordError: If the password is incorrect.

        Notes:
            On success, this method updates the client configuration with the
            received token so subsequent requests are authenticated.
        """
        env = await self.request(
            "POST",
            "account/login-in",
            json_body={"email": email, "password": password},
        )

        login = LoginData.model_validate(env.data or {})
        token: str = login.token
        self._cfg = self._cfg.model_copy(update={"token": token})
        return login

    async def get_my_account(self) -> AccountInfo:
        """Retrieve information about the authenticated account.

        Returns:
            AccountInfo: Details of the current account.

        Raises:
            APIError: If the API returns an error or an invalid response.
        """
        env = await self.request("POST", "account/my")
        return AccountInfo.model_validate(env.data or {})

    async def ping(self) -> Status:
        """Ping the API server.

        Returns:
            Status: Server health/status response.

        Raises:
            APIError: If the API returns an error or an invalid response.
        """
        env = await self.request("POST", "ping")
        return Status.model_validate(env.status)
