"""Base HTTP client with sync/async support."""

from __future__ import annotations

import time
from typing import Any, Generator, AsyncGenerator, Mapping

import httpx

from ._exceptions import (
    ManiacError,
    AuthenticationError,
    RateLimitError,
    NotFoundError,
    BadRequestError,
    APIError,
    APIConnectionError,
)

DEFAULT_BASE_URL = "https://platform.maniac.ai"
DEFAULT_TIMEOUT = 60.0
DEFAULT_MAX_RETRIES = 2

# Status codes that should trigger a retry
RETRYABLE_STATUS_CODES = {408, 429, 500, 502, 503, 504}


class BaseClient:
    """Base class for sync and async clients."""

    def __init__(
        self,
        api_key: str | None = None,
        base_url: str | None = None,
        timeout: float = DEFAULT_TIMEOUT,
        max_retries: int = DEFAULT_MAX_RETRIES,
    ) -> None:
        import os

        self.api_key = api_key or os.environ.get("MANIAC_API_KEY")
        if not self.api_key:
            raise AuthenticationError("No API key provided. Set MANIAC_API_KEY or pass api_key=")

        self.base_url = (base_url or os.environ.get("MANIAC_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
        self.timeout = timeout
        self.max_retries = max_retries

    def _build_headers(self, extra_headers: Mapping[str, str] | None = None) -> dict[str, str]:
        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json",
            "User-Agent": "maniac-python/1.0.0",
        }
        if extra_headers:
            headers.update(extra_headers)
        return headers

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

    def _handle_error_response(self, response: httpx.Response) -> None:
        """Raise appropriate exception based on response status."""
        try:
            body = response.json()
            error_data = body.get("error", {})
            message = error_data.get("message", response.text)
            code = error_data.get("code", "UNKNOWN")
        except Exception:
            message = response.text
            code = "UNKNOWN"

        if response.status_code == 401:
            raise AuthenticationError(message)
        elif response.status_code == 403:
            raise AuthenticationError(f"Forbidden: {message}")
        elif response.status_code == 404:
            raise NotFoundError(message)
        elif response.status_code == 400:
            raise BadRequestError(message)
        elif response.status_code == 422:
            raise BadRequestError(f"Validation error: {message}")
        elif response.status_code == 429:
            retry_after = response.headers.get("Retry-After")
            raise RateLimitError(message, retry_after=int(retry_after) if retry_after else None)
        else:
            raise APIError(message, status_code=response.status_code, code=code)


class SyncAPIClient(BaseClient):
    """Synchronous HTTP client."""

    def __init__(
        self,
        api_key: str | None = None,
        base_url: str | None = None,
        timeout: float = DEFAULT_TIMEOUT,
        max_retries: int = DEFAULT_MAX_RETRIES,
        http_client: httpx.Client | None = None,
    ) -> None:
        super().__init__(api_key, base_url, timeout, max_retries)
        self._client = http_client or httpx.Client(timeout=timeout)

    def close(self) -> None:
        self._client.close()

    def __enter__(self) -> SyncAPIClient:
        return self

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

    def _request(
        self,
        method: str,
        path: str,
        *,
        params: dict[str, Any] | None = None,
        json: dict[str, Any] | None = None,
        data: dict[str, Any] | None = None,
        files: dict[str, Any] | None = None,
        headers: Mapping[str, str] | None = None,
        stream: bool = False,
    ) -> httpx.Response:
        url = self._build_url(path)
        request_headers = self._build_headers(headers)

        # Don't set Content-Type for multipart
        if files:
            del request_headers["Content-Type"]

        last_exception: Exception | None = None

        for attempt in range(self.max_retries + 1):
            try:
                response = self._client.request(
                    method,
                    url,
                    params=params,
                    json=json,
                    data=data,
                    files=files,
                    headers=request_headers,
                )

                if response.status_code in RETRYABLE_STATUS_CODES and attempt < self.max_retries:
                    retry_after = response.headers.get("Retry-After")
                    sleep_time = int(retry_after) if retry_after else (2**attempt)
                    time.sleep(min(sleep_time, 60))
                    continue

                if response.status_code >= 400:
                    self._handle_error_response(response)

                return response

            except httpx.RequestError as e:
                last_exception = e
                if attempt < self.max_retries:
                    time.sleep(2**attempt)
                    continue
                raise APIConnectionError(f"Connection error: {e}") from e

        raise last_exception or APIError("Max retries exceeded")

    def get(self, path: str, *, params: dict[str, Any] | None = None) -> Any:
        response = self._request("GET", path, params=params)
        return response.json()

    def post(
        self,
        path: str,
        *,
        json: dict[str, Any] | None = None,
        data: dict[str, Any] | None = None,
        files: dict[str, Any] | None = None,
    ) -> Any:
        response = self._request("POST", path, json=json, data=data, files=files)
        return response.json()

    def patch(self, path: str, *, json: dict[str, Any] | None = None) -> Any:
        response = self._request("PATCH", path, json=json)
        return response.json()

    def delete(self, path: str) -> Any:
        response = self._request("DELETE", path)
        return response.json()

    def post_stream(
        self, path: str, *, json: dict[str, Any] | None = None
    ) -> Generator[str, None, None]:
        """POST request that yields SSE chunks."""
        url = self._build_url(path)
        headers = self._build_headers()
        headers["Accept"] = "text/event-stream"

        with self._client.stream("POST", url, json=json, headers=headers) as response:
            if response.status_code >= 400:
                # Read full response for error
                response.read()
                self._handle_error_response(response)

            for line in response.iter_lines():
                if line.startswith("data: "):
                    yield line[6:]

    def get_raw(self, path: str) -> bytes:
        """GET request returning raw bytes (for file content)."""
        response = self._request("GET", path)
        return response.content


class AsyncAPIClient(BaseClient):
    """Asynchronous HTTP client."""

    def __init__(
        self,
        api_key: str | None = None,
        base_url: str | None = None,
        timeout: float = DEFAULT_TIMEOUT,
        max_retries: int = DEFAULT_MAX_RETRIES,
        http_client: httpx.AsyncClient | None = None,
    ) -> None:
        super().__init__(api_key, base_url, timeout, max_retries)
        self._client = http_client or httpx.AsyncClient(timeout=timeout)

    async def close(self) -> None:
        await self._client.aclose()

    async def __aenter__(self) -> AsyncAPIClient:
        return self

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

    async def _request(
        self,
        method: str,
        path: str,
        *,
        params: dict[str, Any] | None = None,
        json: dict[str, Any] | None = None,
        data: dict[str, Any] | None = None,
        files: dict[str, Any] | None = None,
        headers: Mapping[str, str] | None = None,
    ) -> httpx.Response:
        import asyncio

        url = self._build_url(path)
        request_headers = self._build_headers(headers)

        if files:
            del request_headers["Content-Type"]

        last_exception: Exception | None = None

        for attempt in range(self.max_retries + 1):
            try:
                response = await self._client.request(
                    method,
                    url,
                    params=params,
                    json=json,
                    data=data,
                    files=files,
                    headers=request_headers,
                )

                if response.status_code in RETRYABLE_STATUS_CODES and attempt < self.max_retries:
                    retry_after = response.headers.get("Retry-After")
                    sleep_time = int(retry_after) if retry_after else (2**attempt)
                    await asyncio.sleep(min(sleep_time, 60))
                    continue

                if response.status_code >= 400:
                    self._handle_error_response(response)

                return response

            except httpx.RequestError as e:
                last_exception = e
                if attempt < self.max_retries:
                    await asyncio.sleep(2**attempt)
                    continue
                raise APIConnectionError(f"Connection error: {e}") from e

        raise last_exception or APIError("Max retries exceeded")

    async def get(self, path: str, *, params: dict[str, Any] | None = None) -> Any:
        response = await self._request("GET", path, params=params)
        return response.json()

    async def post(
        self,
        path: str,
        *,
        json: dict[str, Any] | None = None,
        data: dict[str, Any] | None = None,
        files: dict[str, Any] | None = None,
    ) -> Any:
        response = await self._request("POST", path, json=json, data=data, files=files)
        return response.json()

    async def patch(self, path: str, *, json: dict[str, Any] | None = None) -> Any:
        response = await self._request("PATCH", path, json=json)
        return response.json()

    async def delete(self, path: str) -> Any:
        response = await self._request("DELETE", path)
        return response.json()

    async def post_stream(
        self, path: str, *, json: dict[str, Any] | None = None
    ) -> AsyncGenerator[str, None]:
        """POST request that yields SSE chunks."""
        url = self._build_url(path)
        headers = self._build_headers()
        headers["Accept"] = "text/event-stream"

        async with self._client.stream("POST", url, json=json, headers=headers) as response:
            if response.status_code >= 400:
                await response.aread()
                self._handle_error_response(response)

            async for line in response.aiter_lines():
                if line.startswith("data: "):
                    yield line[6:]

    async def get_raw(self, path: str) -> bytes:
        """GET request returning raw bytes."""
        response = await self._request("GET", path)
        return response.content
