"""
Credo AI SDK client classes.

Auto-generated from OpenAPI specification.
Generated: 2026-01-28T01:42:33.414545+00:00
Source: 0.1.0

This module provides both synchronous and asynchronous clients for the API,
with comprehensive Pydantic validation and error handling.
"""

import os
import ssl
from typing import Any, Dict, List, Literal, Optional, Union

import httpx
from attrs import define, evolve, field
from pydantic import BaseModel, TypeAdapter
from pydantic import ValidationError as PydanticValidationError

from .auth import (
    AuthenticationError,
    CredoAIAuth,
    create_auth_handler,
    create_auth_handler_async,
)
from .errors import ApiError, ClientValidationError
from .models import (
    ClientRequest,
    ClientResponse,
    HTTPValidationError,
    HealthResponse,
    JsonApiResponse,
    ModelCreate,
    ModelResponse,
    ModelUpdate,
    PaginatedResponse_ModelResponse_,
    PaginatedResponse_QuestionnaireAttachmentResponse_,
    PaginatedResponse_QuestionnaireResponse_,
    PaginatedResponse_UseCaseResponse_,
    PaginatedResponse_VendorResponse_,
    PaginationInfo,
    PingResponse,
    QuestionEvidenceCreate,
    QuestionEvidenceUpdate,
    QuestionnaireAttachment,
    QuestionnaireAttachmentResponse,
    QuestionnaireAttachmentUpdate,
    QuestionnaireBaseResponse,
    QuestionnaireCreate,
    QuestionnaireDuplicate,
    QuestionnaireInfo,
    QuestionnaireResponse,
    QuestionnaireUpdate,
    RelationshipAdd,
    TokenResponse,
    UseCaseCreate,
    UseCaseCustomField,
    UseCaseResponse,
    UseCaseUpdate,
    ValidationError,
    VendorCreate,
    VendorResponse,
    VendorUpdate,
)
from .resources import (
    AsyncAuthenticationResource,
    AsyncHealthResource,
    AsyncModelsResource,
    AsyncQuestionnairesResource,
    AsyncSystemResource,
    AsyncUseCasesResource,
    AsyncVendorsResource,
    AuthenticationResource,
    HealthResource,
    ModelsResource,
    QuestionnairesResource,
    SystemResource,
    UseCasesResource,
    VendorsResource,
)

# API path to append to base URL
API_PATH = "/api/v1/integration"

# Environment variable names
ENV_API_KEY = "CREDOAI_API_KEY"  # pragma: allowlist secret
ENV_API_URL = "CREDOAI_API_URL"
ENV_TENANT = "CREDOAI_TENANT"


