"""GDB REST API client — async HTTP client for Django DRF backend."""

from __future__ import annotations

from typing import Any

import httpx

from mcp_eregistrations_gdb.gdb_client.errors import (
    GDBAuthError,
    GDBClientError,
    GDBNotFoundError,
    GDBServerError,
    GDBValidationError,
)

_DEFAULT_TIMEOUT = 30.0


class GDBClient:
    """Async HTTP client for GDB REST API."""

    def __init__(
        self,
        base_url: str,
        token: str | None = None,
        instance: str | None = None,
        timeout: float = _DEFAULT_TIMEOUT,
    ):
        self.base_url = base_url.rstrip("/")
        self._token = token
        self._instance = instance
        self._timeout = timeout
        self._client: httpx.AsyncClient | None = None

    @classmethod
    def for_instance(cls, instance: str | None = None) -> GDBClient:
        """Create a GDBClient from instance config.

        Uses the common config module (instances.yaml + component profiles).
        Token resolved lazily in __aenter__ from common auth module.
        """
        from mcp_eregistrations_common.config import get_instance_config

        config = get_instance_config(instance)
        gdb_url = config.get("gdb_url")
        if not gdb_url:
            from fastmcp.exceptions import ToolError

            raise ToolError(
                f"No gdb_url configured for instance '{instance}'. "
                f"Check instances.yaml or register with instance_add."
            )
        return cls(base_url=gdb_url, instance=instance)

    async def _resolve_token(self) -> str | None:
        """Resolve a fresh access token from common auth module."""
        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) -> GDBClient:
        if not self._token:
            self._token = await self._resolve_token()

        headers = {"Accept": "application/json"}
        if self._token:
            headers["Authorization"] = f"Bearer {self._token}"
        self._client = httpx.AsyncClient(
            base_url=self.base_url,
            headers=headers,
            timeout=self._timeout,
        )
        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:
        """Return the httpx client, raising if not in context manager."""
        assert self._client is not None, "Use 'async with GDBClient(...)' first"
        return self._client

    async def _refresh_and_update_header(self) -> bool:
        """Try to refresh the token and update the session header.

        Returns True if refresh succeeded, False otherwise.
        """
        new_token = await self._resolve_token()
        if new_token and new_token != self._token:
            self._token = new_token
            client = self._ensure_client()
            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] | list[Any] | None = None,
    ) -> Any:
        """Execute request with one retry on 401 (token refresh)."""
        client = self._ensure_client()
        resp = await client.request(method, path, params=params, json=json)

        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)

        return self._handle_response(resp)

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

    async def post(
        self, path: str, data: dict[str, Any] | list[Any] | None = None
    ) -> Any:
        """POST request with JSON body."""
        return await self._request_with_retry("POST", path, json=data)

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

    async def delete(self, path: str, **params: Any) -> Any:
        """DELETE request with optional query params."""
        return await self._request_with_retry("DELETE", path, params=params or None)

    async def get_page(
        self,
        path: str,
        page: int = 1,
        page_size: int = 100,
        **params: Any,
    ) -> dict[str, Any]:
        """Get a single page with normalized pagination metadata."""
        params["page"] = page
        params["page_size"] = page_size
        data = await self._request_with_retry("GET", path, params=params)
        if isinstance(data, list):
            return {
                "count": len(data),
                "current_page": 1,
                "has_next": False,
                "results": data,
            }
        return {
            "count": data.get("count", 0),
            "current_page": data.get("currentPage", page),
            "has_next": data.get("next") is not None,
            "results": data.get("results", []),
        }

    async def get_paginated(
        self,
        path: str,
        page_size: int = 100,
        max_pages: int = 50,
        **params: Any,
    ) -> list[Any]:
        """Fetch all pages and return combined results list.

        Repeatedly calls get_page until has_next is False or max_pages reached.
        """
        all_results: list[Any] = []
        page = 1
        for _ in range(max_pages):
            page_data = await self.get_page(
                path, page=page, page_size=page_size, **params
            )
            all_results.extend(page_data["results"])
            if not page_data["has_next"]:
                break
            page += 1
        return all_results

    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 {}
            return resp.json()

        try:
            body = resp.json()
        except Exception:
            body = resp.text

        msg = body.get("detail", str(body)) if isinstance(body, dict) else str(body)

        if resp.status_code in (401, 403):
            raise GDBAuthError(msg, status_code=resp.status_code)
        if resp.status_code == 404:
            raise GDBNotFoundError(msg, status_code=resp.status_code)
        if resp.status_code == 400:
            raise GDBValidationError(body, status_code=resp.status_code)
        if resp.status_code == 406:
            raise GDBValidationError(msg, status_code=resp.status_code)
        if resp.status_code >= 500:
            raise GDBServerError(msg, status_code=resp.status_code)

        raise GDBClientError(msg, status_code=resp.status_code)
