"""
Retry Mechanism - Decorator for automatic retry with exponential backoff

This module provides a retry decorator that automatically retries failed operations
with exponential backoff strategy.
"""

import time
import logging
from functools import wraps
from typing import Callable, Type, Tuple

from openfund_agent.datas.exceptions import RetryableError


def with_retry(
    max_retries: int = 3,
    delay: float = 1.0,
    backoff: float = 2.0,
    exceptions: Tuple[Type[Exception], ...] = (RetryableError,),
):
    """
    Decorator for automatic retry with exponential backoff

    This decorator will retry the decorated function if it raises one of the
    specified exceptions. The delay between retries increases exponentially.

    Args:
        max_retries: Maximum number of retry attempts (default: 3)
        delay: Initial delay in seconds between retries (default: 1.0)
        backoff: Backoff multiplier for exponential delay (default: 2.0)
        exceptions: Tuple of exception types to catch and retry (default: (RetryableError,))

    Returns:
        Decorated function with retry logic

    Example:
        @with_retry(max_retries=3, delay=1.0, backoff=2.0)
        def fetch_data():
            # This will be retried up to 3 times with exponential backoff
            return api.get_data()

    Retry delays:
        - Attempt 1: delay * backoff^0 = 1.0 seconds
        - Attempt 2: delay * backoff^1 = 2.0 seconds
        - Attempt 3: delay * backoff^2 = 4.0 seconds
    """
    logger = logging.getLogger(__name__)

    def decorator(func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args, **kwargs):
            current_delay = delay
            last_exception = None

            for attempt in range(max_retries):
                try:
                    return func(*args, **kwargs)

                except exceptions as e:
                    last_exception = e

                    # Check if this is the last attempt
                    if attempt == max_retries - 1:
                        logger.error(
                            f"{func.__name__} failed after {max_retries} attempts: {str(e)}"
                        )
                        raise

                    # Log retry attempt
                    logger.warning(
                        f"{func.__name__} failed (attempt {attempt + 1}/{max_retries}): {str(e)}. "
                        f"Retrying in {current_delay:.1f} seconds..."
                    )

                    # Wait before retry
                    time.sleep(current_delay)

                    # Increase delay for next attempt (exponential backoff)
                    current_delay *= backoff

                except Exception as e:
                    # Non-retryable exception, raise immediately
                    logger.error(f"{func.__name__} failed with non-retryable error: {str(e)}")
                    raise

            # This should never be reached, but just in case
            if last_exception:
                raise last_exception

        return wrapper

    return decorator


def with_timeout_retry(
    max_retries: int = 3, delay: float = 1.0, backoff: float = 2.0, timeout: float = 30.0
):
    """
    Decorator for retry with timeout

    Similar to with_retry but also enforces a timeout for each attempt.

    Args:
        max_retries: Maximum number of retry attempts
        delay: Initial delay in seconds between retries
        backoff: Backoff multiplier for exponential delay
        timeout: Timeout in seconds for each attempt

    Returns:
        Decorated function with retry and timeout logic

    Note:
        This requires the function to support timeout parameter or
        implement timeout logic internally.
    """
    logger = logging.getLogger(__name__)

    def decorator(func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args, **kwargs):
            current_delay = delay
            last_exception = None

            for attempt in range(max_retries):
                try:
                    # Add timeout to kwargs if function supports it
                    if "timeout" not in kwargs:
                        kwargs["timeout"] = timeout

                    return func(*args, **kwargs)

                except (RetryableError, TimeoutError) as e:
                    last_exception = e

                    if attempt == max_retries - 1:
                        logger.error(
                            f"{func.__name__} failed after {max_retries} attempts: {str(e)}"
                        )
                        raise

                    logger.warning(
                        f"{func.__name__} failed (attempt {attempt + 1}/{max_retries}): {str(e)}. "
                        f"Retrying in {current_delay:.1f} seconds..."
                    )

                    time.sleep(current_delay)
                    current_delay *= backoff

                except Exception as e:
                    logger.error(f"{func.__name__} failed with non-retryable error: {str(e)}")
                    raise

            if last_exception:
                raise last_exception

        return wrapper

    return decorator
