"""
Retry utilities for LLM API calls (Issue #38).

Provides exponential backoff retry decorators for handling transient
LLM API failures including rate limits, server errors, and timeouts.
"""

import logging
import functools
from typing import Type, Tuple, Callable, Any
from tenacity import (
    retry,
    stop_after_attempt,
    wait_exponential_jitter,
    retry_if_exception_type,
    before_sleep_log,
    RetryError,
)

logger = logging.getLogger(__name__)


# =============================================================================
# Retryable Exception Types
# =============================================================================

class LLMRetryableError(Exception):
    """Base class for retryable LLM errors."""
    pass


class LLMRateLimitError(LLMRetryableError):
    """Rate limit exceeded (HTTP 429)."""
    pass


class LLMServerError(LLMRetryableError):
    """Server-side error (HTTP 5xx)."""
    pass


class LLMTimeoutError(LLMRetryableError):
    """Connection or request timeout."""
    pass


class LLMThrottlingError(LLMRetryableError):
    """AWS/Cloud provider throttling."""
    pass


# Default retryable exceptions tuple
RETRYABLE_EXCEPTIONS: Tuple[Type[Exception], ...] = (
    LLMRateLimitError,
    LLMServerError,
    LLMTimeoutError,
    LLMThrottlingError,
)


# =============================================================================
# Retry Decorator Factory
# =============================================================================

def llm_retry(
    max_attempts: int = 3,
    min_wait: float = 1.0,
    max_wait: float = 10.0,
    jitter: float = 1.0,
    retryable_exceptions: Tuple[Type[Exception], ...] = RETRYABLE_EXCEPTIONS,
):
    """
    Create a retry decorator for LLM API calls.
    
    Uses exponential backoff with jitter to handle transient failures:
    - HTTP 429 (Rate Limited)
    - HTTP 500, 502, 503, 504 (Server Errors)  
    - Connection timeouts
    - AWS throttling exceptions
    
    Args:
        max_attempts: Maximum number of retry attempts (default: 3)
        min_wait: Minimum wait time in seconds (default: 1.0)
        max_wait: Maximum wait time in seconds (default: 10.0)
        jitter: Random jitter added to wait time (default: 1.0)
        retryable_exceptions: Tuple of exception types to retry
        
    Returns:
        Decorator function
        
    Example:
        @llm_retry(max_attempts=3)
        async def call_llm(prompt: str) -> str:
            ...
    """
    return retry(
        stop=stop_after_attempt(max_attempts),
        wait=wait_exponential_jitter(initial=min_wait, max=max_wait, jitter=jitter),
        retry=retry_if_exception_type(retryable_exceptions),
        before_sleep=before_sleep_log(logger, logging.WARNING),
        reraise=True,
    )


def llm_retry_sync(
    max_attempts: int = 3,
    min_wait: float = 1.0,
    max_wait: float = 10.0,
    jitter: float = 1.0,
    retryable_exceptions: Tuple[Type[Exception], ...] = RETRYABLE_EXCEPTIONS,
):
    """
    Synchronous version of llm_retry for sync functions.
    
    Same parameters as llm_retry().
    """
    return retry(
        stop=stop_after_attempt(max_attempts),
        wait=wait_exponential_jitter(initial=min_wait, max=max_wait, jitter=jitter),
        retry=retry_if_exception_type(retryable_exceptions),
        before_sleep=before_sleep_log(logger, logging.WARNING),
        reraise=True,
    )


# =============================================================================
# Error Classification Helpers
# =============================================================================

def classify_http_error(status_code: int, message: str = "") -> Exception:
    """
    Classify an HTTP error into the appropriate exception type.
    
    Args:
        status_code: HTTP status code
        message: Error message
        
    Returns:
        Appropriate exception instance
    """
    if status_code == 429:
        return LLMRateLimitError(f"Rate limited: {message}")
    elif status_code in (500, 502, 503, 504):
        return LLMServerError(f"Server error ({status_code}): {message}")
    else:
        # Non-retryable client error (4xx except 429)
        return RuntimeError(f"HTTP {status_code}: {message}")


def classify_boto_error(error_code: str, message: str = "") -> Exception:
    """
    Classify a boto3/botocore error into the appropriate exception type.
    
    Args:
        error_code: AWS error code (e.g., 'ThrottlingException')
        message: Error message
        
    Returns:
        Appropriate exception instance
    """
    throttling_codes = {
        'ThrottlingException',
        'Throttling',
        'TooManyRequestsException',
        'RequestThrottled',
        'ProvisionedThroughputExceededException',
        'ServiceUnavailableException',
        'ModelStreamErrorException',
    }
    
    server_error_codes = {
        'InternalServerException',
        'ServiceException',
        'InternalFailure',
    }
    
    if error_code in throttling_codes:
        return LLMThrottlingError(f"AWS throttling ({error_code}): {message}")
    elif error_code in server_error_codes:
        return LLMServerError(f"AWS server error ({error_code}): {message}")
    else:
        # Non-retryable AWS error
        return RuntimeError(f"AWS error ({error_code}): {message}")


def is_retryable_exception(exc: Exception) -> bool:
    """Check if an exception is retryable."""
    return isinstance(exc, RETRYABLE_EXCEPTIONS)