@define
class Client:
    """A class for keeping track of data related to the API

    The following are accepted as keyword arguments and will be used to construct httpx Clients internally:

        ``base_url``: The base URL for the API, all requests are made to a relative path to this URL

        ``cookies``: A dictionary of cookies to be sent with every request

        ``headers``: A dictionary of headers to be sent with every request

        ``timeout``: The maximum amount of a time a request can take. API functions will raise
        httpx.TimeoutException if this is exceeded.

        ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production,
        but can be set to False for testing purposes.

        ``follow_redirects``: Whether or not to follow redirects. Default value is False.

        ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor.


    Attributes:
        raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a
            status code that was not documented in the source OpenAPI document. Can also be provided as a keyword
            argument to the constructor.
    """

    raise_on_unexpected_status: bool = field(default=False, kw_only=True)
    _base_url: str = field(alias="base_url")
    _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies")
    _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers")
    _timeout: httpx.Timeout | None = field(default=None, kw_only=True, alias="timeout")
    _verify_ssl: str | bool | ssl.SSLContext = field(
        default=True, kw_only=True, alias="verify_ssl"
    )
    _follow_redirects: bool = field(
        default=False, kw_only=True, alias="follow_redirects"
    )
    _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args")
    _client: httpx.Client | None = field(default=None, init=False)
    _async_client: httpx.AsyncClient | None = field(default=None, init=False)

    def with_headers(self, headers: dict[str, str]) -> "Client":
        """Get a new client matching this one with additional headers"""
        if self._client is not None:
            self._client.headers.update(headers)
        if self._async_client is not None:
            self._async_client.headers.update(headers)
        return evolve(self, headers={**self._headers, **headers})

    def with_cookies(self, cookies: dict[str, str]) -> "Client":
        """Get a new client matching this one with additional cookies"""
        if self._client is not None:
            self._client.cookies.update(cookies)
        if self._async_client is not None:
            self._async_client.cookies.update(cookies)
        return evolve(self, cookies={**self._cookies, **cookies})

    def with_timeout(self, timeout: httpx.Timeout) -> "Client":
        """Get a new client matching this one with a new timeout configuration"""
        if self._client is not None:
            self._client.timeout = timeout
        if self._async_client is not None:
            self._async_client.timeout = timeout
        return evolve(self, timeout=timeout)

    def set_httpx_client(self, client: httpx.Client) -> "Client":
        """Manually set the underlying httpx.Client

        **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
        """
        self._client = client
        return self

    def get_httpx_client(self) -> httpx.Client:
        """Get the underlying httpx.Client, constructing a new one if not previously set"""
        if self._client is None:
            self._client = httpx.Client(
                base_url=self._base_url,
                cookies=self._cookies,
                headers=self._headers,
                timeout=self._timeout,
                verify=self._verify_ssl,
                follow_redirects=self._follow_redirects,
                **self._httpx_args,
            )
        return self._client

    def __enter__(self) -> "Client":
        """Enter a context manager for self.client—you cannot enter twice (see httpx docs)"""
        self.get_httpx_client().__enter__()
        return self

    def __exit__(self, *args: Any, **kwargs: Any) -> None:
        """Exit a context manager for internal httpx.Client (see httpx docs)"""
        self.get_httpx_client().__exit__(*args, **kwargs)

    def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "Client":
        """Manually set the underlying httpx.AsyncClient

        **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
        """
        self._async_client = async_client
        return self

    def get_async_httpx_client(self) -> httpx.AsyncClient:
        """Get the underlying httpx.AsyncClient, constructing a new one if not previously set"""
        if self._async_client is None:
            self._async_client = httpx.AsyncClient(
                base_url=self._base_url,
                cookies=self._cookies,
                headers=self._headers,
                timeout=self._timeout,
                verify=self._verify_ssl,
                follow_redirects=self._follow_redirects,
                **self._httpx_args,
            )
        return self._async_client

    async def __aenter__(self) -> "Client":
        """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)"""
        await self.get_async_httpx_client().__aenter__()
        return self

    async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
        """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)"""
        await self.get_async_httpx_client().__aexit__(*args, **kwargs)


