"""
Retry Logic with Exponential Backoff

Provides decorators for automatic retry with exponential backoff on transient failures.
"""

import asyncio
from functools import wraps
from typing import Callable, TypeVar, Tuple, Type
from .exceptions import NetworkError, RateLimitError

T = TypeVar('T')


def retry_with_backoff(
    max_retries: int = 3,
    base_delay: float = 2.0,
    backoff_factor: float = 2.0,
    retry_exceptions: Tuple[Type[Exception], ...] = (NetworkError, TimeoutError, ConnectionError)
):
    """
    Decorator that retries a function with exponential backoff.
    
    Args:
        max_retries: Maximum number of retry attempts (default: 3)
        base_delay: Initial delay in seconds (default: 2.0)
        backoff_factor: Multiplier for delay on each retry (default: 2.0)
        retry_exceptions: Tuple of exceptions that trigger a retry
    
    Example:
        @retry_with_backoff(max_retries=3, base_delay=1.0)
        async def api_call():
            # May raise NetworkError
            return await some_network_request()
    
    Behavior:
        - Attempt 1: Immediate
        - Attempt 2: Wait 2.0s (base_delay)
        - Attempt 3: Wait 4.0s (base_delay * backoff_factor)
        - Attempt 4: Wait 8.0s (base_delay * backoff_factor^2)
        
        RateLimitError is always retried with its retry_after value,
        regardless of backoff settings.
    """
    def decorator(func: Callable[..., T]) -> Callable[..., T]:
        @wraps(func)
        async def wrapper(*args, **kwargs) -> T:
            last_exception = None
            
            for attempt in range(max_retries):
                try:
                    return await func(*args, **kwargs)
                    
                except RateLimitError as e:
                    # Always respect rate limit retry_after
                    last_exception = e
                    if attempt < max_retries - 1:
                        await asyncio.sleep(e.retry_after)
                    else:
                        raise
                        
                except retry_exceptions as e:
                    last_exception = e
                    if attempt < max_retries - 1:
                        # Calculate exponential backoff
                        delay = base_delay * (backoff_factor ** attempt)
                        await asyncio.sleep(delay)
                    else:
                        raise
                        
                except Exception as e:
                    # Don't retry on unexpected exceptions
                    raise
            
            # This should never be reached, but just in case
            if last_exception:
                raise last_exception
            raise NetworkError(f"Failed after {max_retries} retries")
            
        return wrapper
    return decorator


def with_timeout(seconds: float):
    """
    Decorator that adds a timeout to an async function.
    
    Args:
        seconds: Timeout in seconds
    
    Example:
        @with_timeout(30.0)
        async def slow_operation():
            await asyncio.sleep(60)  # Will timeout after 30s
    """
    def decorator(func: Callable[..., T]) -> Callable[..., T]:
        @wraps(func)
        async def wrapper(*args, **kwargs) -> T:
            try:
                return await asyncio.wait_for(func(*args, **kwargs), timeout=seconds)
            except asyncio.TimeoutError:
                raise NetworkError(f"Operation timed out after {seconds} seconds")
        return wrapper
    return decorator
