"""Undetecta API Client.

Main client class for interacting with the Undetecta API.
"""

from __future__ import annotations

from typing import Any

import httpx

from ._transport import TransportConfig, build_url, request_with_retry
from .errors import (
    ApiKeyError,
    HttpError,
    NotFoundError,
    UndetectaError,
    parse_error_response,
)
from .types import (
    HealthResponse,
    ScrapeFormat,
    ScrapeJobResponse,
    ScrapeOptions,
    SearchEngine,
    SearchJobResponse,
    SearchOptions,
    SearchSource,
)

DEFAULT_BASE_URL = "https://api.undetecta.com"
DEFAULT_TIMEOUT = 60000
DEFAULT_MAX_RETRIES = 3
DEFAULT_USER_AGENT = "undetecta-python/0.1.0"


class UndetectaClient:
    """Undetecta API client.

    Example:
        ```python
        import asyncio
        from undetecta import UndetectaClient

        async def main():
            client = UndetectaClient(api_key="your-api-key")
            result = await client.scrape(url="https://example.com")
            print(result.markdown)

        asyncio.run(main())
        ```
    """

    def __init__(
        self,
        *,
        api_key: str,
        base_url: str = DEFAULT_BASE_URL,
        timeout: int = DEFAULT_TIMEOUT,
        max_retries: int = DEFAULT_MAX_RETRIES,
        user_agent: str | None = None,
    ) -> None:
        """Initialize the Undetecta client.

        Args:
            api_key: API key for authentication
            base_url: Base URL for the API (default: https://api.undetecta.com)
            timeout: Request timeout in milliseconds (default: 60000)
            max_retries: Maximum number of retries (default: 3)
            user_agent: Custom user agent string

        Raises:
            ValueError: If api_key is not provided
        """
        if not api_key:
            raise ValueError("API key is required")

        self.api_key = api_key
        self.base_url = base_url
        self.timeout = timeout
        self.max_retries = max_retries

        self._config = TransportConfig(
            base_url=self.base_url,
            api_key=self.api_key,
            timeout=self.timeout,
            max_retries=self.max_retries,
            user_agent=user_agent or DEFAULT_USER_AGENT,
        )
        self._client = httpx.AsyncClient()

    async def scrape(
        self,
        *,
        url: str,
        formats: list[ScrapeFormat | str] | None = None,
        **kwargs: Any,
    ) -> ScrapeJobResponse:
        """Scrape a URL and return the results.

        Args:
            url: URL to scrape
            formats: Output formats (e.g., ['markdown', 'screenshot'])
            **kwargs: Additional scrape options

        Returns:
            Scrape job response

        Raises:
            ApiKeyError: If API key is invalid
            ValidationError: If request validation fails
            RateLimitError: If rate limit is exceeded
            NotFoundError: If resource is not found
            NetworkError: On network errors
            TimeoutError: On request timeout

        Example:
            ```python
            result = await client.scrape(
                url="https://example.com",
                formats=["markdown"],
            )
            print(result.markdown)
            ```
        """
        # Convert string formats to ScrapeFormat enum
        converted_formats: list[ScrapeFormat] | None = None
        if formats:
            converted_formats = [
                f if isinstance(f, ScrapeFormat) else ScrapeFormat(f) for f in formats
            ]
        options = ScrapeOptions(url=url, formats=converted_formats, **kwargs)
        data = await self._request(
            "/v1/scrape",
            options.model_dump(by_alias=True, exclude_none=True),
        )
        return ScrapeJobResponse(**data)

    async def search(
        self,
        *,
        query: str,
        engine: SearchEngine | str | None = None,
        limit: int | None = None,
        sources: list[SearchSource | str] | None = None,
        **kwargs: Any,
    ) -> SearchJobResponse:
        """Perform a web search and return the results.

        Args:
            query: Search query
            engine: Search engine ('google' or 'duckduckgo')
            limit: Maximum number of results
            sources: Sources to search ('web', 'news', 'images')
            **kwargs: Additional search options

        Returns:
            Search job response

        Raises:
            ApiKeyError: If API key is invalid
            ValidationError: If request validation fails
            RateLimitError: If rate limit is exceeded
            NotFoundError: If resource is not found
            NetworkError: On network errors
            TimeoutError: On request timeout

        Example:
            ```python
            result = await client.search(
                query="web scraping tools",
                limit=10,
            )
            print(result.web)
            ```
        """
        # Convert string engine to SearchEngine enum
        converted_engine: SearchEngine | None = None
        if engine:
            converted_engine = engine if isinstance(engine, SearchEngine) else SearchEngine(engine)

        # Convert string sources to SearchSource enum
        converted_sources: list[SearchSource] | None = None
        if sources:
            converted_sources = [
                s if isinstance(s, SearchSource) else SearchSource(s) for s in sources
            ]

        options = SearchOptions(
            query=query,
            engine=converted_engine,
            limit=limit,
            sources=converted_sources,
            **kwargs,
        )
        data = await self._request(
            "/v1/search",
            options.model_dump(by_alias=True, exclude_none=True),
        )
        return SearchJobResponse(**data)

    async def health(self) -> HealthResponse:
        """Check the API health status.

        Returns:
            Health response with status and timestamp

        Raises:
            NetworkError: On network errors
            TimeoutError: On request timeout

        Example:
            ```python
            health = await client.health()
            print(health.status)  # 'ok'
            ```
        """
        data = await self._request("/v1/health")
        return HealthResponse(**data)

    async def _request(self, path: str, body: dict[str, Any] | None = None) -> Any:
        """Make a request to the API.

        Args:
            path: API endpoint path
            body: Request body

        Returns:
            Response data

        Raises:
            UndetectaError: On API errors
        """
        url = build_url(self._config.base_url, path)

        try:
            return await request_with_retry(
                client=self._client,
                method="POST",
                url=url,
                config=self._config,
                json_data=body,
            )
        except HttpError as e:
            raise self._handle_http_error(e)
        except Exception as e:
            raise UndetectaError("UNKNOWN_ERROR", str(e))

    def _handle_http_error(self, error: HttpError) -> UndetectaError:
        """Handle HTTP errors and convert to specific error types.

        Args:
            error: HTTP error

        Returns:
            Specific error type
        """
        parsed = parse_error_response(error.data)

        match error.status_code:
            case 400:
                return parsed if parsed.code == "VALIDATION_ERROR" else parsed
            case 401 | 403:
                return ApiKeyError(parsed.message)
            case 404:
                return NotFoundError(parsed.message)
            case 429:
                return parsed  # RateLimitError from parse_error_response
            case 500 | 502 | 503 | 504:
                return UndetectaError(
                    parsed.code,
                    "Server error, please try again later",
                    error.status_code,
                )
            case _:
                return parsed

    async def __aenter__(self) -> UndetectaClient:
        """Enter async context manager.

        Returns:
            The client instance
        """
        return self

    async def __aexit__(
        self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: Any,
    ) -> None:
        """Exit async context manager.

        Args:
            exc_type: Exception type
            exc_val: Exception value
            exc_tb: Exception traceback
        """
        await self.close()

    async def close(self) -> None:
        """Close the HTTP client.

        This should be called when done using the client, or use the client
        as an async context manager.
        """
        await self._client.aclose()
