"""
Async client for interacting with Crawl4AI Docker server.

This is a lightweight, standalone client that only requires httpx.
No browser, playwright, or heavy crawl4ai dependencies needed.
"""

from typing import Any, AsyncGenerator, Callable, Dict, List, Optional, Union
import httpx
import json
import logging
import asyncio
from urllib.parse import urljoin

from .models import CrawlResult
from .config import BrowserConfig, CrawlerRunConfig

logger = logging.getLogger("crawl4ai_client")


class Crawl4aiClientError(Exception):
    """Base exception for Crawl4ai client errors."""
    pass


class ConnectionError(Crawl4aiClientError):
    """Raised when connection to the Docker server fails."""
    pass


class RequestError(Crawl4aiClientError):
    """Raised when the server returns an error response."""
    pass


class Crawl4aiDockerClient:
    """Lightweight async client for Crawl4AI Docker server.

    Usage:
        async with Crawl4aiDockerClient(base_url="http://localhost:11235") as client:
            result = await client.crawl(["https://example.com"])
            print(result.raw_markdown)

    With authentication:
        async with Crawl4aiDockerClient(base_url="http://localhost:11235") as client:
            await client.authenticate("user@example.com")
            result = await client.crawl(["https://example.com"])

    With token:
        async with Crawl4aiDockerClient(
            base_url="http://localhost:11235",
            api_token="your-token"
        ) as client:
            result = await client.crawl(["https://example.com"])
    """

    def __init__(
        self,
        base_url: str = "http://localhost:11235",
        timeout: float = 30.0,
        verify_ssl: bool = True,
        api_token: Optional[str] = None,
        verbose: bool = True,
    ):
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self._verbose = verbose

        headers = {"Content-Type": "application/json"}
        if api_token:
            headers["Authorization"] = f"Bearer {api_token}"

        self._http_client = httpx.AsyncClient(
            timeout=timeout,
            verify=verify_ssl,
            headers=headers,
        )
        self._token: Optional[str] = api_token

    def _log(self, level: str, msg: str, tag: str = "") -> None:
        prefix = f"[{tag}]... " if tag else ""
        if self._verbose:
            print(f"{prefix}{msg}")
        getattr(logger, level, logger.info)(f"{prefix}{msg}")

    async def authenticate(self, email: str) -> None:
        """Authenticate with the server and store the token."""
        url = f"{self.base_url}/token"
        try:
            self._log("info", f"Authenticating with email: {email}", "AUTH")
            response = await self._http_client.post(url, json={"email": email})
            response.raise_for_status()
            data = response.json()
            self._token = data["access_token"]
            self._http_client.headers["Authorization"] = f"Bearer {self._token}"
            self._log("info", "Authentication successful", "AUTH")
        except (httpx.RequestError, httpx.HTTPStatusError) as e:
            error_msg = f"Authentication failed: {str(e)}"
            self._log("error", error_msg, "ERROR")
            raise ConnectionError(error_msg)

    async def health(self) -> Dict[str, Any]:
        """Check server health."""
        try:
            response = await self._http_client.get(f"{self.base_url}/health")
            response.raise_for_status()
            self._log("info", f"Connected to {self.base_url}", "READY")
            return response.json()
        except httpx.RequestError as e:
            self._log("error", f"Server unreachable: {str(e)}", "ERROR")
            raise ConnectionError(f"Cannot connect to server: {str(e)}")

    async def _request(self, method: str, endpoint: str, **kwargs) -> httpx.Response:
        """Make an HTTP request with error handling."""
        url = f"{self.base_url}{endpoint}"
        try:
            response = await self._http_client.request(method, url, **kwargs)
            response.raise_for_status()
            return response
        except httpx.TimeoutException as e:
            raise ConnectionError(f"Request timed out: {str(e)}")
        except httpx.RequestError as e:
            raise ConnectionError(f"Failed to connect: {str(e)}")
        except httpx.HTTPStatusError as e:
            error_msg = str(e)
            if "application/json" in e.response.headers.get("content-type", ""):
                detail = e.response.json().get("detail", str(e))
                error_msg = f"Server error {e.response.status_code}: {detail}"
            raise RequestError(error_msg)

    def _prepare_request(
        self,
        urls: List[str],
        browser_config: Optional[BrowserConfig] = None,
        crawler_config: Optional[CrawlerRunConfig] = None,
        crawler_configs: Optional[List[CrawlerRunConfig]] = None,
        extra: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """Prepare request payload."""
        data: Dict[str, Any] = {
            "urls": urls,
            "browser_config": browser_config.dump() if browser_config else {},
            "crawler_config": crawler_config.dump() if crawler_config else {},
        }
        if crawler_configs:
            data["crawler_configs"] = [cc.dump() for cc in crawler_configs]
        if extra:
            data.update(extra)
        return data

    async def crawl(
        self,
        urls: List[str],
        browser_config: Optional[BrowserConfig] = None,
        crawler_config: Optional[CrawlerRunConfig] = None,
        crawler_configs: Optional[List[CrawlerRunConfig]] = None,
    ) -> Union[CrawlResult, List[CrawlResult]]:
        """Crawl one or more URLs.

        Args:
            urls: List of URLs to crawl.
            browser_config: Browser settings.
            crawler_config: Single crawler config (applied to all URLs).
            crawler_configs: List of per-URL crawler configs (for arun_many).

        Returns:
            Single CrawlResult for one URL, or list for multiple.
        """
        await self.health()
        data = self._prepare_request(urls, browser_config, crawler_config, crawler_configs)

        self._log("info", f"Crawling {len(urls)} URLs", "CRAWL")
        response = await self._request("POST", "/crawl", json=data)
        result_data = response.json()

        results = [CrawlResult(**r) for r in result_data.get("results", [])]
        self._log("info", f"Crawl completed with {len(results)} results", "COMPLETE")
        return results[0] if len(results) == 1 else results

    async def crawl_stream(
        self,
        urls: List[str],
        browser_config: Optional[BrowserConfig] = None,
        crawler_config: Optional[CrawlerRunConfig] = None,
    ) -> AsyncGenerator[CrawlResult, None]:
        """Crawl URLs with streaming results.

        Args:
            urls: List of URLs to crawl.
            browser_config: Browser settings.
            crawler_config: Crawler config (stream=True is set automatically).

        Yields:
            CrawlResult for each completed URL.
        """
        if crawler_config is None:
            crawler_config = CrawlerRunConfig(stream=True)
        else:
            crawler_config.stream = True

        data = self._prepare_request(urls, browser_config, crawler_config)

        self._log("info", f"Streaming crawl for {len(urls)} URLs", "STREAM")
        async with self._http_client.stream("POST", f"{self.base_url}/crawl/stream", json=data) as response:
            response.raise_for_status()
            async for line in response.aiter_lines():
                if not line.strip():
                    continue
                result = json.loads(line)
                if result.get("status") == "completed":
                    continue
                if "error" in result:
                    self._log("error", f"Error: {result['error']}", "STREAM")
                    continue
                yield CrawlResult(**result)

    async def get_markdown(
        self,
        url: str,
        content_filter: str = "fit",
        query: Optional[str] = None,
    ) -> str:
        """Get markdown content for a URL via the /md endpoint.

        Args:
            url: URL to fetch.
            content_filter: Filter type — "fit", "raw", "bm25", or "llm".
            query: Query string for BM25/LLM filters.

        Returns:
            Markdown content as a string.
        """
        data = {"url": url, "f": content_filter}
        if query:
            data["q"] = query
        response = await self._request("POST", "/md", json=data)
        return response.json().get("result", "")

    async def screenshot(self, url: str, wait_for: float = 2.0) -> str:
        """Take a screenshot of a URL.

        Returns:
            Base64-encoded screenshot string.
        """
        data = {"url": url, "screenshot_wait_for": wait_for}
        response = await self._request("POST", "/screenshot", json=data)
        return response.json().get("result", "")

    async def get_schema(self) -> Dict[str, Any]:
        """Retrieve the server's configuration schemas."""
        response = await self._request("GET", "/schema")
        return response.json()

    async def close(self) -> None:
        """Close the HTTP client session."""
        self._log("info", "Closing client", "CLOSE")
        await self._http_client.aclose()

    async def __aenter__(self) -> "Crawl4aiDockerClient":
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
        await self.close()
