"""Low-level HTTP client with retries, auth, and envelope unwrapping."""

import time
from typing import Any, Dict, List, Optional, Tuple, Union

import httpx

from .errors import CoinCircuitApiError, CoinCircuitAuthError, CoinCircuitError
from .pagination import PaginatedResponse

DEFAULT_BASE_URL = "https://api.coincircuit.io"
DEFAULT_TIMEOUT = 30.0
MAX_RETRIES = 2
RETRYABLE_STATUS_CODES = (429, 500, 502, 503, 504)


def _build_headers(api_key: str) -> Dict[str, str]:
    return {
        "x-api-key": api_key,
        "Content-Type": "application/json",
        "Accept": "application/json",
    }


def _parse_response(response: httpx.Response) -> Any:
    """Parse the response, raise on errors, unwrap the envelope."""
    status = response.status_code

    # Try to decode JSON body
    try:
        body = response.json()
    except Exception:
        body = {}

    # Auth errors (401, 403)
    if status in (401, 403):
        message = body.get("message", "Authentication failed")
        if isinstance(message, list):
            message = "; ".join(message)
        raise CoinCircuitAuthError(
            message=message,
            status_code=status,
            body=body,
        )

    # Other error status codes
    if status >= 400:
        message = body.get("message", f"API request failed with status {status}")
        if isinstance(message, list):
            message = "; ".join(message)
        raise CoinCircuitApiError(
            message=message,
            status_code=status,
            body=body,
        )

    # Envelope unwrapping: {"success": bool, "message": str, "data": T}
    # If the body matches the envelope format, return just "data".
    if isinstance(body, dict) and "success" in body:
        if not body.get("success", True):
            raise CoinCircuitApiError(
                message=body.get("message", "Request was not successful"),
                status_code=status,
                body=body,
            )
        # Return data if present, otherwise the full body
        if "data" in body:
            return body["data"]
        return body

    return body


def _parse_paginated_response(response: httpx.Response) -> PaginatedResponse:
    """Parse a paginated response, returning a PaginatedResponse wrapper."""
    status = response.status_code

    try:
        body = response.json()
    except Exception:
        body = {}

    if status in (401, 403):
        message = body.get("message", "Authentication failed")
        if isinstance(message, list):
            message = "; ".join(message)
        raise CoinCircuitAuthError(
            message=message,
            status_code=status,
            body=body,
        )

    if status >= 400:
        message = body.get("message", f"API request failed with status {status}")
        if isinstance(message, list):
            message = "; ".join(message)
        raise CoinCircuitApiError(
            message=message,
            status_code=status,
            body=body,
        )

    if isinstance(body, dict) and "success" in body:
        if not body.get("success", True):
            raise CoinCircuitApiError(
                message=body.get("message", "Request was not successful"),
                status_code=status,
                body=body,
            )

    data = body.get("data", [])
    meta = body.get("meta", {"page": 1, "size": len(data), "total": len(data), "totalPages": 1})
    return PaginatedResponse(data=data, meta=meta)


def _backoff_delay(attempt: int) -> float:
    """Exponential backoff: 0.5s, 1.0s, 2.0s, etc."""
    return 0.5 * (2 ** attempt)


