"""
HTTP transport layer for AMI SDK.

Handles HTTP requests with retry logic, timeout handling, and error mapping.
Provides both sync (HTTPTransport) and async (AsyncHTTPTransport) variants.
"""

import time
import asyncio
import random
from enum import Enum
from typing import Optional, Dict, Any
import httpx

from .config import AMIConfig
from .errors import (
    AMIError, AMINetworkError, AMITimeoutError,
    map_http_error
)


class RetryPolicy(str, Enum):
    """Retry policy for HTTP requests."""
    NEVER = "never"  # Never retry
    IDEMPOTENT = "idempotent"  # Only retry idempotent requests (GET, PUT, DELETE)
    ALWAYS = "always"  # Always retry (use with caution)


class HTTPTransport:
    """
    HTTP transport layer with connection pooling, retries, and error handling.

    Args:
        config: AMI SDK configuration

    Example:
        with HTTPTransport(config) as transport:
            response = transport.get("/workspaces/prod/projects/api")
    """

    def __init__(self, config: AMIConfig):
        """Initialize HTTP transport with configuration."""
        self.config = config
        self.base_url = config.base_url

        # Create httpx client with connection pooling and timeout
        self.client = httpx.Client(
            base_url=self.base_url,
            timeout=httpx.Timeout(config.timeout_s),
            headers=self._build_base_headers(),
        )

    def _build_base_headers(self) -> Dict[str, str]:
        """Build base headers from config (auth + tenancy)."""
        headers = {}

        # Auth header (always required)
        if self.config.api_key:
            headers["Authorization"] = f"AMI-Key {self.config.api_key}"

        # Tenancy headers (IDs override slugs)
        if self.config.org_id:
            headers["X-AMI-Org-Id"] = self.config.org_id
        elif self.config.org:
            headers["X-AMI-Org"] = self.config.org

        if self.config.team_id:
            headers["X-AMI-Team-Id"] = self.config.team_id
        elif self.config.team:
            headers["X-AMI-Team"] = self.config.team

        if self.config.workspace_id:
            headers["X-AMI-Workspace-Id"] = self.config.workspace_id

        if self.config.project_id:
            headers["X-AMI-Project-Id"] = self.config.project_id

        return headers

    def _build_headers(self, additional: Optional[Dict[str, str]] = None) -> Dict[str, str]:
        """
        Build request headers with optional additional headers.

        Args:
            additional: Additional headers to merge (e.g., idempotency key)

        Returns:
            Complete headers dict
        """
        headers = self._build_base_headers()
        if additional:
            headers.update(additional)
        return headers

    def _should_retry(
        self,
        retry_policy: RetryPolicy,
        method: str,
        exception: Optional[Exception] = None,
        response: Optional[httpx.Response] = None,
    ) -> bool:
        """
        Determine if request should be retried.

        Args:
            retry_policy: Retry policy for this request
            method: HTTP method
            exception: Exception raised (if any)
            response: HTTP response (if any)

        Returns:
            True if should retry, False otherwise
        """
        # Never retry
        if retry_policy == RetryPolicy.NEVER:
            return False

        # Always retry network/timeout errors
        if exception:
            if isinstance(exception, (httpx.ConnectError, httpx.TimeoutException)):
                return True
            return False

        # Retry on 5xx server errors
        if response and 500 <= response.status_code < 600:
            return True

        # For 4xx errors, don't retry (client error)
        if response and 400 <= response.status_code < 500:
            return False

        # For idempotent policy, only retry safe methods
        if retry_policy == RetryPolicy.IDEMPOTENT:
            return method.upper() in ("GET", "HEAD", "OPTIONS", "PUT", "DELETE")

        # For always policy, retry everything (except 4xx)
        return True

    def _calculate_backoff(self, attempt: int) -> float:
        """
        Calculate exponential backoff with jitter.

        Args:
            attempt: Retry attempt number (0-indexed)

        Returns:
            Sleep duration in seconds
        """
        # Exponential backoff: 2^attempt * base_delay
        base_delay = 0.5  # 500ms
        max_delay = 10.0  # 10 seconds

        delay = min(base_delay * (2 ** attempt), max_delay)

        # Add jitter (±25%)
        jitter = delay * 0.25 * (2 * random.random() - 1)
        return max(0.1, delay + jitter)

    def _make_request(
        self,
        method: str,
        path: str,
        *,
        headers: Optional[Dict[str, str]] = None,
        retry_policy: RetryPolicy = RetryPolicy.NEVER,
        **kwargs: Any,
    ) -> Any:
        """
        Make HTTP request with retry logic and error handling.

        Args:
            method: HTTP method (GET, POST, etc.)
            path: Request path (relative to base_url)
            headers: Additional headers
            retry_policy: Retry policy for this request
            **kwargs: Additional arguments passed to httpx

        Returns:
            Parsed JSON response

        Raises:
            AMIError subclass on error
        """
        # Build full URL
        url = f"{self.base_url}{path}"

        # Merge headers
        request_headers = self._build_headers(headers)

        # Retry loop
        last_exception = None
        for attempt in range(self.config.max_retries + 1):
            try:
                # Make request
                response = self.client.request(
                    method=method,
                    url=url,
                    headers=request_headers,
                    **kwargs,
                )

                # Check for HTTP errors
                if response.status_code >= 400:
                    # Check if should retry
                    if attempt < self.config.max_retries and self._should_retry(
                        retry_policy, method, response=response
                    ):
                        # Backoff before retry
                        time.sleep(self._calculate_backoff(attempt))
                        continue

                    # Map to SDK exception
                    raise map_http_error(response)

                # Success - return JSON response
                try:
                    return response.json()
                except Exception:
                    # If response has no JSON body, return None
                    return None

            except httpx.ConnectError as e:
                # Network error
                last_exception = AMINetworkError(str(e))

                if attempt < self.config.max_retries and self._should_retry(
                    retry_policy, method, exception=e
                ):
                    time.sleep(self._calculate_backoff(attempt))
                    continue

                raise last_exception

            except httpx.TimeoutException as e:
                # Timeout error
                last_exception = AMITimeoutError(f"Request timeout: {e}")

                if attempt < self.config.max_retries and self._should_retry(
                    retry_policy, method, exception=e
                ):
                    time.sleep(self._calculate_backoff(attempt))
                    continue

                raise last_exception

            except AMIError:
                # AMI errors are already mapped - just re-raise
                raise

            except Exception as e:
                # Unexpected error
                raise AMIError(f"Unexpected error: {e}") from e

        # Should not reach here, but raise last exception if we do
        if last_exception:
            raise last_exception
        raise AMIError("Request failed after retries")

    def get(
        self,
        path: str,
        *,
        headers: Optional[Dict[str, str]] = None,
        retry_policy: RetryPolicy = RetryPolicy.IDEMPOTENT,
        **kwargs: Any,
    ) -> Any:
        """
        Make GET request.

        Args:
            path: Request path
            headers: Additional headers
            retry_policy: Retry policy (default: IDEMPOTENT)
            **kwargs: Additional arguments

        Returns:
            Parsed JSON response
        """
        return self._make_request("GET", path, headers=headers, retry_policy=retry_policy, **kwargs)

    def post(
        self,
        path: str,
        *,
        json: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
        retry_policy: RetryPolicy = RetryPolicy.NEVER,
        **kwargs: Any,
    ) -> Any:
        """
        Make POST request.

        Args:
            path: Request path
            json: JSON body
            headers: Additional headers
            retry_policy: Retry policy (default: NEVER - not idempotent)
            **kwargs: Additional arguments

        Returns:
            Parsed JSON response
        """
        return self._make_request("POST", path, json=json, headers=headers, retry_policy=retry_policy, **kwargs)

    def put(
        self,
        path: str,
        *,
        json: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
        retry_policy: RetryPolicy = RetryPolicy.IDEMPOTENT,
        **kwargs: Any,
    ) -> Any:
        """
        Make PUT request.

        Args:
            path: Request path
            json: JSON body
            headers: Additional headers
            retry_policy: Retry policy (default: IDEMPOTENT)
            **kwargs: Additional arguments

        Returns:
            Parsed JSON response
        """
        return self._make_request("PUT", path, json=json, headers=headers, retry_policy=retry_policy, **kwargs)

    def patch(
        self,
        path: str,
        *,
        json: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
        retry_policy: RetryPolicy = RetryPolicy.NEVER,
        **kwargs: Any,
    ) -> Any:
        """
        Make PATCH request.

        Args:
            path: Request path
            json: JSON body
            headers: Additional headers
            retry_policy: Retry policy (default: NEVER - not idempotent)
            **kwargs: Additional arguments

        Returns:
            Parsed JSON response
        """
        return self._make_request("PATCH", path, json=json, headers=headers, retry_policy=retry_policy, **kwargs)

    def delete(
        self,
        path: str,
        *,
        headers: Optional[Dict[str, str]] = None,
        retry_policy: RetryPolicy = RetryPolicy.IDEMPOTENT,
        **kwargs: Any,
    ) -> Any:
        """
        Make DELETE request.

        Args:
            path: Request path
            headers: Additional headers
            retry_policy: Retry policy (default: IDEMPOTENT)
            **kwargs: Additional arguments

        Returns:
            Parsed JSON response
        """
        return self._make_request("DELETE", path, headers=headers, retry_policy=retry_policy, **kwargs)

    def close(self) -> None:
        """Close HTTP client and clean up resources."""
        if hasattr(self, 'client'):
            self.client.close()

    def __enter__(self) -> "HTTPTransport":
        """Context manager entry."""
        return self

    def __exit__(self, exc_type, exc_val, exc_tb) -> None:
        """Context manager exit - close client."""
        self.close()


