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 SyncClientCache
from ._transport import (
    BodyData,
    RequestKwargs,
    SyncSession,
)
from ._transport_sse import (
    RequestStreamKwargs,
    SSEEvent,
    stream_json_events,
)
from ._utils import (
    mask_sensitive_json,
    mask_token,
)
from .exceptions import APIError
from .resources.sync import (
    AccountResource,
    AuthResource,
    CharactersResource,
    GenAgentResource,
    HealthResource,
    ImagesResource,
    ModelsResource,
    PaymentsResource,
    PromptsResource,
    SearchResource,
    TasksResource,
)

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

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

logger = logging.getLogger(__name__)


T = TypeVar("T")


class SyncClient(BaseClient[SyncSession]):
    """Synchronous SeaArt API client.

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

    def _init_session(self) -> SyncSession:
        return cast(
            "SyncSession",
            curl.Session(
                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 = SyncClientCache(self)
        self.auth = AuthResource(self)
        self.health = HealthResource(self)
        self.account = AccountResource(self)
        self.tasks = TasksResource(self)
        self.images = ImagesResource(self)
        self.prompts = PromptsResource(self)
        self.payments = PaymentsResource(self)
        self.genagent = GenAgentResource(self)
        self.characters = CharactersResource(self)
        self.search = SearchResource(self)
        self.models = ModelsResource(self)

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

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

    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:
            >>> with SyncClient(token="abc123") as client:
            ...     envelope = 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 = 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)

    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],
    ) -> Iterator[T]:
        """Open an SSE stream and 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:
            >>> with SyncClient(token="abc123") as client:
            ...     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 (stream/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 (stream opened/native): %s", url)
        try:
            yield from stream_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,
            )
        except Exception as e:
            if isinstance(e, APIError):
                raise
            raise APIError(0, b"", msg=str(e)) from e

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

    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:
            >>> with SyncClient() as client:
            ...     r = 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 = 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]

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

    def request_route_stream(
        self,
        route: Route,
        **kwargs: Unpack[RequestStreamKwargs],
    ) -> Iterator[Any]:
        yield from self.request_stream(route.method, route.path, **kwargs)
