"""Async HTTP client for the Influ2 API."""

from __future__ import annotations

import asyncio
import logging
from typing import Any

import httpx

from influ2_mcp.config import get_config
from influ2_mcp.errors import (
    Influ2AuthError,
    Influ2Error,
    Influ2NotFoundError,
    Influ2RateLimitError,
    Influ2ServerError,
    Influ2ValidationError,
)

logger = logging.getLogger(__name__)

# Module-level singleton
_client: Influ2ApiClient | None = None


class Influ2ApiClient:
    """Thin async wrapper around the Influ2 REST API.

    Handles authentication, retries, and error mapping so that tools
    never have to deal with raw HTTP details.
    """

    def __init__(self, api_key: str, base_url: str = "https://v2-api.influ2.com"):
        self._api_key = api_key
        self._base_url = base_url.rstrip("/")
        self._http = httpx.AsyncClient(
            base_url=self._base_url,
            headers={
                "Authorization": f"Bearer {api_key}",
                "Content-Type": "application/json",
                "Accept": "application/json",
            },
            timeout=httpx.Timeout(60.0, connect=10.0),
        )

    # -- Public HTTP methods --------------------------------------------------

    async def get(self, path: str, params: dict[str, Any] | None = None) -> Any:
        """GET request with retries and error mapping."""
        return await self._request("GET", path, params=params)

    async def post(
        self,
        path: str,
        json: Any | None = None,
        data: Any | None = None,
        files: Any | None = None,
    ) -> Any:
        """POST request with retries and error mapping."""
        return await self._request("POST", path, json=json, data=data, files=files)

    async def put(self, path: str, json: Any | None = None) -> Any:
        """PUT request with retries and error mapping."""
        return await self._request("PUT", path, json=json)

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

    # -- Internal request handling --------------------------------------------

    async def _request(
        self,
        method: str,
        path: str,
        params: dict[str, Any] | None = None,
        json: Any | None = None,
        data: Any | None = None,
        files: Any | None = None,
        _retry_count: int = 0,
    ) -> Any:
        """Execute an HTTP request with retries and error mapping."""
        max_retries = 2

        try:
            kwargs: dict[str, Any] = {}
            if params:
                kwargs["params"] = params
            if json is not None:
                kwargs["json"] = json
            if data is not None:
                kwargs["data"] = data
            if files is not None:
                kwargs["files"] = files
                # Remove Content-Type for multipart uploads
                kwargs["headers"] = {
                    "Authorization": f"Bearer {self._api_key}",
                    "Accept": "application/json",
                }

            response = await self._http.request(method, path, **kwargs)

            # Success
            if response.status_code < 400:
                if response.headers.get("content-type", "").startswith("application/json"):
                    return response.json()
                return {"status": "ok", "status_code": response.status_code}

            # Error handling by status code
            await self._handle_error(response, method, path, _retry_count, max_retries)

        except httpx.ConnectError as e:
            if _retry_count < max_retries:
                await asyncio.sleep(2 ** _retry_count)
                return await self._request(
                    method, path, params=params, json=json,
                    data=data, files=files, _retry_count=_retry_count + 1,
                )
            raise Influ2Error(
                message="Could not reach Influ2 API. Check network connection and try again.",
                suggestion="Verify network connectivity and that the API is accessible.",
                retry_possible=True,
            ) from e

        except httpx.TimeoutException as e:
            if _retry_count < max_retries:
                await asyncio.sleep(2 ** _retry_count)
                return await self._request(
                    method, path, params=params, json=json,
                    data=data, files=files, _retry_count=_retry_count + 1,
                )
            raise Influ2Error(
                message="Request to Influ2 API timed out. The server may be slow — try again.",
                suggestion="Try again in a moment.",
                retry_possible=True,
            ) from e

    async def _handle_error(
        self,
        response: httpx.Response,
        method: str,
        path: str,
        retry_count: int,
        max_retries: int,
    ) -> None:
        """Map HTTP error responses to typed exceptions."""
        status = response.status_code

        # Try to parse error body
        detail = ""
        try:
            body = response.json()
            detail = body.get("message", "") or body.get("error", "") or str(body)
        except Exception:
            detail = response.text[:500] if response.text else ""

        logger.warning(
            "Influ2 API error: %s %s -> %d: %s", method, path, status, detail
        )

        # 401/403 — Auth error (never retry)
        if status in (401, 403):
            raise Influ2AuthError(
                f"Authentication failed ({status}). {detail}"
                if detail
                else None
            )

        # 404 — Not found
        if status == 404:
            raise Influ2NotFoundError("Resource", "")

        # 400/422 — Validation error
        if status in (400, 422):
            raise Influ2ValidationError(detail or "Invalid request parameters.")

        # 429 — Rate limit
        if status == 429:
            retry_after = response.headers.get("Retry-After")
            retry_seconds = int(retry_after) if retry_after else 30
            if retry_count < 1:
                await asyncio.sleep(retry_seconds)
                # Re-raise to trigger retry in _request
                raise Influ2RateLimitError(retry_seconds)
            raise Influ2RateLimitError(retry_seconds)

        # 5xx — Server error (retry once)
        if status >= 500:
            if retry_count < max_retries:
                await asyncio.sleep(2)
                # Need to signal retry - raise and catch in _request
                raise Influ2ServerError(status)
            raise Influ2ServerError(status)

        # Unknown error
        raise Influ2Error(
            message=f"Unexpected API error ({status}): {detail}",
            status_code=status,
            retry_possible=False,
        )


def get_api_client() -> Influ2ApiClient:
    """Get or create the singleton API client."""
    global _client
    if _client is None:
        config = get_config()
        if not config.api_key:
            raise Influ2Error(
                message=(
                    "INFLU2_API_KEY environment variable is not set. "
                    "Configure it in your MCP server settings."
                ),
                suggestion=(
                    "Set INFLU2_API_KEY in your environment or Claude Desktop config "
                    "under mcpServers > influ2 > env."
                ),
            )
        _client = Influ2ApiClient(
            api_key=config.api_key,
            base_url=config.base_url,
        )
    return _client
