"""Low-level HTTP client with retry logic, used internally by resource classes."""

from __future__ import annotations

import asyncio
import math
import random
import time
from typing import Any, BinaryIO, Dict, Optional, Tuple, Union

import httpx

from .exceptions import (
    AuthenticationError,
    InsufficientBalanceError,
    NotFoundError,
    RateLimitError,
    ValidationError,
    VerilockError,
)

__all__ = ["HttpClient", "AsyncHttpClient"]

_SDK_VERSION = "1.0.0"

# File-like types the upload methods accept
FileInput = Union[bytes, BinaryIO, Tuple[str, Union[bytes, BinaryIO]]]


class _BaseHttpClient:
    """Shared logic for sync and async HTTP clients."""

    def __init__(
        self,
        api_key: str,
        base_url: str,
        timeout: float,
        max_retries: int,
    ) -> None:
        self._api_key = api_key
        self._base_url = base_url.rstrip("/")
        self._timeout = timeout
        self._max_retries = max_retries

    def _default_headers(self) -> Dict[str, str]:
        return {
            "Authorization": f"Bearer {self._api_key}",
            "Accept": "application/json",
            "User-Agent": f"verilock-python/{_SDK_VERSION}",
            "X-SDK": f"python/{_SDK_VERSION}",
        }

    def _build_url(self, path: str) -> str:
        return f"{self._base_url}{path}"

    def _build_params(
        self, params: Optional[Dict[str, Any]]
    ) -> Optional[Dict[str, str]]:
        if not params:
            return None
        return {k: str(v) for k, v in params.items() if v is not None}

    def _parse_error_body(self, response: httpx.Response) -> Dict[str, Any]:
        try:
            return response.json()  # type: ignore[no-any-return]
        except Exception:
            return {"error": "unknown", "message": response.reason_phrase or "Unknown error"}

    def _create_error(self, status_code: int, body: Dict[str, Any]) -> VerilockError:
        message = body.get("message", "Unknown error")
        if status_code == 401:
            return AuthenticationError(message, body)
        if status_code == 402:
            return InsufficientBalanceError(body)
        if status_code == 404:
            return NotFoundError(message)
        if status_code == 422:
            return ValidationError(message, body.get("errors"))
        if status_code == 429:
            return RateLimitError(message)
        return VerilockError(message, body.get("error", "unknown"), status_code, body)

    def _should_retry(self, status_code: int) -> bool:
        return status_code == 429 or status_code >= 500

    def _backoff_delay(self, attempt: int, retry_after: Optional[str] = None) -> float:
        if retry_after is not None:
            try:
                return float(retry_after)
            except ValueError:
                pass
        delay = min(0.5 * math.pow(2, attempt), 8.0)
        delay += random.uniform(0, 0.2)
        return delay


class HttpClient(_BaseHttpClient):
    """Synchronous HTTP client powered by ``httpx``."""

    def __init__(
        self,
        api_key: str,
        base_url: str,
        timeout: float,
        max_retries: int,
    ) -> None:
        super().__init__(api_key, base_url, timeout, max_retries)
        self._client = httpx.Client(
            timeout=timeout,
            headers=self._default_headers(),
        )

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

    def get(
        self,
        path: str,
        params: Optional[Dict[str, Any]] = None,
        *,
        idempotency_key: Optional[str] = None,
    ) -> Any:
        return self._request("GET", path, params=params, idempotency_key=idempotency_key)

    def post(
        self,
        path: str,
        body: Optional[Any] = None,
        *,
        idempotency_key: Optional[str] = None,
    ) -> Any:
        return self._request("POST", path, json_body=body, idempotency_key=idempotency_key)

    def delete(
        self,
        path: str,
        *,
        idempotency_key: Optional[str] = None,
    ) -> Any:
        return self._request("DELETE", path, idempotency_key=idempotency_key)

    def upload(
        self,
        path: str,
        file: FileInput,
        filename: str,
        fields: Optional[Dict[str, str]] = None,
        *,
        idempotency_key: Optional[str] = None,
    ) -> Any:
        return self._request_multipart(
            path, file, filename, fields, idempotency_key=idempotency_key
        )

    # ── Private ───────────────────────────────────────────────────────────────

    def _request(
        self,
        method: str,
        path: str,
        params: Optional[Dict[str, Any]] = None,
        json_body: Optional[Any] = None,
        *,
        idempotency_key: Optional[str] = None,
    ) -> Any:
        url = self._build_url(path)
        query = self._build_params(params)
        headers: Dict[str, str] = {}
        if idempotency_key:
            headers["Idempotency-Key"] = idempotency_key

        last_error: Optional[Exception] = None

        for attempt in range(self._max_retries + 1):
            try:
                response = self._client.request(
                    method,
                    url,
                    params=query,
                    json=json_body,
                    headers=headers if headers else None,
                )

                if response.is_success:
                    return response.json()

                error_body = self._parse_error_body(response)
                error = self._create_error(response.status_code, error_body)

                if self._should_retry(response.status_code) and attempt < self._max_retries:
                    last_error = error
                    retry_after = response.headers.get("Retry-After")
                    time.sleep(self._backoff_delay(attempt, retry_after))
                    continue

                raise error

            except VerilockError:
                raise
            except Exception as exc:
                last_error = exc
                if attempt < self._max_retries:
                    time.sleep(self._backoff_delay(attempt))
                    continue

        raise last_error or VerilockError("Request failed after retries", "request_failed")

    def _request_multipart(
        self,
        path: str,
        file: FileInput,
        filename: str,
        fields: Optional[Dict[str, str]] = None,
        *,
        idempotency_key: Optional[str] = None,
    ) -> Any:
        url = self._build_url(path)
        headers: Dict[str, str] = {}
        if idempotency_key:
            headers["Idempotency-Key"] = idempotency_key

        data: Dict[str, str] = {}
        if fields:
            data.update(fields)

        if isinstance(file, tuple):
            fname, fdata = file
            files = {"file": (fname, fdata)}
        elif isinstance(file, bytes):
            files = {"file": (filename, file)}
        else:
            files = {"file": (filename, file)}

        response = self._client.post(
            url,
            data=data,
            files=files,
            headers=headers if headers else None,
        )

        if response.is_success:
            return response.json()

        error_body = self._parse_error_body(response)
        raise self._create_error(response.status_code, error_body)


