"""Core HTTP client for the Curvestone API."""

from __future__ import annotations

import os
import time
from typing import Any, BinaryIO

import httpx

from curvestone._exceptions import CurvestoneError, RateLimitError, _raise_for_status
from curvestone._version import __version__

_DEFAULT_BASE_URL = "https://agent.curvestone.ai/agent"
_DEFAULT_TIMEOUT = 60.0
_DEFAULT_MAX_RETRIES = 2


class _BaseClient:
    """Shared configuration 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:
        self.api_key = api_key or os.environ.get("CURVESTONE_API_KEY", "")
        if not self.api_key:
            raise CurvestoneError(
                "No API key provided. Pass api_key= or set CURVESTONE_API_KEY env var."
            )
        self.base_url = (base_url or os.environ.get("CURVESTONE_BASE_URL", "")).rstrip("/")
        if not self.base_url:
            self.base_url = _DEFAULT_BASE_URL
        self.timeout = timeout
        self.max_retries = max_retries

    def _headers(self) -> dict[str, str]:
        return {
            "Ocp-Apim-Subscription-Key": self.api_key,
            "User-Agent": f"curvestone-python/{__version__}",
            "Accept": "application/json",
        }


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


def _retry_delay(attempt: int, headers: httpx.Headers) -> float:
    retry_after = headers.get("Retry-After")
    if retry_after:
        try:
            return float(retry_after)
        except ValueError:
            pass
    return min(0.5 * (2**attempt), 8.0)


# ── Sync client ──────────────────────────────────────────────────


class Curvestone(_BaseClient):
    """Synchronous Curvestone API client."""

    def __init__(self, **kwargs: Any) -> None:
        super().__init__(**kwargs)
        self._http = httpx.Client(
            base_url=self.base_url,
            headers=self._headers(),
            timeout=self.timeout,
        )

        from curvestone.resources.checks import SyncChecks
        from curvestone.resources.jobs import SyncJobs
        from curvestone.resources.files import SyncFiles
        from curvestone.resources.ask import SyncAsk
        from curvestone.resources.generate import SyncGenerate
        from curvestone.resources.monitors import SyncMonitors
        from curvestone.resources.webhooks import SyncWebhooks

        self.checks = SyncChecks(self)
        self.jobs = SyncJobs(self)
        self.files = SyncFiles(self)
        self.ask = SyncAsk(self)
        self.generate = SyncGenerate(self)
        self.monitors = SyncMonitors(self)
        self.webhooks = SyncWebhooks(self)

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

    def __enter__(self) -> Curvestone:
        return self

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

    def _request(
        self,
        method: str,
        path: str,
        *,
        json: dict | None = None,
        params: dict | None = None,
        data: dict | None = None,
        files: dict | None = None,
    ) -> dict | None:
        """Make an HTTP request with retry logic."""
        last_exc: Exception | None = None

        for attempt in range(1 + self.max_retries):
            try:
                resp = self._http.request(
                    method,
                    path,
                    json=json,
                    params=_clean_params(params),
                    data=data,
                    files=files,
                )
            except httpx.TimeoutException as e:
                last_exc = e
                if attempt < self.max_retries:
                    time.sleep(_retry_delay(attempt, httpx.Headers()))
                    continue
                raise CurvestoneError(f"Request timed out: {e}") from e

            if _should_retry(resp.status_code) and attempt < self.max_retries:
                time.sleep(_retry_delay(attempt, resp.headers))
                continue

            if resp.status_code == 204:
                return None

            body = resp.json() if resp.content else None
            _raise_for_status(resp.status_code, body)
            return body

        raise last_exc or CurvestoneError("Request failed after retries")

    def _upload(self, path: str, file: BinaryIO, purpose: str) -> dict:
        """Upload a file via multipart form."""
        filename = getattr(file, "name", "upload")
        if "/" in filename:
            filename = filename.rsplit("/", 1)[-1]

        last_exc: Exception | None = None
        for attempt in range(1 + self.max_retries):
            file.seek(0)
            try:
                resp = self._http.post(
                    path,
                    files={"file": (filename, file)},
                    data={"purpose": purpose},
                )
            except httpx.TimeoutException as e:
                last_exc = e
                if attempt < self.max_retries:
                    time.sleep(_retry_delay(attempt, httpx.Headers()))
                    continue
                raise CurvestoneError(f"Upload timed out: {e}") from e

            if _should_retry(resp.status_code) and attempt < self.max_retries:
                time.sleep(_retry_delay(attempt, resp.headers))
                continue

            body = resp.json() if resp.content else None
            _raise_for_status(resp.status_code, body)
            return body  # type: ignore[return-value]

        raise last_exc or CurvestoneError("Upload failed after retries")

    def _stream_sse(self, path: str) -> Any:
        """Open an SSE stream."""
        from httpx_sse import connect_sse

        return connect_sse(self._http, "GET", path)


# ── Async client ─────────────────────────────────────────────────


class AsyncCurvestone(_BaseClient):
    """Asynchronous Curvestone API client."""

    def __init__(self, **kwargs: Any) -> None:
        super().__init__(**kwargs)
        self._http = httpx.AsyncClient(
            base_url=self.base_url,
            headers=self._headers(),
            timeout=self.timeout,
        )

        from curvestone.resources.checks import AsyncChecks
        from curvestone.resources.jobs import AsyncJobs
        from curvestone.resources.files import AsyncFiles
        from curvestone.resources.ask import AsyncAsk
        from curvestone.resources.generate import AsyncGenerate
        from curvestone.resources.monitors import AsyncMonitors
        from curvestone.resources.webhooks import AsyncWebhooks

        self.checks = AsyncChecks(self)
        self.jobs = AsyncJobs(self)
        self.files = AsyncFiles(self)
        self.ask = AsyncAsk(self)
        self.generate = AsyncGenerate(self)
        self.monitors = AsyncMonitors(self)
        self.webhooks = AsyncWebhooks(self)

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

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

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

    async def _request(
        self,
        method: str,
        path: str,
        *,
        json: dict | None = None,
        params: dict | None = None,
        data: dict | None = None,
        files: dict | None = None,
    ) -> dict | None:
        """Make an async HTTP request with retry logic."""
        import asyncio

        last_exc: Exception | None = None

        for attempt in range(1 + self.max_retries):
            try:
                resp = await self._http.request(
                    method,
                    path,
                    json=json,
                    params=_clean_params(params),
                    data=data,
                    files=files,
                )
            except httpx.TimeoutException as e:
                last_exc = e
                if attempt < self.max_retries:
                    await asyncio.sleep(_retry_delay(attempt, httpx.Headers()))
                    continue
                raise CurvestoneError(f"Request timed out: {e}") from e

            if _should_retry(resp.status_code) and attempt < self.max_retries:
                await asyncio.sleep(_retry_delay(attempt, resp.headers))
                continue

            if resp.status_code == 204:
                return None

            body = resp.json() if resp.content else None
            _raise_for_status(resp.status_code, body)
            return body

        raise last_exc or CurvestoneError("Request failed after retries")

    async def _upload(self, path: str, file: BinaryIO, purpose: str) -> dict:
        """Upload a file via multipart form."""
        import asyncio

        filename = getattr(file, "name", "upload")
        if "/" in filename:
            filename = filename.rsplit("/", 1)[-1]

        last_exc: Exception | None = None
        for attempt in range(1 + self.max_retries):
            file.seek(0)
            try:
                resp = await self._http.post(
                    path,
                    files={"file": (filename, file)},
                    data={"purpose": purpose},
                )
            except httpx.TimeoutException as e:
                last_exc = e
                if attempt < self.max_retries:
                    await asyncio.sleep(_retry_delay(attempt, httpx.Headers()))
                    continue
                raise CurvestoneError(f"Upload timed out: {e}") from e

            if _should_retry(resp.status_code) and attempt < self.max_retries:
                await asyncio.sleep(_retry_delay(attempt, resp.headers))
                continue

            body = resp.json() if resp.content else None
            _raise_for_status(resp.status_code, body)
            return body  # type: ignore[return-value]

        raise last_exc or CurvestoneError("Upload failed after retries")

    def _stream_sse(self, path: str) -> Any:
        """Open an async SSE stream."""
        from httpx_sse import aconnect_sse

        return aconnect_sse(self._http, "GET", path)


# ── Helpers ──────────────────────────────────────────────────────


def _clean_params(params: dict | None) -> dict | None:
    """Remove None values from query params."""
    if params is None:
        return None
    return {k: v for k, v in params.items() if v is not None}