@define
class AuthenticatedClient:
    """A Client which has been authenticated for use on secured endpoints

    The following are accepted as keyword arguments and will be used to construct httpx Clients internally:

        ``base_url``: The base URL for the API, all requests are made to a relative path to this URL

        ``cookies``: A dictionary of cookies to be sent with every request

        ``headers``: A dictionary of headers to be sent with every request

        ``timeout``: The maximum amount of a time a request can take. API functions will raise
        httpx.TimeoutException if this is exceeded.

        ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production,
        but can be set to False for testing purposes.

        ``follow_redirects``: Whether or not to follow redirects. Default value is False.

        ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor.


    Attributes:
        raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a
            status code that was not documented in the source OpenAPI document. Can also be provided as a keyword
            argument to the constructor.
        token: The token to use for authentication
        prefix: The prefix to use for the Authorization header
        auth_header_name: The name of the Authorization header
    """

    raise_on_unexpected_status: bool = field(default=False, kw_only=True)
    _base_url: str = field(alias="base_url")
    _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies")
    _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers")
    _timeout: httpx.Timeout | None = field(default=None, kw_only=True, alias="timeout")
    _verify_ssl: str | bool | ssl.SSLContext = field(
        default=True, kw_only=True, alias="verify_ssl"
    )
    _follow_redirects: bool = field(
        default=False, kw_only=True, alias="follow_redirects"
    )
    _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args")
    _client: httpx.Client | None = field(default=None, init=False)
    _async_client: httpx.AsyncClient | None = field(default=None, init=False)

    token: str
    prefix: str = "Bearer"
    auth_header_name: str = "Authorization"

    def with_headers(self, headers: dict[str, str]) -> "AuthenticatedClient":
        """Get a new client matching this one with additional headers"""
        if self._client is not None:
            self._client.headers.update(headers)
        if self._async_client is not None:
            self._async_client.headers.update(headers)
        return evolve(self, headers={**self._headers, **headers})

    def with_cookies(self, cookies: dict[str, str]) -> "AuthenticatedClient":
        """Get a new client matching this one with additional cookies"""
        if self._client is not None:
            self._client.cookies.update(cookies)
        if self._async_client is not None:
            self._async_client.cookies.update(cookies)
        return evolve(self, cookies={**self._cookies, **cookies})

    def with_timeout(self, timeout: httpx.Timeout) -> "AuthenticatedClient":
        """Get a new client matching this one with a new timeout configuration"""
        if self._client is not None:
            self._client.timeout = timeout
        if self._async_client is not None:
            self._async_client.timeout = timeout
        return evolve(self, timeout=timeout)

    def set_httpx_client(self, client: httpx.Client) -> "AuthenticatedClient":
        """Manually set the underlying httpx.Client

        **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
        """
        self._client = client
        return self

    def get_httpx_client(self) -> httpx.Client:
        """Get the underlying httpx.Client, constructing a new one if not previously set"""
        if self._client is None:
            self._headers[self.auth_header_name] = (
                f"{self.prefix} {self.token}" if self.prefix else self.token
            )
            self._client = httpx.Client(
                base_url=self._base_url,
                cookies=self._cookies,
                headers=self._headers,
                timeout=self._timeout,
                verify=self._verify_ssl,
                follow_redirects=self._follow_redirects,
                **self._httpx_args,
            )
        return self._client

    def __enter__(self) -> "AuthenticatedClient":
        """Enter a context manager for self.client—you cannot enter twice (see httpx docs)"""
        self.get_httpx_client().__enter__()
        return self

    def __exit__(self, *args: Any, **kwargs: Any) -> None:
        """Exit a context manager for internal httpx.Client (see httpx docs)"""
        self.get_httpx_client().__exit__(*args, **kwargs)

    def set_async_httpx_client(
        self, async_client: httpx.AsyncClient
    ) -> "AuthenticatedClient":
        """Manually set the underlying httpx.AsyncClient

        **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
        """
        self._async_client = async_client
        return self

    def get_async_httpx_client(self) -> httpx.AsyncClient:
        """Get the underlying httpx.AsyncClient, constructing a new one if not previously set"""
        if self._async_client is None:
            self._headers[self.auth_header_name] = (
                f"{self.prefix} {self.token}" if self.prefix else self.token
            )
            self._async_client = httpx.AsyncClient(
                base_url=self._base_url,
                cookies=self._cookies,
                headers=self._headers,
                timeout=self._timeout,
                verify=self._verify_ssl,
                follow_redirects=self._follow_redirects,
                **self._httpx_args,
            )
        return self._async_client

    async def __aenter__(self) -> "AuthenticatedClient":
        """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)"""
        await self.get_async_httpx_client().__aenter__()
        return self

    async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
        """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)"""
        await self.get_async_httpx_client().__aexit__(*args, **kwargs)


