"""
Async HTTP Client

Provides async HTTP client to replace synchronous requests library in FastAPI endpoints.
This prevents blocking the event loop and improves concurrency.

Usage:
    from web.backend.core.http_client import http_client, get_http_client

    # In async endpoint
    async def my_endpoint():
        async with get_http_client() as client:
            response = await client.get("https://api.example.com/data")
            return response.json()

    # Or use the singleton
    response = await http_client.get("https://api.example.com/data")
"""

import httpx
from typing import Optional, Dict, Any, Union
from contextlib import asynccontextmanager
import asyncio

from .config import settings
from .exceptions import NetworkError, ServiceError


# Default timeouts
DEFAULT_TIMEOUT = httpx.Timeout(
    connect=10.0,
    read=30.0,
    write=10.0,
    pool=10.0
)

# Longer timeout for slow operations
LONG_TIMEOUT = httpx.Timeout(
    connect=10.0,
    read=120.0,
    write=30.0,
    pool=10.0
)


class AsyncHTTPClient:
    """
    Async HTTP client wrapper with retry logic and error handling.

    Features:
    - Connection pooling
    - Automatic retries
    - Timeout handling
    - Error conversion to custom exceptions
    """

    def __init__(
        self,
        base_url: Optional[str] = None,
        timeout: Optional[httpx.Timeout] = None,
        headers: Optional[Dict[str, str]] = None,
        max_retries: int = 3
    ):
        self.base_url = base_url
        self.timeout = timeout or DEFAULT_TIMEOUT
        self.headers = headers or {}
        self.max_retries = max_retries
        self._client: Optional[httpx.AsyncClient] = None

    async def _get_client(self) -> httpx.AsyncClient:
        """Get or create the async client"""
        if self._client is None or self._client.is_closed:
            client_kwargs = {
                "timeout": self.timeout,
                "headers": self.headers,
                "follow_redirects": True,
                "limits": httpx.Limits(
                    max_keepalive_connections=20,
                    max_connections=100,
                    keepalive_expiry=30.0
                )
            }
            # Only add base_url if it's not None
            if self.base_url is not None:
                client_kwargs["base_url"] = self.base_url

            self._client = httpx.AsyncClient(**client_kwargs)
        return self._client

    async def close(self):
        """Close the client connection"""
        if self._client and not self._client.is_closed:
            await self._client.aclose()
            self._client = None

    async def _request(
        self,
        method: str,
        url: str,
        **kwargs
    ) -> httpx.Response:
        """
        Make an HTTP request with retry logic.

        Args:
            method: HTTP method (GET, POST, etc.)
            url: URL to request
            **kwargs: Additional arguments for httpx

        Returns:
            httpx.Response

        Raises:
            NetworkError: On network failures
            ServiceError: On HTTP errors
        """
        client = await self._get_client()
        last_error = None

        for attempt in range(self.max_retries):
            try:
                response = await client.request(method, url, **kwargs)

                # Raise for 4xx/5xx status codes
                if response.status_code >= 400:
                    if response.status_code >= 500:
                        raise ServiceError(
                            f"Server error: {response.status_code}",
                            {"url": url, "status": response.status_code}
                        )
                    # Don't retry client errors
                    return response

                return response

            except httpx.ConnectError as e:
                last_error = NetworkError(
                    f"Connection failed: {e}",
                    {"url": url, "attempt": attempt + 1}
                )
            except httpx.TimeoutException as e:
                last_error = NetworkError(
                    f"Request timed out: {e}",
                    {"url": url, "attempt": attempt + 1}
                )
            except httpx.HTTPStatusError as e:
                last_error = ServiceError(
                    f"HTTP error: {e}",
                    {"url": url, "status": e.response.status_code}
                )
                # Don't retry client errors
                if e.response.status_code < 500:
                    raise last_error
            except Exception as e:
                last_error = NetworkError(
                    f"Request failed: {e}",
                    {"url": url, "attempt": attempt + 1}
                )

            # Wait before retry (exponential backoff)
            if attempt < self.max_retries - 1:
                await asyncio.sleep(2 ** attempt)

        raise last_error

    # Convenience methods

    async def get(
        self,
        url: str,
        params: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
        timeout: Optional[float] = None,
        **kwargs
    ) -> httpx.Response:
        """Make GET request"""
        request_timeout = httpx.Timeout(timeout) if timeout else None
        return await self._request(
            "GET",
            url,
            params=params,
            headers=headers,
            timeout=request_timeout,
            **kwargs
        )

    async def post(
        self,
        url: str,
        data: Optional[Dict[str, Any]] = None,
        json: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
        timeout: Optional[float] = None,
        **kwargs
    ) -> httpx.Response:
        """Make POST request"""
        request_timeout = httpx.Timeout(timeout) if timeout else None
        return await self._request(
            "POST",
            url,
            data=data,
            json=json,
            headers=headers,
            timeout=request_timeout,
            **kwargs
        )

    async def put(
        self,
        url: str,
        data: Optional[Dict[str, Any]] = None,
        json: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
        **kwargs
    ) -> httpx.Response:
        """Make PUT request"""
        return await self._request(
            "PUT",
            url,
            data=data,
            json=json,
            headers=headers,
            **kwargs
        )

    async def delete(
        self,
        url: str,
        headers: Optional[Dict[str, str]] = None,
        **kwargs
    ) -> httpx.Response:
        """Make DELETE request"""
        return await self._request(
            "DELETE",
            url,
            headers=headers,
            **kwargs
        )

    async def head(
        self,
        url: str,
        headers: Optional[Dict[str, str]] = None,
        **kwargs
    ) -> httpx.Response:
        """Make HEAD request"""
        return await self._request(
            "HEAD",
            url,
            headers=headers,
            **kwargs
        )


