"""
Base HTTP client for fabias API interactions.

Provides common functionality for all API clients including:
- Authentication header management
- Request/response handling with retries
- Error handling and status code parsing
- Long-running operation (Job) tracking
- Automatic pagination handling
"""

import json
from abc import ABC
from time import sleep
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional

import requests

from .auth import AuthProvider
from .exceptions import ApiError, AuthenticationError

if TYPE_CHECKING:
    from .response import AzResponse


class BaseClient(ABC):
    """
    Abstract base HTTP client for Azure/Fabric API interactions.

    Provides common functionality for authenticated API requests including
    automatic token management, retry logic, and error handling.

    Subclasses should set _base_uri and _scope, and optionally override
    authentication behavior.

    Attributes:
        _base_uri (str): Base URI for API requests
        _scope (str): OAuth2 scope for authentication
        _auth (AuthProvider): Authentication provider instance
        _default_headers (dict): Default headers for all requests

    Examples:
        >>> class FabricClient(BaseClient):
        ...     def __init__(self, auth):
        ...         super().__init__(auth)
        ...         self._base_uri = "https://api.fabric.microsoft.com/v1"
        ...         self._scope = "https://api.fabric.microsoft.com/.default"
    """

    def __init__(self, auth: AuthProvider):
        """
        Initialize the base client with an authentication provider.

        Args:
            auth: Authentication provider for obtaining tokens
        """
        self._auth = auth
        self._base_uri = ""
        self._scope = ""
        self._default_headers = {"Content-Type": "application/json"}

    def _get_headers(self, additional_headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
        """
        Build request headers with authentication.

        Args:
            additional_headers: Extra headers to include

        Returns:
            dict: Complete headers dictionary
        """
        headers = self._default_headers.copy()
        headers["Authorization"] = self._auth.getAuthorizationHeader(self._scope)

        if additional_headers:
            headers.update(additional_headers)

        return headers

    def _handle_response(self, response: requests.Response) -> "AzResponse":
        """
        Process response and raise appropriate errors for failures.

        Converts 200/202 responses to AzResponse for automatic job tracking.

        Args:
            response: HTTP response object

        Returns:
            AzResponse: Wrapped response for all successful requests

        Raises:
            AuthenticationError: For 401 responses
            ApiError: For other error status codes
        """
        if response.status_code == 401:
            raise AuthenticationError(f"Authentication failed: {response.text}")

        if response.status_code >= 400:
            error_data = {}
            error_msg = response.text

            try:
                if response.headers.get("content-type", "").startswith("application/json"):
                    error_data = response.json()
                    # Try common error message locations
                    error_msg = (
                        error_data.get("error", {}).get("message")
                        or error_data.get("message")
                        or error_data.get("error_description")
                        or response.text
                    )
            except Exception:
                pass

            raise ApiError(
                f"HTTP {response.status_code}: {error_msg}",
                status_code=response.status_code,
                response_body=error_data or response.text,
            )

        from .response import AzResponse

        return AzResponse(response, self)

    def _request_with_retry(
        self, request_func: Callable[[], requests.Response], retry_on_401: bool = True
    ) -> "AzResponse":
        """
        Execute request with automatic retry on authentication errors.

        Args:
            request_func: Function that makes the HTTP request
            retry_on_401: Whether to retry on 401 errors

        Returns:
            AzResponse: Successful response

        Raises:
            ApiError: If request fails after retries
        """
        try:
            response = request_func()

            # Retry once on 401
            if response.status_code == 401 and retry_on_401:
                sleep(1)  # Brief delay before retry
                response = request_func()

            return self._handle_response(response)

        except requests.RequestException as e:
            raise ApiError(f"Request failed: {str(e)}", cause=e)

    def get(
        self,
        endpoint: str,
        headers: Optional[Dict[str, str]] = None,
        params: Optional[Dict[str, Any]] = None,
    ) -> "AzResponse":
        """
        Execute HTTP GET request with automatic pagination.

        Automatically handles continuationToken and continuationUri pagination
        by fetching all pages and combining results into a single response.

        Args:
            endpoint: API endpoint (relative to base URI, or absolute)
            headers: Additional headers
            params: Query parameters

        Returns:
            AzResponse: Response object with all paginated results combined
        """
        url = endpoint if endpoint.startswith("http") else f"{self._base_uri}{endpoint}"

        # First request
        response = self._request_with_retry(
            lambda: requests.get(url, headers=self._get_headers(headers), params=params)
        )

        # Parse response to check for pagination
        try:
            data = response.json()
        except Exception:
            # Not JSON or can't parse - return as-is
            return response

        # Check for pagination markers
        continuation_token = data.get("continuationToken")
        continuation_uri = data.get("continuationUri")

        if not (continuation_token or continuation_uri):
            # No pagination - return original response
            return response

        # Pagination detected - collect all pages
        all_values = list(data.get("value", []))

        while continuation_token or continuation_uri:
            # Use continuationUri if available, otherwise append token to original endpoint
            if continuation_uri:
                next_url = continuation_uri
            else:
                # Append continuationToken to original endpoint
                separator = "&" if "?" in url else "?"
                next_url = f"{url}{separator}continuationToken={continuation_token}"

            next_response = self._request_with_retry(
                lambda: requests.get(next_url, headers=self._get_headers(headers))
            )
            next_data = next_response.json()

            # Collect values from this page
            all_values.extend(next_data.get("value", []))

            # Get next continuation markers
            continuation_token = next_data.get("continuationToken")
            continuation_uri = next_data.get("continuationUri")

        # Update response with combined values
        data["value"] = all_values
        data.pop("continuationToken", None)
        data.pop("continuationUri", None)

        # Replace response content with combined data
        response._content = json.dumps(data).encode("utf-8")

        return response

    def post(
        self,
        endpoint: str,
        data: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
    ) -> "AzResponse":
        """
        Execute HTTP POST request.

        Args:
            endpoint: API endpoint (relative to base URI, or absolute)
            data: JSON body data
            headers: Additional headers

        Returns:
            AzResponse: Response object
        """
        url = endpoint if endpoint.startswith("http") else f"{self._base_uri}{endpoint}"

        return self._request_with_retry(
            lambda: requests.post(url, json=data, headers=self._get_headers(headers))
        )

    def put(
        self,
        endpoint: str,
        data: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
    ) -> "AzResponse":
        """
        Execute HTTP PUT request.

        Args:
            endpoint: API endpoint (relative to base URI, or absolute)
            data: JSON body data
            headers: Additional headers

        Returns:
            AzResponse: Response object
        """
        url = endpoint if endpoint.startswith("http") else f"{self._base_uri}{endpoint}"

        return self._request_with_retry(
            lambda: requests.put(url, json=data, headers=self._get_headers(headers))
        )

    def patch(
        self,
        endpoint: str,
        data: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
    ) -> "AzResponse":
        """
        Execute HTTP PATCH request.

        Args:
            endpoint: API endpoint (relative to base URI, or absolute)
            data: JSON body data
            headers: Additional headers

        Returns:
            AzResponse: Response object
        """
        url = endpoint if endpoint.startswith("http") else f"{self._base_uri}{endpoint}"

        return self._request_with_retry(
            lambda: requests.patch(url, json=data, headers=self._get_headers(headers))
        )

    def delete(self, endpoint: str, headers: Optional[Dict[str, str]] = None) -> "AzResponse":
        """
        Execute HTTP DELETE request.

        Args:
            endpoint: API endpoint (relative to base URI, or absolute)
            headers: Additional headers

        Returns:
            AzResponse: Response object
        """
        url = endpoint if endpoint.startswith("http") else f"{self._base_uri}{endpoint}"

        return self._request_with_retry(
            lambda: requests.delete(url, headers=self._get_headers(headers))
        )

    def postMultipart(
        self,
        endpoint: str,
        files: Dict[str, Any],
        data: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
    ) -> "AzResponse":
        """
        Execute HTTP POST request with multipart/form-data.

        Args:
            endpoint: API endpoint
            files: Files dictionary for multipart upload
            data: Additional form data
            headers: Additional headers (Content-Type is set automatically)

        Returns:
            AzResponse: Response object
        """
        url = endpoint if endpoint.startswith("http") else f"{self._base_uri}{endpoint}"

        # Don't set Content-Type for multipart - requests handles it
        req_headers = self._get_headers(headers)
        req_headers.pop("Content-Type", None)

        return self._request_with_retry(
            lambda: requests.post(url, files=files, data=data, headers=req_headers)
        )
