"""
HTTP client for the Edupaid API.

Provides a thin wrapper around httpx.Client with:
- Automatic authentication header injection via x-api-key
- Retry logic for rate-limited requests (429)
- Structured error handling matching the API's {"error": "string"} format

Used by: edupaid/service.py for making API requests.

Reference: edupaid/docs/edupaid_api_docs.md for API error responses.
"""

import time
from typing import Any, Dict, Optional

import httpx

from edupaid.errors import (
    AuthError,
    NotFoundError,
    RateLimitError,
    RequestError,
    ServerError,
    ValidationError,
)
from edupaid.models.errors import EdupaidErrorResponse


class HttpClient:
    """
    HTTP client for the Edupaid API with authentication and retry logic.

    Handles authentication via x-api-key header as required by the Edupaid API.
    Implements retry logic for rate-limited requests (429) with exponential backoff.

    Used by: edupaid/service.py::EdupaidService for making API requests.

    Attributes:
        _base_url: Base URL for the API (no trailing slash).
        _api_key: API key for x-api-key header authentication.
        _client: The underlying httpx.Client instance.
    """

    def __init__(self, base_url: str, api_key: str):
        """
        Initialize the HTTP client.

        Args:
            base_url: The base URL for the Edupaid API.
            api_key: API key for x-api-key header authentication.
        """
        self._base_url = base_url.rstrip("/")
        self._api_key = api_key
        self._client = httpx.Client(timeout=httpx.Timeout(30.0, connect=10.0))

    def _headers(self) -> Dict[str, str]:
        """
        Build request headers with authentication.

        Returns:
            Dict containing x-api-key header, Accept header, and Content-Type header.
        """
        return {
            "x-api-key": self._api_key,
            "Accept": "application/json",
            "Content-Type": "application/json",
        }

    def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
        """
        Make a GET request to the API.

        Args:
            path: API endpoint path (e.g., "/get-app-authorization").
            params: Optional query parameters.

        Returns:
            Parsed JSON response as a dictionary.

        Raises:
            AuthError: For 401 Unauthorized responses.
            NotFoundError: For 404 Not Found responses.
            RateLimitError: For 429 Too Many Requests (after retries exhausted).
            ServerError: For 5xx server errors.
            RequestError: For other non-2xx responses.
        """
        url = f"{self._base_url}{path}"
        attempt = 0
        max_attempts = 3

        while True:
            attempt += 1
            start = time.time()
            try:
                resp = self._client.get(url, headers=self._headers(), params=params)
                duration = (time.time() - start) * 1000
                request_id = resp.headers.get("x-request-id") or resp.headers.get(
                    "x-amzn-requestid"
                )
                if 200 <= resp.status_code < 300:
                    return resp.json()
                self._raise_for_status(resp, request_id, duration)
            except RateLimitError:
                if attempt < max_attempts:
                    time.sleep(0.5 * attempt)
                    continue
                raise

    def post(self, path: str, json: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
        """
        Make a POST request to the API.

        Args:
            path: API endpoint path (e.g., "/get-app-authorization").
            json: Request body as a dictionary.

        Returns:
            Parsed JSON response as a dictionary.

        Raises:
            AuthError: For 401 Unauthorized responses.
            ValidationError: For 400 Bad Request responses.
            NotFoundError: For 404 Not Found responses.
            RateLimitError: For 429 Too Many Requests (after retries exhausted).
            ServerError: For 5xx server errors.
            RequestError: For other non-2xx responses.
        """
        url = f"{self._base_url}{path}"
        attempt = 0
        max_attempts = 3

        while True:
            attempt += 1
            start = time.time()
            try:
                resp = self._client.post(url, headers=self._headers(), json=json)
                duration = (time.time() - start) * 1000
                request_id = resp.headers.get("x-request-id") or resp.headers.get(
                    "x-amzn-requestid"
                )
                if 200 <= resp.status_code < 300:
                    return resp.json()
                self._raise_for_status(resp, request_id, duration)
            except RateLimitError:
                if attempt < max_attempts:
                    time.sleep(0.5 * attempt)
                    continue
                raise

    def delete(self, path: str) -> Optional[Dict[str, Any]]:
        """
        Make a DELETE request to the API.

        Args:
            path: API endpoint path.

        Returns:
            Parsed JSON response as a dictionary, or None for empty responses.

        Raises:
            AuthError: For 401 Unauthorized responses.
            NotFoundError: For 404 Not Found responses.
            RateLimitError: For 429 Too Many Requests (after retries exhausted).
            ServerError: For 5xx server errors.
            RequestError: For other non-2xx responses.
        """
        url = f"{self._base_url}{path}"
        attempt = 0
        max_attempts = 3

        while True:
            attempt += 1
            start = time.time()
            try:
                resp = self._client.delete(url, headers=self._headers())
                duration = (time.time() - start) * 1000
                request_id = resp.headers.get("x-request-id") or resp.headers.get(
                    "x-amzn-requestid"
                )
                if 200 <= resp.status_code < 300:
                    if not resp.text:
                        return None
                    return resp.json()
                self._raise_for_status(resp, request_id, duration)
            except RateLimitError:
                if attempt < max_attempts:
                    time.sleep(0.5 * attempt)
                    continue
                raise

    def put(self, path: str, json: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
        """
        Make a PUT request to the API.

        Args:
            path: API endpoint path.
            json: Request body as a dictionary.

        Returns:
            Parsed JSON response as a dictionary.

        Raises:
            AuthError: For 401 Unauthorized responses.
            ValidationError: For 400 Bad Request responses.
            NotFoundError: For 404 Not Found responses.
            RateLimitError: For 429 Too Many Requests (after retries exhausted).
            ServerError: For 5xx server errors.
            RequestError: For other non-2xx responses.
        """
        url = f"{self._base_url}{path}"
        attempt = 0
        max_attempts = 3

        while True:
            attempt += 1
            start = time.time()
            try:
                resp = self._client.put(url, headers=self._headers(), json=json)
                duration = (time.time() - start) * 1000
                request_id = resp.headers.get("x-request-id") or resp.headers.get(
                    "x-amzn-requestid"
                )
                if 200 <= resp.status_code < 300:
                    return resp.json()
                self._raise_for_status(resp, request_id, duration)
            except RateLimitError:
                if attempt < max_attempts:
                    time.sleep(0.5 * attempt)
                    continue
                raise

    @staticmethod
    def _raise_for_status(
        resp: httpx.Response, request_id: Optional[str], duration_ms: float
    ) -> None:
        """
        Raise appropriate exception based on HTTP status code.

        Parses error responses according to the Edupaid API format: {"error": "string"}

        Args:
            resp: The httpx Response object.
            request_id: Request ID from response headers (for debugging).
            duration_ms: Request duration in milliseconds.

        Raises:
            AuthError: For 401 Unauthorized.
            ValidationError: For 400 Bad Request.
            NotFoundError: For 404 Not Found.
            RateLimitError: For 429 Too Many Requests.
            ServerError: For 5xx errors.
            RequestError: For other non-2xx responses.
        """
        status = resp.status_code
        method = resp.request.method
        path = resp.request.url.path

        # Build request context for error message
        request_ctx = f"{method} {path}"
        if request_id:
            request_ctx += f" (Request ID: {request_id})"

        # Try to parse error response as {"error": "string"}
        # Store both the raw dict and the extracted error message
        error_dict = None
        body_excerpt = resp.text[:500] if resp.text else ""

        try:
            data = resp.json()
            if isinstance(data, dict):
                # Validate against our expected error format
                EdupaidErrorResponse.model_validate(data)
                error_dict = data  # Keep the raw dict for error_details
        except Exception:
            # If JSON parsing fails, we'll use the body excerpt in the message
            pass

        # Build context message - include body only if we couldn't parse the error
        context_msg = request_ctx
        if not error_dict and body_excerpt:
            context_msg += f"\nResponse: {body_excerpt}"

        # Raise appropriate exception based on status code
        # Per API docs:
        # - 400: Bad Request (invalid input)
        # - 401: Unauthorized (invalid or missing API key)
        # - 404: Not Found (resource not found)
        # - 429: Too Many Requests (rate limited - not in docs but handled for robustness)
        # - 5xx: Server Error

        if status == 400:
            raise ValidationError(context_msg, error_details=error_dict)
        if status == 401:
            raise AuthError(context_msg, error_details=error_dict)
        if status == 404:
            raise NotFoundError(context_msg, error_details=error_dict)
        if status == 429:
            raise RateLimitError(context_msg, error_details=error_dict)
        if 500 <= status < 600:
            raise ServerError(context_msg, error_details=error_dict)

        # Catch-all for other non-2xx responses (403, 405, 422, etc.)
        raise RequestError(context_msg, error_details=error_dict)
