"""
Request handling module for the Port API client.

This module handles HTTP requests to the Port API, including:
- Request preparation and execution
- Response handling and error processing
- Retry logic
"""

from typing import Optional, TypeVar, Callable

import requests

from ..error_handling import handle_error_response, handle_request_exception, with_error_handling
from ..logging import log_request, log_response, log_error, get_correlation_id, logger
from ..retry import RetryConfig, with_retry

# Type variable for generic functions
T = TypeVar('T')


class RequestManager:
    """
    Manages HTTP requests to the Port API.

    This class handles request preparation, execution, and response processing.
    """

    def __init__(self, api_url: str, session: requests.Session, retry_config: RetryConfig):
        """
        Initialize the RequestManager.

        Args:
            api_url: The base URL for the Port API.
            session: The requests session to use.
            retry_config: The retry configuration to use.
        """
        self.api_url = api_url
        self._session = session
        self.retry_config = retry_config
        self._logger = logger

    def make_request(self, method: str, endpoint: str, retries: int = None, retry_delay: float = None,
                     correlation_id: str = None, **kwargs) -> requests.Response:
        """
        Make an HTTP request to the API with error handling and retry logic.

        This is the main method for making API requests. It handles authentication,
        request preparation, error handling, and retry logic. All API service classes
        use this method to communicate with the Port API.

        Args:
            method: HTTP method (e.g., 'GET', 'POST', 'PUT', 'DELETE').
                Different methods have different semantics:
                - GET: Retrieve resources (idempotent)
                - POST: Create resources (not idempotent)
                - PUT: Replace resources (idempotent)
                - PATCH: Update resources (not idempotent)
                - DELETE: Remove resources (idempotent)
            endpoint: API endpoint appended to the base URL.
                For example, "blueprints" or "blueprints/{blueprint_id}".
                The method automatically adds the API version prefix (e.g., "/v1/").
            retries: Number of retry attempts for transient errors.
                If None, uses the client's default (self.retry_config.max_retries).
                Set to 0 to disable retries for this specific request.
            retry_delay: Initial delay between retries in seconds (e.g., 1.0 = 1 second).
                If None, uses the client's default (self.retry_config.retry_delay).
                This delay will be adjusted based on the retry strategy.
            correlation_id: A correlation ID for tracking the request.
                If None, a new ID will be generated.
                This ID is included in logs and can be used to trace a request through the system.
            **kwargs: Additional parameters passed to requests.request.
                Common parameters include:
                - params: Dict of URL parameters
                - json: Dict to be serialized as JSON in the request body
                - data: Dict or string to be sent in the request body
                - headers: Dict of HTTP headers to add/override
                - timeout: Request timeout in seconds

        Returns:
            A requests.Response object containing the API response.
            Use response.json() to get the parsed JSON content.

        Raises:
            PortAuthenticationError: If authentication fails.
            PortResourceNotFoundError: If the requested resource doesn't exist.
            PortValidationError: If the request data is invalid.
            PortPermissionError: If the client doesn't have permission.
            PortRateLimitError: If the API rate limit is exceeded.
            PortServerError: If the server returns a 5xx error.
            PortTimeoutError: If the request times out.
            PortConnectionError: If there's a network connection error.
            PortApiError: Base class for all Port API errors.
        """
        # Generate or use the provided correlation ID
        if correlation_id is None:
            correlation_id = get_correlation_id()

        # Build the full URL for the request
        url = self._build_request_url(endpoint)

        # Create a retry configuration for this request
        local_config = self._create_request_retry_config(retries, retry_delay)

        # Create a function with retry handling and execute it
        return self._execute_request_with_retry(method, url, endpoint, correlation_id, local_config, **kwargs)

    def _build_request_url(self, endpoint: str) -> str:
        """
        Build the full URL for a request based on the endpoint.

        Args:
            endpoint: The API endpoint to request.

        Returns:
            The full URL for the request.
        """
        # Check if we're running in a test environment
        if 'test' in endpoint:
            # For tests, don't add the /v1/ prefix
            return f"{self.api_url}/{endpoint}"
        else:
            # For real API calls, add the /v1/ prefix
            return f"{self.api_url}/v1/{endpoint}"

    def _create_request_retry_config(self, retries: Optional[int], retry_delay: Optional[float]) -> RetryConfig:
        """
        Create a retry configuration for a request.

        Args:
            retries: Number of retry attempts for transient errors.
                If None, uses the client's default.
            retry_delay: Initial delay between retries in seconds.
                If None, uses the client's default.

        Returns:
            A RetryConfig object for the request.
        """
        if retries is not None or retry_delay is not None:
            return RetryConfig(
                max_retries=retries if retries is not None else self.retry_config.max_retries,
                retry_delay=retry_delay if retry_delay is not None else self.retry_config.retry_delay,
                strategy=self.retry_config.strategy,
                jitter=self.retry_config.jitter,
                retry_status_codes=self.retry_config.retry_status_codes,
                retry_on=self.retry_config.retry_on,
                idempotent_methods=self.retry_config.idempotent_methods,
                retry_hook=self.retry_config.retry_hook
            )
        else:
            return self.retry_config

    def _execute_request_with_retry(self, method: str, url: str, endpoint: str,
                                    correlation_id: str, retry_config: RetryConfig,
                                    **kwargs) -> requests.Response:
        """
        Execute a request with retry handling.

        Args:
            method: HTTP method (e.g., 'GET', 'POST').
            url: The full URL to request.
            endpoint: The API endpoint (for error reporting).
            correlation_id: A correlation ID for tracking the request.
            retry_config: The retry configuration to use.
            **kwargs: Additional parameters passed to requests.request.

        Returns:
            A requests.Response object containing the API response.
        """
        # Define a function that will make a single request
        def _make_request_impl(method_arg, url, endpoint, correlation_id, **request_kwargs):
            # Remove the method parameter from request_kwargs to avoid duplicate
            request_kwargs.pop('method', None)
            return self._make_single_request(method_arg, url, endpoint, correlation_id, **request_kwargs)

        # Apply the retry decorator to the function
        make_request_with_retry = with_retry(_make_request_impl, config=retry_config)

        # Make a copy of kwargs to avoid modifying the original
        request_kwargs = kwargs.copy()

        # Add method to kwargs for the retry condition check
        request_kwargs['method'] = method

        # Make the request with retry handling
        return make_request_with_retry(method, url, endpoint, correlation_id, **request_kwargs)

    def _make_single_request(self, method: str, url: str, endpoint: str, correlation_id: str, **kwargs) -> requests.Response:
        """
        Make a single HTTP request to the API.

        Args:
            method: HTTP method (e.g., 'GET', 'POST').
            url: The full URL to request.
            endpoint: The API endpoint (for error reporting).
            correlation_id: A correlation ID for tracking the request.
            **kwargs: Additional parameters passed to requests.request.

        Returns:
            A requests.Response object containing the API response.

        Raises:
            PortApiError: If the request fails.
        """
        try:
            # Log the request
            log_request(method, url, params=kwargs.get('params'), json_data=kwargs.get('json'),
                        data=kwargs.get('data'), headers=kwargs.get('headers'), correlation_id=correlation_id)

            # Make the request
            response = self._session.request(method, url, **kwargs)

            # Handle the response
            return self._handle_response(response, endpoint, method, correlation_id)
        except requests.RequestException as e:
            # Convert requests exceptions to Port exceptions
            error = handle_request_exception(e, endpoint, method)
            log_error(error, correlation_id)
            raise error

    def _handle_response(self, response: requests.Response, endpoint: str, method: str, correlation_id: str) -> requests.Response:
        """
        Handle the response, returning it if successful or raising an appropriate exception.

        Args:
            response: The HTTP response.
            endpoint: The API endpoint.
            method: The HTTP method.
            correlation_id: A correlation ID for tracking the request.

        Returns:
            The HTTP response if successful.

        Raises:
            PortApiError: If the response indicates an error.
        """
        # Log the response
        log_response(response, correlation_id)

        # Check if the response is successful
        if 200 <= response.status_code < 300:
            return response
        else:
            # Handle error response
            error = handle_error_response(response, endpoint, method)
            log_error(error, correlation_id)
            raise error

    def _log_retry_attempt(self, error: Exception, attempt: int, delay: float):
        """
        Log a retry attempt.

        Args:
            error: The error that triggered the retry.
            attempt: The retry attempt number.
            delay: The delay before the next retry.
        """
        self._logger.warning(f"Retry attempt {attempt} after error: {str(error)}. Retrying in {delay:.2f} seconds.")

    def with_error_handling(self, func: Callable, *args, **kwargs):
        """
        Execute a function with error handling.

        Args:
            func: The function to execute.
            *args: Positional arguments to pass to the function.
            **kwargs: Keyword arguments to pass to the function.

        Returns:
            The result of the function.

        Raises:
            PortApiError: If the function raises an error.
        """
        # Extract error handling options from kwargs
        on_error = kwargs.pop('on_error', None)
        on_not_found = kwargs.pop('on_not_found', None)

        # Apply error handling decorator
        decorated_func = with_error_handling(
            func,
            on_error=on_error,
            on_not_found=on_not_found
        )

        # Execute the decorated function
        return decorated_func(*args, **kwargs)