# Singleton client for general use
http_client = AsyncHTTPClient()


@asynccontextmanager
async def get_http_client(
    base_url: Optional[str] = None,
    timeout: Optional[httpx.Timeout] = None,
    headers: Optional[Dict[str, str]] = None
):
    """
    Context manager for creating a temporary HTTP client.

    Usage:
        async with get_http_client(base_url="https://api.example.com") as client:
            response = await client.get("/endpoint")
    """
    client = AsyncHTTPClient(
        base_url=base_url,
        timeout=timeout,
        headers=headers
    )
    try:
        yield client
    finally:
        await client.close()


# Helper functions for common patterns

async def fetch_json(
    url: str,
    params: Optional[Dict[str, Any]] = None,
    headers: Optional[Dict[str, str]] = None
) -> Dict[str, Any]:
    """
    Fetch JSON from URL.

    Args:
        url: URL to fetch
        params: Query parameters
        headers: Request headers

    Returns:
        Parsed JSON response
    """
    response = await http_client.get(url, params=params, headers=headers)
    return response.json()


async def post_json(
    url: str,
    data: Dict[str, Any],
    headers: Optional[Dict[str, str]] = None
) -> Dict[str, Any]:
    """
    POST JSON to URL and return JSON response.

    Args:
        url: URL to post to
        data: JSON data to send
        headers: Request headers

    Returns:
        Parsed JSON response
    """
    response = await http_client.post(url, json=data, headers=headers)
    return response.json()


async def check_url_accessible(url: str, timeout: float = 5.0) -> bool:
    """
    Check if a URL is accessible.

    Args:
        url: URL to check
        timeout: Request timeout

    Returns:
        True if URL is accessible
    """
    try:
        response = await http_client.head(url, timeout=timeout)
        return response.status_code < 400
    except Exception:
        return False


async def download_file(
    url: str,
    save_path: str,
    headers: Optional[Dict[str, str]] = None,
    chunk_size: int = 8192
) -> int:
    """
    Download file from URL.

    Args:
        url: URL to download
        save_path: Path to save file
        headers: Request headers
        chunk_size: Size of chunks for streaming

    Returns:
        Number of bytes downloaded
    """
    from pathlib import Path

    client = await http_client._get_client()
    total_bytes = 0

    async with client.stream("GET", url, headers=headers) as response:
        response.raise_for_status()

        # Ensure directory exists
        Path(save_path).parent.mkdir(parents=True, exist_ok=True)

        with open(save_path, 'wb') as f:
            async for chunk in response.aiter_bytes(chunk_size):
                f.write(chunk)
                total_bytes += len(chunk)

    return total_bytes