class HttpClient:
    """Synchronous HTTP client for the CoinCircuit API.

    Handles authentication, retries with exponential backoff, and
    response envelope unwrapping.
    """

    def __init__(
        self,
        api_key: str,
        base_url: str = DEFAULT_BASE_URL,
        timeout: float = DEFAULT_TIMEOUT,
        max_retries: int = MAX_RETRIES,
    ) -> None:
        if not api_key:
            raise CoinCircuitError("api_key is required")

        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.max_retries = max_retries
        self._client = httpx.Client(
            base_url=self.base_url,
            headers=_build_headers(api_key),
            timeout=timeout,
        )

    def request(
        self,
        method: str,
        path: str,
        params: Optional[Dict[str, Any]] = None,
        json_body: Optional[Dict[str, Any]] = None,
    ) -> Any:
        """Make a request, retry on transient errors, unwrap envelope."""
        # Strip None values from params
        if params:
            params = {k: v for k, v in params.items() if v is not None}

        last_error: Optional[Exception] = None
        for attempt in range(self.max_retries + 1):
            try:
                response = self._client.request(
                    method=method,
                    url=path,
                    params=params,
                    json=json_body,
                )
                if response.status_code in RETRYABLE_STATUS_CODES and attempt < self.max_retries:
                    time.sleep(_backoff_delay(attempt))
                    continue
                return _parse_response(response)
            except (CoinCircuitAuthError, CoinCircuitApiError):
                raise
            except httpx.HTTPError as exc:
                last_error = exc
                if attempt < self.max_retries:
                    time.sleep(_backoff_delay(attempt))
                    continue
                raise CoinCircuitError(f"HTTP request failed: {exc}") from exc

        # Should not reach here, but just in case
        raise CoinCircuitError(f"Request failed after {self.max_retries + 1} attempts")

    def request_paginated(
        self,
        method: str,
        path: str,
        params: Optional[Dict[str, Any]] = None,
    ) -> PaginatedResponse:
        """Make a request and return a PaginatedResponse."""
        if params:
            params = {k: v for k, v in params.items() if v is not None}

        last_error: Optional[Exception] = None
        for attempt in range(self.max_retries + 1):
            try:
                response = self._client.request(
                    method=method,
                    url=path,
                    params=params,
                )
                if response.status_code in RETRYABLE_STATUS_CODES and attempt < self.max_retries:
                    time.sleep(_backoff_delay(attempt))
                    continue
                return _parse_paginated_response(response)
            except (CoinCircuitAuthError, CoinCircuitApiError):
                raise
            except httpx.HTTPError as exc:
                last_error = exc
                if attempt < self.max_retries:
                    time.sleep(_backoff_delay(attempt))
                    continue
                raise CoinCircuitError(f"HTTP request failed: {exc}") from exc

        raise CoinCircuitError(f"Request failed after {self.max_retries + 1} attempts")

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

    def __enter__(self) -> "HttpClient":
        return self

    def __exit__(self, *args: Any) -> None:
        self.close()


class AsyncHttpClient:
    """Asynchronous HTTP client for the CoinCircuit API.

    Same behaviour as ``HttpClient`` but uses ``httpx.AsyncClient``.
    """

    def __init__(
        self,
        api_key: str,
        base_url: str = DEFAULT_BASE_URL,
        timeout: float = DEFAULT_TIMEOUT,
        max_retries: int = MAX_RETRIES,
    ) -> None:
        if not api_key:
            raise CoinCircuitError("api_key is required")

        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.max_retries = max_retries
        self._client = httpx.AsyncClient(
            base_url=self.base_url,
            headers=_build_headers(api_key),
            timeout=timeout,
        )

    async def request(
        self,
        method: str,
        path: str,
        params: Optional[Dict[str, Any]] = None,
        json_body: Optional[Dict[str, Any]] = None,
    ) -> Any:
        """Make an async request, retry on transient errors, unwrap envelope."""
        import asyncio

        if params:
            params = {k: v for k, v in params.items() if v is not None}

        last_error: Optional[Exception] = None
        for attempt in range(self.max_retries + 1):
            try:
                response = await self._client.request(
                    method=method,
                    url=path,
                    params=params,
                    json=json_body,
                )
                if response.status_code in RETRYABLE_STATUS_CODES and attempt < self.max_retries:
                    await asyncio.sleep(_backoff_delay(attempt))
                    continue
                return _parse_response(response)
            except (CoinCircuitAuthError, CoinCircuitApiError):
                raise
            except httpx.HTTPError as exc:
                last_error = exc
                if attempt < self.max_retries:
                    await asyncio.sleep(_backoff_delay(attempt))
                    continue
                raise CoinCircuitError(f"HTTP request failed: {exc}") from exc

        raise CoinCircuitError(f"Request failed after {self.max_retries + 1} attempts")

    async def request_paginated(
        self,
        method: str,
        path: str,
        params: Optional[Dict[str, Any]] = None,
    ) -> PaginatedResponse:
        """Make an async request and return a PaginatedResponse."""
        import asyncio

        if params:
            params = {k: v for k, v in params.items() if v is not None}

        last_error: Optional[Exception] = None
        for attempt in range(self.max_retries + 1):
            try:
                response = await self._client.request(
                    method=method,
                    url=path,
                    params=params,
                )
                if response.status_code in RETRYABLE_STATUS_CODES and attempt < self.max_retries:
                    await asyncio.sleep(_backoff_delay(attempt))
                    continue
                return _parse_paginated_response(response)
            except (CoinCircuitAuthError, CoinCircuitApiError):
                raise
            except httpx.HTTPError as exc:
                last_error = exc
                if attempt < self.max_retries:
                    await asyncio.sleep(_backoff_delay(attempt))
                    continue
                raise CoinCircuitError(f"HTTP request failed: {exc}") from exc

        raise CoinCircuitError(f"Request failed after {self.max_retries + 1} attempts")

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

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

    async def __aexit__(self, *args: Any) -> None:
        await self.close()