# Sync client class that extends AuthenticatedClient with structured Pydantic types
class CredoAI(AuthenticatedClient):
    """Synchronous client for Credo AI SDK.

    This client provides a high-level, type-safe interface to the credoai-integration-service API.
    All methods return validated Pydantic models and handle errors comprehensively.

    Features:
        - Automatic API key to JWT token exchange
        - Full Pydantic model validation for requests and responses
        - Comprehensive error handling with detailed error types
        - Resource-based API organization for intuitive usage
        - Built-in retry logic and timeout management

    Available Resources:
        - system: 2 operation(s)
        - use_cases: 5 operation(s)
        - models: 5 operation(s)
        - vendors: 5 operation(s)
        - questionnaires: 6 operation(s)
        - authentication: 1 operation(s)
        - health: 2 operation(s)
    Example:
        >>> # Option 1: Using environment variables (recommended)
        >>> import os
        >>> os.environ["CREDOAI_API_KEY"] = "your-api-key"
        >>> os.environ["CREDOAI_API_URL"] = "https://api.credo.ai"  # optional
        >>> client = CredoAI()
        >>>
        >>> # Option 2: Using explicit arguments
        >>> client = CredoAI(
        ...     base_url="https://api.credo.ai",
        ...     api_key="your-api-key"
        ... )
        >>>
        >>> # Use resource-based operations
        >>> result = client.system.metrics(...)
        >>> print(result.id)  # Access validated response data
        Authentication:
        API key authentication is handled automatically. The client exchanges your
        API key for a JWT token on initialization and handles token refresh as needed.

    Error Handling:
        All API errors are wrapped in structured exceptions:
        - ApiError: HTTP errors from the API
        - ClientValidationError: Request/response validation failures
        - AuthenticationError: Authentication and authorization failures
    """

    # Type annotations for resource attributes (enables IDE autocomplete)
    system: "SystemResource"
    use_cases: "UseCasesResource"
    models: "ModelsResource"
    vendors: "VendorsResource"
    questionnaires: "QuestionnairesResource"
    authentication: "AuthenticationResource"
    health: "HealthResource"

    def __init__(
        self,
        base_url: Optional[str] = None,
        api_key: Optional[str] = None,
        tenant: Optional[str] = None,
        validate_responses: bool = True,
        timeout: Optional[float] = 30.0,
        **kwargs,
    ):
        """Initialize the synchronous credoai client.

        Performs automatic API key to JWT token exchange and sets up all resource
        endpoints for immediate use. All network operations are performed during
        initialization to validate credentials.

        Configuration can be provided via constructor arguments or environment variables.
        Constructor arguments take precedence over environment variables.

        Environment Variables:
            CREDOAI_API_KEY: API key for authentication
            CREDOAI_API_URL: Base URL of the API service (without /api/v1/integration path)
            CREDOAI_TENANT: Tenant identifier for multi-tenant authentication

        Args:
            base_url (str, optional): The base URL of the API service (without path).
                The SDK automatically appends '/api/v1/integration' to this URL.
                Default: CREDOAI_API_URL env var or "https://api.credo.ai"
            api_key (str, optional): Your API key for authentication. The client will
                automatically exchange this for a JWT token.
                Default: CREDOAI_API_KEY env var (required)
            tenant (str, optional): Your CredoAI tenant identifier.
                Default: CREDOAI_TENANT env var (required)
            validate_responses (bool): Whether to validate API responses against
                Pydantic models. Recommended for type safety. Default: True
            timeout (float, optional): Default timeout for all requests in seconds.
                Can be overridden per request. Default: 30.0
            **kwargs: Additional arguments passed to the underlying HTTP client:
                - cookies: Default cookies for all requests
                - headers: Default headers for all requests
                - verify_ssl: SSL certificate verification (default: True)
                - follow_redirects: Whether to follow redirects (default: False)

        Raises:
            ValueError: If api_key or tenant is not provided and corresponding env var is not set
            AuthenticationError: If API key exchange fails
            ConnectionError: If unable to connect to the API

        Example:
            >>> # Using environment variables
            >>> import os
            >>> os.environ["CREDOAI_API_KEY"] = "your-api-key"
            >>> os.environ["CREDOAI_TENANT"] = "your-tenant"
            >>> client = CredoAI()
            >>>
            >>> # Using explicit arguments
            >>> client = CredoAI(
            ...     base_url="https://api.credo.ai",
            ...     api_key="your-api-key",
            ...     tenant="your-tenant",
            ...     timeout=60.0
            ... )
        """
        # Resolve configuration from env vars with defaults
        _base_url = (
            base_url
            if base_url is not None
            else os.environ.get(ENV_API_URL, "https://api.credo.ai")
        )
        _api_key = api_key if api_key is not None else os.environ.get(ENV_API_KEY)
        _tenant = tenant if tenant is not None else os.environ.get(ENV_TENANT)

        if not _api_key:
            raise ValueError(
                "API key is required for authentication. "
                "Provide api_key argument or set CREDOAI_API_KEY environment variable."
            )

        if not _tenant:
            raise ValueError(
                "Tenant is required. "
                "Provide tenant argument or set CREDOAI_TENANT environment variable."
            )

        # Construct full API URL with path
        full_api_url = f"{_base_url.rstrip('/')}{API_PATH}"

        # Perform JWT exchange
        jwt_token = None
        try:
            auth_handler = create_auth_handler(
                base_url=full_api_url,
                auth_type="api_key",
                token=_api_key,
                tenant=_tenant,
                timeout=timeout,
            )
            jwt_token = auth_handler._access_token

        except AuthenticationError as e:
            raise AuthenticationError(f"Authentication failed: {e}")

        # Configure authentication for the underlying client
        # All requests use Bearer token format internally
        kwargs.setdefault("auth_header_name", "Authorization")
        kwargs.setdefault("prefix", "Bearer")
        kwargs.setdefault("timeout", httpx.Timeout(timeout))

        # Add tenant header to default headers
        default_headers = kwargs.get("headers", {})
        default_headers["X-Tenant"] = _tenant
        kwargs["headers"] = default_headers

        super().__init__(base_url=full_api_url, token=jwt_token, **kwargs)

        # Store configuration details
        self._user_base_url = _base_url
        self._validate_responses = validate_responses
        self._auth_handler = auth_handler

        # Initialize resource objects for resource-based API
        self.system = SystemResource(self)
        self.use_cases = UseCasesResource(self)
        self.models = ModelsResource(self)
        self.vendors = VendorsResource(self)
        self.questionnaires = QuestionnairesResource(self)
        self.authentication = AuthenticationResource(self)
        self.health = HealthResource(self)

    def _make_request(
        self,
        method: str,
        url: str,
        data: Optional[Union[BaseModel, Dict[str, Any]]] = None,
        params: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
        timeout: Optional[float] = None,
        response_model: Optional[type] = None,
        is_list_response: bool = False,
    ) -> Union[ClientResponse, Any]:
        """Make a structured HTTP request with comprehensive validation and error handling.

        This internal method handles all HTTP communication, including request serialization,
        response validation, and error handling. It's used by all resource methods.

        Args:
            method (str): HTTP method (GET, POST, PUT, PATCH, DELETE)
            url (str): Request URL path (relative to base_url)
            data (BaseModel | Dict[str, Any], optional): Request payload. If a Pydantic
                model, will be serialized using model_dump(). If a dict, sent as-is.
            params (Dict[str, Any], optional): Query parameters to send with the request.
            headers (Dict[str, str], optional): Additional HTTP headers to send.
                Will be merged with default headers.
            timeout (float, optional): Request timeout in seconds. Overrides the
                default client timeout for this request only.
            response_model (type, optional): Expected Pydantic model class for the
                response. If provided and validation is enabled, the response will
                be validated and returned as this model type.
            is_list_response (bool): Whether the response is expected to be a list
                of response_model instances. Default: False

        Returns:
            Union[ClientResponse, Any]: If response_model is provided and validation
                is enabled, returns the validated model instance (or List of instances
                if is_list_response=True). Otherwise returns a ClientResponse with
                status information and raw data.

        Raises:
            ApiError: For HTTP error status codes (4xx, 5xx)
            ClientValidationError: If response validation fails
            httpx.TimeoutException: If the request times out
            httpx.ConnectError: If unable to connect to the server

        Note:
            This method automatically handles JSON:API response format detection
            and extraction of the 'data' attribute when appropriate.
        """
        # Prepare headers
        request_headers = {"Accept": "application/json"}
        if headers:
            request_headers.update(headers)

        # Serialize request data
        json_data = None
        if data is not None:
            if isinstance(data, BaseModel):
                json_data = data.model_dump(exclude_unset=True, by_alias=True)
            else:
                json_data = data
            request_headers["Content-Type"] = "application/json"

        # Make request
        httpx_client = self.get_httpx_client()
        response = httpx_client.request(
            method=method,
            url=url,
            params=params,
            json=json_data,
            headers=request_headers,
            timeout=timeout,
        )

        # Handle errors
        if response.status_code >= 400:
            raise ApiError(
                status_code=response.status_code,
                message=f"{method.upper()} {url} failed",
                response=response,
            )

        # Parse and validate response
        if self._validate_responses and response_model and response.content:
            try:
                response_data = response.json()

                # Handle JSON:API format
                if isinstance(response_data, dict) and "data" in response_data:
                    data = response_data.get("data")
                    meta = response_data.get("meta", {})

                    # Handle JSON:API list response for PaginatedResponse
                    if isinstance(data, list):
                        # Check if response_model is PaginatedResponse by looking for 'items' field
                        response_model_str = str(response_model)
                        if "PaginatedResponse" in response_model_str:
                            # Convert JSON:API list to PaginatedResponse format
                            items = []
                            for item in data:
                                if isinstance(item, dict) and "attributes" in item:
                                    merged = {
                                        "id": item.get("id"),
                                        **item["attributes"],
                                    }
                                    items.append(merged)
                                else:
                                    items.append(item)
                            # Build pagination info from meta
                            pagination = {
                                "next_cursor": meta.get("next_cursor"),
                                "has_more": meta.get("has_more", False),
                            }
                            paginated_data = {"items": items, "pagination": pagination}
                            adapter = TypeAdapter(response_model)
                            return adapter.validate_python(paginated_data)
                        else:
                            # Return raw list for non-PaginatedResponse models
                            adapter = TypeAdapter(List[response_model])
                            return adapter.validate_python(data)

                    # Handle single object JSON:API response
                    if isinstance(data, dict) and "attributes" in data:
                        # Merge id from data level with attributes
                        merged = {"id": data.get("id"), **data["attributes"]}
                        parsed_data = response_model.model_validate(merged)
                    elif hasattr(data, "attributes"):
                        parsed_data = response_model.model_validate(data.attributes)
                    else:
                        parsed_data = response_model.model_validate(data)
                    return parsed_data

                # Handle list responses with TypeAdapter
                if is_list_response:
                    adapter = TypeAdapter(List[response_model])
                    return adapter.validate_python(response_data)
                else:
                    # Use TypeAdapter for generic types (e.g., PaginatedResponse[T])
                    adapter = TypeAdapter(response_model)
                    return adapter.validate_python(response_data)

            except (PydanticValidationError, ValueError) as e:
                if self._validate_responses:
                    raise ClientValidationError(f"Response validation failed: {e}")

        # Return basic response wrapper
        return ClientResponse.from_httpx_response(
            response, response.json() if response.content else None
        )