class AsyncHTTPTransport:
    """
    Async HTTP transport layer with connection pooling, retries, and error handling.

    Args:
        config: AMI SDK configuration

    Example:
        async with AsyncHTTPTransport(config) as transport:
            response = await transport.get("/workspaces/prod/projects/api")
    """

    def __init__(self, config: AMIConfig, client: Optional[httpx.AsyncClient] = None):
        """
        Initialize async HTTP transport with configuration.

        Args:
            config: AMI SDK configuration
            client: Optional pre-configured httpx.AsyncClient (for testing with ASGITransport)
        """
        self.config = config
        self.base_url = config.base_url

        # Use provided client (for testing) or create new one
        if client:
            self.client = client
            self._owns_client = False  # Don't close client we don't own
        else:
            # Create httpx async client with connection pooling and timeout
            self.client = httpx.AsyncClient(
                base_url=self.base_url,
                timeout=httpx.Timeout(config.timeout_s),
                headers=self._build_base_headers(),
            )
            self._owns_client = True

    def _build_base_headers(self) -> Dict[str, str]:
        """Build base headers from config (auth + tenancy)."""
        headers = {}

        # Auth header (always required)
        if self.config.api_key:
            headers["Authorization"] = f"AMI-Key {self.config.api_key}"

        # Tenancy headers (IDs override slugs)
        if self.config.org_id:
            headers["X-AMI-Org-Id"] = self.config.org_id
        elif self.config.org:
            headers["X-AMI-Org"] = self.config.org

        if self.config.team_id:
            headers["X-AMI-Team-Id"] = self.config.team_id
        elif self.config.team:
            headers["X-AMI-Team"] = self.config.team

        if self.config.workspace_id:
            headers["X-AMI-Workspace-Id"] = self.config.workspace_id

        if self.config.project_id:
            headers["X-AMI-Project-Id"] = self.config.project_id

        return headers

    def _build_headers(self, additional: Optional[Dict[str, str]] = None) -> Dict[str, str]:
        """
        Build request headers with optional additional headers.

        Merges base headers with additional headers, with additional taking precedence.
        """
        headers = self._build_base_headers().copy()
        if additional:
            headers.update(additional)
        return headers

    def _should_retry(
        self,
        retry_policy: RetryPolicy,
        method: str,
        *,
        response: Optional[httpx.Response] = None,
        exception: Optional[Exception] = None,
    ) -> bool:
        """Determine if request should be retried based on policy."""
        if retry_policy == RetryPolicy.NEVER:
            return False

        if retry_policy == RetryPolicy.ALWAYS:
            return True

        # IDEMPOTENT policy
        if method.upper() in ["GET", "PUT", "DELETE", "HEAD", "OPTIONS"]:
            # Retry on server errors (5xx) or network errors
            if response and 500 <= response.status_code < 600:
                return True
            if exception and isinstance(exception, (httpx.ConnectError, httpx.TimeoutException)):
                return True

        return False

    def _calculate_backoff(self, attempt: int) -> float:
        """Calculate exponential backoff with jitter."""
        # Exponential backoff: min(0.5 * 2^attempt, 10) + jitter
        delay = min(0.5 * (2 ** attempt), 10.0)
        jitter = delay * 0.25 * (2 * random.random() - 1)  # +/- 25% jitter
        return max(0.1, delay + jitter)

    async def _make_request(
        self,
        method: str,
        path: str,
        *,
        headers: Optional[Dict[str, str]] = None,
        retry_policy: RetryPolicy = RetryPolicy.IDEMPOTENT,
        **kwargs: Any,
    ) -> Any:
        """
        Make HTTP request with retry logic.

        Args:
            method: HTTP method
            path: Request path (will be joined with base_url)
            headers: Additional headers
            retry_policy: Retry policy
            **kwargs: Additional arguments passed to httpx

        Returns:
            Parsed JSON response

        Raises:
            AMIError: On HTTP error or network failure
        """
        # Build full URL
        url = f"{self.base_url}{path}"

        # Merge headers
        request_headers = self._build_headers(headers)

        # Retry loop
        last_exception = None
        for attempt in range(self.config.max_retries + 1):
            try:
                # Make request
                response = await self.client.request(
                    method=method,
                    url=url,
                    headers=request_headers,
                    **kwargs,
                )

                # Check for HTTP errors
                if response.status_code >= 400:
                    # Check if should retry
                    if attempt < self.config.max_retries and self._should_retry(
                        retry_policy, method, response=response
                    ):
                        # Backoff before retry
                        await asyncio.sleep(self._calculate_backoff(attempt))
                        continue

                    # Map to SDK exception
                    raise map_http_error(response)

                # Success - return JSON response
                try:
                    return response.json()
                except Exception:
                    # If response has no JSON body, return None
                    return None

            except httpx.ConnectError as e:
                # Network error
                last_exception = AMINetworkError(str(e))

                if attempt < self.config.max_retries and self._should_retry(
                    retry_policy, method, exception=e
                ):
                    await asyncio.sleep(self._calculate_backoff(attempt))
                    continue

                raise last_exception

            except httpx.TimeoutException as e:
                # Timeout error
                last_exception = AMITimeoutError(str(e))

                if attempt < self.config.max_retries and self._should_retry(
                    retry_policy, method, exception=e
                ):
                    await asyncio.sleep(self._calculate_backoff(attempt))
                    continue

                raise last_exception

            except AMIError:
                # AMI errors are already mapped - just re-raise
                raise

            except Exception as e:
                # Unexpected error
                raise AMIError(f"Unexpected error: {e}") from e

        # Should not reach here, but raise last exception if we do
        if last_exception:
            raise last_exception
        raise AMIError("Request failed after retries")

    async def get(
        self,
        path: str,
        *,
        headers: Optional[Dict[str, str]] = None,
        retry_policy: RetryPolicy = RetryPolicy.IDEMPOTENT,
        **kwargs: Any,
    ) -> Any:
        """Make GET request."""
        return await self._make_request("GET", path, headers=headers, retry_policy=retry_policy, **kwargs)

    async def post(
        self,
        path: str,
        *,
        json: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
        retry_policy: RetryPolicy = RetryPolicy.NEVER,
        **kwargs: Any,
    ) -> Any:
        """Make POST request."""
        return await self._make_request("POST", path, json=json, headers=headers, retry_policy=retry_policy, **kwargs)

    async def put(
        self,
        path: str,
        *,
        json: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
        retry_policy: RetryPolicy = RetryPolicy.IDEMPOTENT,
        **kwargs: Any,
    ) -> Any:
        """Make PUT request."""
        return await self._make_request("PUT", path, json=json, headers=headers, retry_policy=retry_policy, **kwargs)

    async def patch(
        self,
        path: str,
        *,
        json: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
        retry_policy: RetryPolicy = RetryPolicy.NEVER,
        **kwargs: Any,
    ) -> Any:
        """Make PATCH request."""
        return await self._make_request("PATCH", path, json=json, headers=headers, retry_policy=retry_policy, **kwargs)

    async def delete(
        self,
        path: str,
        *,
        headers: Optional[Dict[str, str]] = None,
        retry_policy: RetryPolicy = RetryPolicy.IDEMPOTENT,
        **kwargs: Any,
    ) -> Any:
        """Make DELETE request."""
        return await self._make_request("DELETE", path, headers=headers, retry_policy=retry_policy, **kwargs)

    async def close(self) -> None:
        """Close HTTP client and clean up resources."""
        if hasattr(self, 'client') and self._owns_client:
            await self.client.aclose()

    async def __aenter__(self) -> "AsyncHTTPTransport":
        """Async context manager entry."""
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
        """Async context manager exit - close client."""
        await self.close()
