"""HTTP client for the DS Django REST Framework API."""

from __future__ import annotations

import secrets
from typing import Any

import httpx

from mcp_eregistrations_ds.ds_client.errors import (
    DSAuthenticationError,
    DSClientError,
    DSConnectionError,
    DSNotFoundError,
    DSPermissionError,
    DSServerError,
    DSTimeoutError,
)


class DSClient:
    """Async HTTP client for DS REST API.

    Usage::

        async with DSClient(base_url="https://jamaica.eregistrations.org") as c:
            files = await c.get_paginated("/backend/admin/files")
    """

    def __init__(
        self,
        base_url: str,
        token: str | None = None,
        timeout: float = 30.0,
        verify_ssl: bool = True,
    ):
        self._base_url = base_url.rstrip("/")
        self._token = token
        self._timeout = timeout
        self._verify_ssl = verify_ssl
        self._client: httpx.AsyncClient | None = None
        self._instance: str | None = None  # Set by for_instance()

    async def __aenter__(self) -> DSClient:
        # Try shared keyring if no in-memory token
        if not self._token and self._instance:
            from mcp_eregistrations_common.auth import get_or_refresh_token

            self._token = await get_or_refresh_token(self._instance)

        headers: dict[str, str] = {}
        if self._token:
            headers["Authorization"] = f"Bearer {self._token}"

        # Generate self-signed CSRF token pair.
        # DRF SessionAuthentication.enforce_csrf() compares the csrftoken
        # cookie against the X-CSRFToken header — a matching pair satisfies it.
        # Django also requires Referer header for HTTPS CSRF validation.
        csrf_token = secrets.token_hex(32)
        headers["X-CSRFToken"] = csrf_token
        headers["Referer"] = self._base_url + "/"

        self._client = httpx.AsyncClient(
            base_url=self._base_url,
            headers=headers,
            timeout=self._timeout,
            cookies={"csrftoken": csrf_token},
            verify=self._verify_ssl,
        )
        return self

    async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        if self._client:
            await self._client.aclose()
            self._client = None

    async def _refresh_and_update_header(self) -> bool:
        """Attempt token refresh and update client headers."""
        if not self._instance:
            return False
        from mcp_eregistrations_common.auth import get_or_refresh_token

        new_token = await get_or_refresh_token(self._instance)
        if new_token and new_token != self._token:
            self._token = new_token
            assert self._client is not None
            self._client.headers["Authorization"] = f"Bearer {new_token}"
            return True
        return False

    async def _request_with_retry(
        self, method: str, path: str, **kwargs: Any
    ) -> httpx.Response:
        """Execute request with one retry on 401 (token refresh)."""
        assert self._client is not None
        response = await self._client.request(method, path, **kwargs)
        if response.status_code == 401 and self._instance:
            refreshed = await self._refresh_and_update_header()
            if refreshed:
                response = await self._client.request(method, path, **kwargs)
        return response

    def _raise_for_status(self, response: httpx.Response) -> None:
        """Translate HTTP status codes to DS errors."""
        if response.is_success:
            return
        status = response.status_code
        detail = ""
        try:
            detail = response.json().get("detail", response.text)
        except Exception:
            detail = response.text

        if status == 401:
            raise DSAuthenticationError(detail, status_code=status)
        if status == 403:
            raise DSPermissionError(detail, status_code=status)
        if status == 404:
            raise DSNotFoundError(detail, status_code=status)
        if status >= 500:
            raise DSServerError(detail, status_code=status)
        # Generic 4xx client error
        raise DSClientError(f"HTTP {status}: {detail}", status_code=status)

    async def get(
        self,
        path: str,
        *,
        params: dict[str, Any] | None = None,
        timeout: float | None = None,
    ) -> Any:
        """GET request returning JSON response."""
        assert self._client is not None, (
            "DSClient must be used as async context manager"
        )
        kwargs: dict[str, Any] = {"params": params}
        if timeout is not None:
            kwargs["timeout"] = httpx.Timeout(timeout)
        try:
            response = await self._request_with_retry("GET", path, **kwargs)
        except httpx.ConnectError as e:
            raise DSConnectionError(str(e)) from e
        except httpx.TimeoutException as e:
            raise DSTimeoutError(str(e)) from e
        self._raise_for_status(response)
        return response.json()

    async def post(
        self,
        path: str,
        *,
        json: dict[str, Any] | None = None,
        params: dict[str, Any] | None = None,
    ) -> Any:
        """POST request with JSON body."""
        assert self._client is not None, (
            "DSClient must be used as async context manager"
        )
        try:
            response = await self._request_with_retry(
                "POST", path, json=json, params=params
            )
        except httpx.ConnectError as e:
            raise DSConnectionError(str(e)) from e
        except httpx.TimeoutException as e:
            raise DSTimeoutError(str(e)) from e
        self._raise_for_status(response)
        return response.json()

    async def put(
        self,
        path: str,
        *,
        json: dict[str, Any] | None = None,
        params: dict[str, Any] | None = None,
    ) -> Any:
        """PUT request with JSON body."""
        assert self._client is not None, (
            "DSClient must be used as async context manager"
        )
        try:
            response = await self._request_with_retry(
                "PUT", path, json=json, params=params
            )
        except httpx.ConnectError as e:
            raise DSConnectionError(str(e)) from e
        except httpx.TimeoutException as e:
            raise DSTimeoutError(str(e)) from e
        self._raise_for_status(response)
        return response.json()

    async def patch(
        self,
        path: str,
        *,
        json: dict[str, Any] | None = None,
        params: dict[str, Any] | None = None,
    ) -> Any:
        """PATCH request with JSON body."""
        assert self._client is not None, (
            "DSClient must be used as async context manager"
        )
        try:
            response = await self._request_with_retry(
                "PATCH", path, json=json, params=params
            )
        except httpx.ConnectError as e:
            raise DSConnectionError(str(e)) from e
        except httpx.TimeoutException as e:
            raise DSTimeoutError(str(e)) from e
        self._raise_for_status(response)
        return response.json()

    async def delete(
        self,
        path: str,
        *,
        params: dict[str, Any] | None = None,
    ) -> Any:
        """DELETE request."""
        assert self._client is not None, (
            "DSClient must be used as async context manager"
        )
        try:
            response = await self._request_with_retry("DELETE", path, params=params)
        except httpx.ConnectError as e:
            raise DSConnectionError(str(e)) from e
        except httpx.TimeoutException as e:
            raise DSTimeoutError(str(e)) from e
        self._raise_for_status(response)
        if response.status_code == 204 or not response.content:
            return {}
        return response.json()

    async def get_bytes(
        self,
        url: str,
        timeout: float = 60.0,
    ) -> tuple[bytes, str]:
        """GET request returning raw bytes and content-type.

        Relative URLs use the authenticated client. Absolute URLs
        (e.g. MinIO presigned) use a plain client without auth headers.

        Args:
            url: Document URL — relative (Django-served) or absolute (MinIO presigned).
            timeout: Per-request timeout in seconds (default: 60).
        """
        if url.startswith(("http://", "https://")):
            # Absolute URL (MinIO presigned) — no auth headers
            try:
                async with httpx.AsyncClient() as ext_client:
                    response = await ext_client.get(url, timeout=timeout)
            except httpx.ConnectError as e:
                raise DSConnectionError(str(e)) from e
            except httpx.TimeoutException as e:
                raise DSTimeoutError(str(e)) from e
        else:
            # Relative URL — use authenticated client
            assert self._client is not None, (
                "DSClient must be used as async context manager"
            )
            try:
                response = await self._request_with_retry("GET", url, timeout=timeout)
            except httpx.ConnectError as e:
                raise DSConnectionError(str(e)) from e
            except httpx.TimeoutException as e:
                raise DSTimeoutError(str(e)) from e
        self._raise_for_status(response)
        content_type = response.headers.get("content-type", "application/octet-stream")
        return response.content, content_type

    async def upload(
        self,
        path: str,
        *,
        file_data: bytes,
        file_name: str,
        field_name: str = "file",
        extra_fields: dict[str, str] | None = None,
        params: dict[str, Any] | None = None,
    ) -> Any:
        """Multipart file upload."""
        assert self._client is not None, (
            "DSClient must be used as async context manager"
        )
        files = {field_name: (file_name, file_data)}
        data = extra_fields or {}
        try:
            response = await self._request_with_retry(
                "POST", path, files=files, data=data, params=params
            )
        except httpx.ConnectError as e:
            raise DSConnectionError(str(e)) from e
        except httpx.TimeoutException as e:
            raise DSTimeoutError(str(e)) from e
        self._raise_for_status(response)
        return response.json()

    async def get_paginated(
        self,
        path: str,
        *,
        params: dict[str, Any] | None = None,
        timeout: float | None = None,
    ) -> dict[str, Any]:
        """GET for DRF paginated endpoints.

        Normalizes: results->items, adds has_next/page.
        """
        params = params or {}
        page = params.get("page", 1)
        raw = await self.get(path, params=params, timeout=timeout)

        # DRF PageNumberPagination format
        if isinstance(raw, dict) and "results" in raw:
            items = raw["results"]
            return {
                "items": items,
                "count": raw.get("count", len(items)),
                "page": page,
                "page_size": len(items),
                "has_next": raw.get("next") is not None,
            }
        # Non-paginated list response — apply client-side pagination
        if isinstance(raw, list):
            default_page_size = 20
            total = len(raw)
            start = (page - 1) * default_page_size
            end = start + default_page_size
            items = raw[start:end]
            return {
                "items": items,
                "count": total,
                "page": page,
                "page_size": len(items),
                "has_next": end < total,
            }
        # Already a dict without "results" key
        result: dict[str, Any] = raw
        return result

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

        Looks up ds_url from common config (built-in or custom profiles).
        Requires an explicit instance name — no env var fallback.
        Token is retrieved from DS's own in-process token store
        (populated by ds_auth_login), with keyring fallback on __aenter__.
        """
        from fastmcp.exceptions import ToolError

        from mcp_eregistrations_common.config import get_instance_config

        if instance is None:
            raise ToolError(
                "No instance specified. Pass instance='<name>'. "
                "Use instance_list to see available instances."
            )

        config = get_instance_config(instance)
        ds_url = config.get("ds_url")

        if not ds_url:
            raise ToolError(
                f"No DS URL configured for instance '{instance}'. "
                f"Use instance_add to register an instance."
            )

        token = _ds_tokens.get(instance)
        client = cls(
            base_url=ds_url,
            token=token,
            verify_ssl=config.get("verify_ssl", True),
        )
        client._instance = instance
        return client


# DS-local in-process token store (populated by ds_auth_login)
_ds_tokens: dict[str, str] = {}


def store_ds_token(instance: str, token: str) -> None:
    """Store an access token for a DS instance."""
    _ds_tokens[instance] = token


def get_ds_token(instance: str) -> str | None:
    """Get stored access token for a DS instance."""
    return _ds_tokens.get(instance)
