from __future__ import annotations

import asyncio
from typing import Any

import httpx

from mcp_eregistrations_smartlink.smartlink_client.errors import (
    SmartLinkAuthError,
    SmartLinkClientError,
    SmartLinkForbiddenError,
    SmartLinkNotFoundError,
    SmartLinkServerError,
    SmartLinkValidationError,
)

_DEFAULT_TIMEOUT = 30.0
_RETRYABLE_STATUSES = {429, 502, 503, 504}
_IDEMPOTENT_METHODS = {"GET", "PUT", "HEAD", "OPTIONS"}
_MAX_RETRIES = 3


class SmartLinkClient:
    """Async HTTP client for the SmartLink REST API."""

    def __init__(
        self,
        base_url: str,
        instance: str | None = None,
        timeout: float = _DEFAULT_TIMEOUT,
        verify_ssl: bool = True,
    ):
        self.base_url = base_url.rstrip("/")
        self._instance = instance
        self._timeout = timeout
        self._verify_ssl = verify_ssl
        self._token: str | None = None
        self._client: httpx.AsyncClient | None = None
        self._refresh_lock = asyncio.Lock()

    @classmethod
    def for_instance(cls, instance: str | None = None) -> SmartLinkClient:
        """Factory: resolves instance config, returns context manager."""
        from fastmcp.exceptions import ToolError

        from mcp_eregistrations_common.config import get_instance_config

        config = get_instance_config(instance)

        smartlink_url = config.get("smartlink_url")
        if not smartlink_url:
            raise ToolError(
                f"Instance '{instance}' has no SmartLink URL configured. "
                f"SmartLink is only available on dev/test/staging instances."
            )

        return cls(
            base_url=smartlink_url,
            instance=instance,
            verify_ssl=config.get("verify_ssl", True),
        )

    async def _resolve_token(self) -> str | None:
        if self._instance:
            from mcp_eregistrations_common.auth import get_or_refresh_token

            return await get_or_refresh_token(self._instance)
        return self._token

    async def __aenter__(self) -> SmartLinkClient:
        self._token = await self._resolve_token()
        if not self._token:
            from fastmcp.exceptions import ToolError

            raise ToolError("Not authenticated. Run smartlink_auth_login first.")
        self._client = httpx.AsyncClient(
            base_url=self.base_url,
            headers={"Authorization": f"Bearer {self._token}"},
            timeout=self._timeout,
            verify=self._verify_ssl,
        )
        return self

    async def __aexit__(self, *args: Any) -> None:
        if self._client:
            await self._client.aclose()
            self._client = None

    def _ensure_client(self) -> httpx.AsyncClient:
        assert self._client is not None, "Use 'async with SmartLinkClient(...)' first"
        return self._client

    async def _refresh_and_update_header(self) -> bool:
        """Refresh token with mutex to prevent concurrent refreshes."""
        async with self._refresh_lock:
            new_token = await self._resolve_token()
            if new_token and new_token != self._token:
                self._token = new_token
                self._ensure_client().headers["Authorization"] = f"Bearer {new_token}"
                return True
            return False

    async def _request_with_retry(
        self,
        method: str,
        path: str,
        *,
        params: dict[str, Any] | None = None,
        json: dict[str, Any] | None = None,
    ) -> Any:
        """Execute request with 401 retry + exponential backoff."""
        client = self._ensure_client()
        resp = await client.request(method, path, params=params, json=json)

        # 401: try token refresh once
        if resp.status_code == 401 and self._instance:
            refreshed = await self._refresh_and_update_header()
            if refreshed:
                resp = await client.request(method, path, params=params, json=json)

        # Transient errors: exponential backoff (idempotent methods only)
        if resp.status_code in _RETRYABLE_STATUSES and method in _IDEMPOTENT_METHODS:
            for attempt in range(1, _MAX_RETRIES + 1):
                delay = min(2**attempt, 10)
                await asyncio.sleep(delay)
                resp = await client.request(method, path, params=params, json=json)
                if resp.status_code not in _RETRYABLE_STATUSES:
                    break

        return self._handle_response(resp)

    async def get(self, path: str, **params: Any) -> Any:
        return await self._request_with_retry("GET", path, params=params or None)

    async def put(self, path: str, data: dict[str, Any] | None = None) -> Any:
        return await self._request_with_retry("PUT", path, json=data)

    def _handle_response(self, resp: httpx.Response) -> Any:
        """Check response status and return JSON or raise appropriate error."""
        if resp.status_code == 204:
            return None

        if resp.status_code in (200, 201):
            if not resp.content:
                return {}
            data = resp.json()
            if isinstance(data, list) and len(data) > 100:
                return {
                    "items": data[:100],
                    "total": len(data),
                    "truncated": True,
                    "message": (
                        f"Response truncated: {len(data)} items, showing first 100."
                    ),
                }
            return data

        # Error responses
        try:
            body = resp.json()
        except Exception:
            body = resp.text
        msg = str(
            body.get("message", body.get("error", str(body)))
            if isinstance(body, dict)
            else body
        )

        if resp.status_code == 401:
            raise SmartLinkAuthError(msg, status_code=401)
        if resp.status_code == 403:
            raise SmartLinkForbiddenError(msg, status_code=403)
        if resp.status_code == 404:
            raise SmartLinkNotFoundError(msg, status_code=404)
        if resp.status_code == 400:
            raise SmartLinkValidationError(msg, status_code=400)
        if resp.status_code >= 500:
            raise SmartLinkServerError(msg, status_code=resp.status_code)

        raise SmartLinkClientError(msg, status_code=resp.status_code)