# Async client class that extends AuthenticatedClient with intuitive async methods
class AsyncCredoAI(AuthenticatedClient):
    """Asynchronous client for Credo AI SDK.

    This async client provides the same high-level, type-safe interface as the
    synchronous CredoAI client, but with async/await support for concurrent operations.

    Features:
        - Full async/await support for all operations
        - Lazy authentication (performed on first request)
        - Concurrent request support with proper connection pooling
        - All the same Pydantic validation and error handling as the sync client
        - Resource-based API organization for intuitive usage

    Available Resources:
        - system: 2 async operation(s)
        - use_cases: 5 async operation(s)
        - models: 5 async operation(s)
        - vendors: 5 async operation(s)
        - questionnaires: 6 async operation(s)
        - authentication: 1 async operation(s)
        - health: 2 async operation(s)
    Example:
        >>> import asyncio
        >>> import os
        >>>
        >>> # Option 1: Using environment variables (recommended)
        >>> os.environ["CREDOAI_API_KEY"] = "your-api-key"
        >>>
        >>> async def main():
        ...     async with AsyncCredoAI() as client:
        ...         # Use resource-based operations
        ...         result = await client.system.metrics(...)
        ...         print(result.id)
        >>>
        >>> # Option 2: Using explicit arguments
        >>> async def main():
        ...     async with AsyncCredoAI(
        ...         base_url="https://api.credo.ai",
        ...         api_key="your-api-key"
        ...     ) as client:
        ...         pass  # Use client...
        >>>
        >>> asyncio.run(main())

    Authentication:
        Authentication is performed lazily on the first API call to avoid blocking
        the constructor. The client handles automatic token refresh as needed.

    Context Manager Support:
        The async client should be used as an async context manager to ensure
        proper connection cleanup:

        >>> async with AsyncCredoAI(...) as client:
        ...     result = await client.use_cases.list()
    """

    # Type annotations for resource attributes (enables IDE autocomplete)
    system: "AsyncSystemResource"
    use_cases: "AsyncUseCasesResource"
    models: "AsyncModelsResource"
    vendors: "AsyncVendorsResource"
    questionnaires: "AsyncQuestionnairesResource"
    authentication: "AsyncAuthenticationResource"
    health: "AsyncHealthResource"

    def __init__(
        self,
        base_url: Optional[str] = None,
        api_key: Optional[str] = None,
        tenant: Optional[str] = None,
        validate_responses: bool = True,
        timeout: Optional[float] = 30.0,
        **kwargs,
    ):
        """Initialize the asynchronous credoai client.

        Unlike the synchronous client, authentication is performed lazily on the first
        API call to avoid blocking the constructor. Use as an async context manager
        for proper resource cleanup.

        Configuration can be provided via constructor arguments or environment variables.
        Constructor arguments take precedence over environment variables.

        Environment Variables:
            CREDOAI_API_KEY: API key for authentication
            CREDOAI_API_URL: Base URL of the API service (without /api/v1/integration path)
            CREDOAI_TENANT: Tenant identifier for multi-tenant authentication

        Args:
            base_url (str, optional): The base URL of the API service (without path).
                The SDK automatically appends '/api/v1/integration' to this URL.
                Default: CREDOAI_API_URL env var or "https://api.credo.ai"
            api_key (str, optional): Your API key for authentication. Authentication will be
                performed on the first API call.
                Default: CREDOAI_API_KEY env var (required)
            tenant (str, optional): Your CredoAI tenant identifier.
                Default: CREDOAI_TENANT env var (required)
            validate_responses (bool): Whether to validate API responses against
                Pydantic models. Default: True
            timeout (float, optional): Default timeout for all requests in seconds.
                Default: 30.0
            **kwargs: Additional arguments passed to the underlying HTTP client.

        Example:
            >>> # Using environment variables
            >>> import os
            >>> os.environ["CREDOAI_API_KEY"] = "your-api-key"
            >>> os.environ["CREDOAI_TENANT"] = "your-tenant"
            >>> async_client = AsyncCredoAI()
            >>>
            >>> # Using explicit arguments
            >>> async_client = AsyncCredoAI(
            ...     base_url="https://api.credo.ai",
            ...     api_key="your-api-key",
            ...     tenant="your-tenant"
            ... )
            >>>
            >>> # Use as context manager (recommended)
            >>> async with async_client as client:
            ...     result = await client.use_cases.list()

        Note:
            No network calls are made during initialization. Authentication occurs
            on the first API request. Use _ensure_authenticated() to force early
            authentication if needed.
        """
        # Resolve configuration from env vars with defaults
        _base_url = (
            base_url
            if base_url is not None
            else os.environ.get(ENV_API_URL, "https://api.credo.ai")
        )
        _api_key = api_key if api_key is not None else os.environ.get(ENV_API_KEY)
        _tenant = tenant if tenant is not None else os.environ.get(ENV_TENANT)

        if not _api_key:
            raise ValueError(
                "API key is required for authentication. "
                "Provide api_key argument or set CREDOAI_API_KEY environment variable."
            )

        if not _tenant:
            raise ValueError(
                "Tenant is required. "
                "Provide tenant argument or set CREDOAI_TENANT environment variable."
            )

        # Construct full API URL with path
        full_api_url = f"{_base_url.rstrip('/')}{API_PATH}"

        # For async client, authentication will be performed on first request
        # to avoid blocking the constructor. Store auth details for later use.
        self._tenant = _tenant
        self._user_base_url = _base_url

        # Configure authentication for the underlying client
        # All requests use Bearer token format internally
        kwargs.setdefault("auth_header_name", "Authorization")
        kwargs.setdefault("prefix", "Bearer")
        kwargs.setdefault("timeout", httpx.Timeout(timeout))

        # Add tenant header to default headers
        default_headers = kwargs.get("headers", {})
        default_headers["X-Tenant"] = _tenant
        kwargs["headers"] = default_headers

        super().__init__(
            base_url=full_api_url,
            token="",  # Will be replaced with JWT on first request
            **kwargs,
        )

        # Store authentication details for async processing
        self._api_key = _api_key
        self._validate_responses = validate_responses
        self._auth_handler: Optional[CredoAIAuth] = None
        self._authenticated = False

        # Initialize resource objects for resource-based API
        self.system = AsyncSystemResource(self)
        self.use_cases = AsyncUseCasesResource(self)
        self.models = AsyncModelsResource(self)
        self.vendors = AsyncVendorsResource(self)
        self.questionnaires = AsyncQuestionnairesResource(self)
        self.authentication = AsyncAuthenticationResource(self)
        self.health = AsyncHealthResource(self)

    async def _ensure_authenticated(self):
        """Ensure the client is authenticated, performing auth if needed."""
        if self._authenticated:
            return

        if not self._api_key:
            raise AuthenticationError("No API key available")

        try:
            self._auth_handler = await create_auth_handler_async(
                base_url=self._base_url,
                auth_type="api_key",
                token=self._api_key,
                tenant=self._tenant,
                timeout=self._timeout.connect if self._timeout else 30.0,
            )

            # Update the token for the underlying client
            jwt_token = self._auth_handler._access_token
            self.token = jwt_token

            # Update headers on existing clients
            auth_header = f"Bearer {jwt_token}"
            if self._client:
                self._client.headers["Authorization"] = auth_header
            if self._async_client:
                self._async_client.headers["Authorization"] = auth_header

            self._authenticated = True

        except AuthenticationError as e:
            raise AuthenticationError(f"Async authentication failed: {e}")

    async def _make_request_async(
        self,
        method: str,
        url: str,
        data: Optional[Union[BaseModel, Dict[str, Any]]] = None,
        params: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
        timeout: Optional[float] = None,
        response_model: Optional[type] = None,
        is_list_response: bool = False,
    ) -> Union[ClientResponse, Any]:
        """Make an async HTTP request with comprehensive validation and error handling.

        This internal method handles all async HTTP communication, including request
        serialization, response validation, and error handling.

        Args:
            method (str): HTTP method (GET, POST, PUT, PATCH, DELETE)
            url (str): Request URL path (relative to base_url)
            data (BaseModel | Dict[str, Any], optional): Request payload
            params (Dict[str, Any], optional): Query parameters
            headers (Dict[str, str], optional): Additional HTTP headers
            timeout (float, optional): Request timeout in seconds
            response_model (type, optional): Expected Pydantic model class
            is_list_response (bool): Whether response is a list

        Returns:
            Union[ClientResponse, Any]: Validated model or ClientResponse

        Raises:
            ApiError: For HTTP error status codes
            ClientValidationError: If response validation fails
        """
        # Ensure authenticated before making request
        await self._ensure_authenticated()

        # Prepare headers
        request_headers = {"Accept": "application/json"}
        if headers:
            request_headers.update(headers)

        # Serialize request data
        json_data = None
        if data is not None:
            if isinstance(data, BaseModel):
                json_data = data.model_dump(exclude_unset=True, by_alias=True)
            else:
                json_data = data
            request_headers["Content-Type"] = "application/json"

        # Make request
        httpx_client = self.get_async_httpx_client()
        response = await httpx_client.request(
            method=method,
            url=url,
            params=params,
            json=json_data,
            headers=request_headers,
            timeout=timeout,
        )

        # Handle errors
        if response.status_code >= 400:
            raise ApiError(
                status_code=response.status_code,
                message=f"{method.upper()} {url} failed",
                response=response,
            )

        # Parse and validate response
        if self._validate_responses and response_model and response.content:
            try:
                response_data = response.json()

                # Handle JSON:API format
                if isinstance(response_data, dict) and "data" in response_data:
                    data = response_data.get("data")
                    meta = response_data.get("meta", {})

                    # Handle JSON:API list response for PaginatedResponse
                    if isinstance(data, list):
                        # Check if response_model is PaginatedResponse by looking for 'items' field
                        response_model_str = str(response_model)
                        if "PaginatedResponse" in response_model_str:
                            # Convert JSON:API list to PaginatedResponse format
                            items = []
                            for item in data:
                                if isinstance(item, dict) and "attributes" in item:
                                    merged = {
                                        "id": item.get("id"),
                                        **item["attributes"],
                                    }
                                    items.append(merged)
                                else:
                                    items.append(item)
                            # Build pagination info from meta
                            pagination = {
                                "next_cursor": meta.get("next_cursor"),
                                "has_more": meta.get("has_more", False),
                            }
                            paginated_data = {"items": items, "pagination": pagination}
                            adapter = TypeAdapter(response_model)
                            return adapter.validate_python(paginated_data)
                        else:
                            # Return raw list for non-PaginatedResponse models
                            adapter = TypeAdapter(List[response_model])
                            return adapter.validate_python(data)

                    # Handle single object JSON:API response
                    if isinstance(data, dict) and "attributes" in data:
                        # Merge id from data level with attributes
                        merged = {"id": data.get("id"), **data["attributes"]}
                        parsed_data = response_model.model_validate(merged)
                    elif hasattr(data, "attributes"):
                        parsed_data = response_model.model_validate(data.attributes)
                    else:
                        parsed_data = response_model.model_validate(data)
                    return parsed_data

                # Handle list responses with TypeAdapter
                if is_list_response:
                    adapter = TypeAdapter(List[response_model])
                    return adapter.validate_python(response_data)
                else:
                    adapter = TypeAdapter(response_model)
                    return adapter.validate_python(response_data)

            except (PydanticValidationError, ValueError) as e:
                if self._validate_responses:
                    raise ClientValidationError(f"Response validation failed: {e}")

        # Return basic response wrapper
        return ClientResponse.from_httpx_response(
            response, response.json() if response.content else None
        )