class AsyncHttpClient(_BaseHttpClient):
    """Asynchronous HTTP client powered by ``httpx``."""

    def __init__(
        self,
        api_key: str,
        base_url: str,
        timeout: float,
        max_retries: int,
    ) -> None:
        super().__init__(api_key, base_url, timeout, max_retries)
        self._client = httpx.AsyncClient(
            timeout=timeout,
            headers=self._default_headers(),
        )

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

    async def get(
        self,
        path: str,
        params: Optional[Dict[str, Any]] = None,
        *,
        idempotency_key: Optional[str] = None,
    ) -> Any:
        return await self._request("GET", path, params=params, idempotency_key=idempotency_key)

    async def post(
        self,
        path: str,
        body: Optional[Any] = None,
        *,
        idempotency_key: Optional[str] = None,
    ) -> Any:
        return await self._request("POST", path, json_body=body, idempotency_key=idempotency_key)

    async def delete(
        self,
        path: str,
        *,
        idempotency_key: Optional[str] = None,
    ) -> Any:
        return await self._request("DELETE", path, idempotency_key=idempotency_key)

    async def upload(
        self,
        path: str,
        file: FileInput,
        filename: str,
        fields: Optional[Dict[str, str]] = None,
        *,
        idempotency_key: Optional[str] = None,
    ) -> Any:
        return await self._request_multipart(
            path, file, filename, fields, idempotency_key=idempotency_key
        )

    # ── Private ───────────────────────────────────────────────────────────────

    async def _request(
        self,
        method: str,
        path: str,
        params: Optional[Dict[str, Any]] = None,
        json_body: Optional[Any] = None,
        *,
        idempotency_key: Optional[str] = None,
    ) -> Any:
        url = self._build_url(path)
        query = self._build_params(params)
        headers: Dict[str, str] = {}
        if idempotency_key:
            headers["Idempotency-Key"] = idempotency_key

        last_error: Optional[Exception] = None

        for attempt in range(self._max_retries + 1):
            try:
                response = await self._client.request(
                    method,
                    url,
                    params=query,
                    json=json_body,
                    headers=headers if headers else None,
                )

                if response.is_success:
                    return response.json()

                error_body = self._parse_error_body(response)
                error = self._create_error(response.status_code, error_body)

                if self._should_retry(response.status_code) and attempt < self._max_retries:
                    last_error = error
                    retry_after = response.headers.get("Retry-After")
                    await asyncio.sleep(self._backoff_delay(attempt, retry_after))
                    continue

                raise error

            except VerilockError:
                raise
            except Exception as exc:
                last_error = exc
                if attempt < self._max_retries:
                    await asyncio.sleep(self._backoff_delay(attempt))
                    continue

        raise last_error or VerilockError("Request failed after retries", "request_failed")

    async def _request_multipart(
        self,
        path: str,
        file: FileInput,
        filename: str,
        fields: Optional[Dict[str, str]] = None,
        *,
        idempotency_key: Optional[str] = None,
    ) -> Any:
        url = self._build_url(path)
        headers: Dict[str, str] = {}
        if idempotency_key:
            headers["Idempotency-Key"] = idempotency_key

        data: Dict[str, str] = {}
        if fields:
            data.update(fields)

        if isinstance(file, tuple):
            fname, fdata = file
            files = {"file": (fname, fdata)}
        elif isinstance(file, bytes):
            files = {"file": (filename, file)}
        else:
            files = {"file": (filename, file)}

        response = await self._client.post(
            url,
            data=data,
            files=files,
            headers=headers if headers else None,
        )

        if response.is_success:
            return response.json()

        error_body = self._parse_error_body(response)
        raise self._create_error(response.status_code, error_body)
