from __future__ import annotations

import contextlib
import logging
from typing import TYPE_CHECKING, Any, TypeVar, Unpack, cast

from curl_cffi import CurlMime
from curl_cffi import requests as curl

from ._base_client import BaseClient
from ._cache_registry import AsyncClientCache
from ._transport import (
    AsyncSession,
    BodyData,
    RequestKwargs,
)
from ._transport_sse import (
    RequestStreamKwargs,
    SSEEvent,
    astream_json_events,
)
from ._utils import (
    mask_sensitive_json,
    mask_token,
)
from .exceptions import APIError
from .resources.async_ import (
    AsyncAccountResource,
    AsyncAuthResource,
    AsyncCharactersResource,
    AsyncGenAgentResource,
    AsyncHealthResource,
    AsyncImagesResource,
    AsyncModelsResource,
    AsyncPaymentsResource,
    AsyncPromptsResource,
    AsyncSearchResource,
    AsyncTasksResource,
)

if TYPE_CHECKING:
    from collections.abc import AsyncIterator, Callable, Mapping
    from types import TracebackType

    from ._endpoints import Route
    from ._internal._schemas import APIEnvelope

logger = logging.getLogger(__name__)


T = TypeVar("T")


class AsyncClient(BaseClient[AsyncSession]):
    """Asynchronous SeaArt API client.

    Example:
        >>> async with AsyncClient(token="abc123") as client:
        ...     info = await client.account.me()
    """

    def _init_session(self) -> AsyncSession:
        return cast(
            "AsyncSession",
            curl.AsyncSession(
                http_version=self._cfg.http_version,
                proxy=self._cfg.proxy,
                proxies=self._cfg.proxies,
                impersonate=self._cfg.impersonate,
            ),
        )

    def _init_resources(self) -> None:
        self.cache = AsyncClientCache(self)
        self.auth = AsyncAuthResource(self)
        self.health = AsyncHealthResource(self)
        self.account = AsyncAccountResource(self)
        self.tasks = AsyncTasksResource(self)
        self.images = AsyncImagesResource(self)
        self.prompts = AsyncPromptsResource(self)
        self.payments = AsyncPaymentsResource(self)
        self.genagent = AsyncGenAgentResource(self)
        self.characters = AsyncCharactersResource(self)
        self.search = AsyncSearchResource(self)
        self.models = AsyncModelsResource(self)

    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:
        with contextlib.suppress(Exception):
            await self._session.close()

    async def get_account_id(self) -> str | None:
        if self._account_id is None and self.token is not None:
            self._account_id = (await self.account.me()).value.id
        return self._account_id

    async def request(
        self,
        method: str,
        path: str,
        *,
        response_model: type[T] | None = None,
        params: Mapping[str, Any] | None = None,
        headers: Mapping[str, str] | None = None,
        json: dict[str, Any] | None = None,
        data: BodyData | None = None,
        stream: bool = False,
        multipart: CurlMime | None = None,
    ) -> APIEnvelope[Any]:
        """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 (``"GET"``, ``"POST"``, etc.).
            path: API endpoint path (e.g. ``"account/login-in"``).
            response_model: Pydantic model used to validate ``envelope.data``.
            params: Query parameters appended to the URL.
            headers: Extra headers merged with the request defaults.
            json: JSON-serializable request body.
            data: Raw bytes request body.
            stream: Whether to keep the response stream open (passed through to
                the transport).
            multipart: Multipart form-data body (``CurlMime`` instance).

        Returns:
            ``APIEnvelope`` wrapping the parsed server response.

        Raises:
            APIError: If the HTTP status is 4xx/5xx or the response body cannot
                be parsed as a valid envelope.

        Note:
            Provide at most one of ``json``, ``data``, or ``multipart``.

        Example:
            >>> async with AsyncClient(token="abc123") as client:
            ...     envelope = await client.request("POST", "account/info")
            ...     print(envelope.value)
        """
        url = self._build_url(path)
        hdrs = self._build_headers(headers)

        safe_json: dict[str, Any] | None
        safe_json = mask_sensitive_json(json) if isinstance(json, dict) else json

        safe_headers = dict(hdrs)
        if "token" in safe_headers:
            safe_headers["token"] = mask_token(safe_headers["token"])

        logger.debug(
            "→ Request: %s %s | headers=%s | params=%s | json=%s | data=%s",
            method,
            url,
            safe_headers,
            params,
            safe_json,
            "<bytes>" if data else None,
        )

        r = await self._session.request(
            method,
            url,
            params=params,
            headers=hdrs,
            json=json,
            data=data,
            timeout=self._cfg.timeout,
            stream=stream,
            multipart=multipart,
        )

        logger.debug(
            "← Response: %s | status=%s | body_length=%s",
            url,
            r.status_code,
            len(r.content),
        )

        return self._handle_response(r.status_code, r.content, response_model)

    async def request_stream(
        self,
        method: str,
        path: str,
        *,
        params: Mapping[str, Any] | None = None,
        headers: Mapping[str, str] | None = None,
        json: dict[str, Any] | None = None,
        data: BodyData | None = None,
        parser: Callable[[SSEEvent, dict[str, Any] | None], T],
    ) -> AsyncIterator[T]:
        """Open an SSE stream and asynchronously yield parsed items.

        Args:
            method: HTTP method (``"GET"``, ``"POST"``, etc.).
            path: API endpoint path.
            params: Query parameters appended to the URL.
            headers: Extra headers merged with the request defaults.
            json: JSON-serializable request body.
            data: Raw bytes request body.
            parser: Callable that converts each ``(SSEEvent, dict | None)``
                into a typed item to yield.

        Yields:
            Items returned by ``parser`` for each received SSE event.

        Raises:
            APIError: On transport or parse failures.

        Example:
            >>> async with AsyncClient(token="abc123") as client:
            ...     async for item in client.request_stream(
            ...         "POST", "chat/completions",
            ...         json={"prompt": "Hello"},
            ...         parser=client.parse_sse_stream,
            ...     ):
            ...         print(item.event, item.data)
        """
        url = self._build_url(path)
        hdrs = self._build_headers(headers)

        safe_json: dict[str, Any] | None
        safe_json = mask_sensitive_json(json) if isinstance(json, dict) else json

        safe_headers = dict(hdrs)
        if "token" in safe_headers:
            safe_headers["token"] = mask_token(safe_headers["token"])

        logger.debug(
            "→ Request (astream/native): %s %s | headers=%s | params=%s | json=%s | data=%s",
            method,
            url,
            safe_headers,
            params,
            safe_json,
            "<bytes>" if data else None,
        )

        logger.debug("← Response (astream opened/native): %s", url)
        try:
            async for item in astream_json_events(
                session_request=self._session.request,
                method=method,
                url=url,
                headers=hdrs,
                params=params,
                json_body=json,
                data=data,
                timeout=self._cfg.timeout,
                parser=parser,
                max_queue=256,
            ):
                yield item

        except Exception as e:
            if isinstance(e, APIError):
                raise
            raise APIError(0, b"", msg=str(e)) from e

        finally:
            logger.debug("← Response (astream closed/native): %s", url)

    async def request_raw(
        self,
        method: str,
        path: str,
        *,
        params: Mapping[str, Any] | None = None,
        headers: Mapping[str, str] | None = None,
        json: dict[str, Any] | None = None,
        data: BodyData | None = None,
        stream: bool = False,
        multipart: CurlMime | None = None,
    ) -> curl.Response:
        """Make an HTTP request and return the raw ``curl_cffi`` response.

        Unlike :meth:`request`, this method does **not** parse the response
        into an ``APIEnvelope``. Use it for endpoints that return non-standard
        JSON, HTML, binary data, or any format outside the SeaArt API envelope
        convention.

        Args:
            method: HTTP method (``"GET"``, ``"POST"``, etc.).
            path: API endpoint path (e.g. ``"some/page"``).
            params: Query parameters appended to the URL.
            headers: Extra headers merged with the request defaults.
            json: JSON-serializable request body.
            data: Raw bytes request body.
            stream: Whether to keep the response stream open (passed through to
                the transport).
            multipart: Multipart form-data body (``CurlMime`` instance).

        Returns:
            Raw ``curl_cffi.requests.Response`` object with ``.status_code``,
            ``.content``, ``.text``, ``.json()``, ``.headers``, etc.

        Raises:
            APIError: If the transport raises an unexpected error.

        Note:
            Provide at most one of ``json``, ``data``, or ``multipart``.

        Example:
            >>> async with AsyncClient() as client:
            ...     r = await client.request_raw("GET", "some/page")
            ...     print(r.status_code, r.text[:100])
        """
        url = self._build_url(path)
        hdrs = self._build_headers(headers)

        safe_json: dict[str, Any] | None
        safe_json = mask_sensitive_json(json) if isinstance(json, dict) else json

        safe_headers = dict(hdrs)
        if "token" in safe_headers:
            safe_headers["token"] = mask_token(safe_headers["token"])

        logger.debug(
            "→ Request (raw): %s %s | headers=%s | params=%s | json=%s | data=%s",
            method,
            url,
            safe_headers,
            params,
            safe_json,
            "<bytes>" if data else None,
        )

        r = await self._session.request(
            method,
            url,
            params=params,
            headers=hdrs,
            json=json,
            data=data,
            timeout=self._cfg.timeout,
            stream=stream,
            multipart=multipart,
        )

        logger.debug(
            "← Response (raw): %s | status=%s | body_length=%s",
            url,
            r.status_code,
            len(r.content),
        )

        return r  # type: ignore[return-value]

    async def request_route(
        self,
        route: Route,
        response_model: type[T] | None = None,
        **kwargs: Unpack[RequestKwargs],
    ) -> APIEnvelope[Any]:
        return await self.request(
            route.method, route.path, response_model=response_model, **kwargs
        )

    async def request_route_stream(
        self, route: Route, **kwargs: Unpack[RequestStreamKwargs]
    ) -> AsyncIterator[Any]:
        async for item in self.request_stream(route.method, route.path, **kwargs):
            yield item
