#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Intelligent Error Handling & Retry System
Provides adaptive retry strategies, error classification, and circuit breaker pattern
"""

import time
import logging
from typing import Callable, Any, Optional, Dict, List
from enum import Enum
from functools import wraps
from datetime import datetime, timedelta
from selenium.common.exceptions import (
    TimeoutException,
    NoSuchElementException,
    StaleElementReferenceException,
    WebDriverException,
    ElementNotInteractableException,
    ElementClickInterceptedException
)


class ErrorType(Enum):
    """Error type classification."""
    NETWORK = "network"
    TIMEOUT = "timeout"
    ELEMENT_NOT_FOUND = "element_not_found"
    ELEMENT_STALE = "element_stale"
    ELEMENT_NOT_INTERACTABLE = "element_not_interactable"
    DRIVER_ERROR = "driver_error"
    UNKNOWN = "unknown"


class ErrorClassifier:
    """Classifies errors into categories for adaptive retry strategies."""
    
    @staticmethod
    def classify(error: Exception) -> ErrorType:
        """Classify an error into a category.
        
        Args:
            error: Exception to classify
            
        Returns:
            ErrorType enum value
        """
        error_str = str(error).lower()
        error_type = type(error).__name__
        
        # Network errors
        if any(keyword in error_str for keyword in ['connection', 'network', 'dns', 'refused', 'timeout']):
            if 'timeout' in error_str or isinstance(error, TimeoutException):
                return ErrorType.TIMEOUT
            return ErrorType.NETWORK
        
        # Timeout errors
        if isinstance(error, TimeoutException) or 'timeout' in error_str:
            return ErrorType.TIMEOUT
        
        # Element not found
        if isinstance(error, NoSuchElementException) or 'no such element' in error_str:
            return ErrorType.ELEMENT_NOT_FOUND
        
        # Stale element
        if isinstance(error, StaleElementReferenceException) or 'stale' in error_str:
            return ErrorType.ELEMENT_STALE
        
        # Element not interactable
        if isinstance(error, (ElementNotInteractableException, ElementClickInterceptedException)):
            return ErrorType.ELEMENT_NOT_INTERACTABLE
        
        # Driver errors
        if isinstance(error, WebDriverException) or 'driver' in error_str:
            return ErrorType.DRIVER_ERROR
        
        return ErrorType.UNKNOWN
    
    @staticmethod
    def get_retry_delay(error_type: ErrorType, attempt: int, base_delay: float = 1.0, 
                       max_delay: float = 10.0, exponential_base: float = 2.0) -> float:
        """Get retry delay based on error type and attempt number.
        
        Args:
            error_type: Type of error
            attempt: Current attempt number (1-based)
            base_delay: Base delay in seconds
            max_delay: Maximum delay in seconds
            exponential_base: Base for exponential backoff
            
        Returns:
            Delay in seconds
        """
        # Different strategies for different error types
        if error_type == ErrorType.NETWORK:
            # Network errors: longer delays, exponential backoff
            delay = base_delay * (exponential_base ** (attempt - 1))
        elif error_type == ErrorType.TIMEOUT:
            # Timeout errors: moderate delays
            delay = base_delay * (1.5 ** (attempt - 1))
        elif error_type == ErrorType.ELEMENT_STALE:
            # Stale element: short delay, retry quickly
            delay = base_delay * 0.5
        elif error_type == ErrorType.ELEMENT_NOT_FOUND:
            # Element not found: moderate delay
            delay = base_delay * attempt
        else:
            # Default: exponential backoff
            delay = base_delay * (exponential_base ** (attempt - 1))
        
        return min(delay, max_delay)
    
    @staticmethod
    def should_retry(error_type: ErrorType, attempt: int, max_retries: int) -> bool:
        """Determine if an error should be retried.
        
        Args:
            error_type: Type of error
            attempt: Current attempt number
            max_retries: Maximum number of retries
            
        Returns:
            True if should retry, False otherwise
        """
        if attempt >= max_retries:
            return False
        
        # Some error types should not be retried
        if error_type == ErrorType.UNKNOWN:
            # Unknown errors: retry once
            return attempt < 2
        
        return True


class CircuitBreaker:
    """Circuit breaker pattern for handling repeated failures."""
    
    def __init__(self, threshold: int = 5, timeout: int = 60):
        """Initialize circuit breaker.
        
        Args:
            threshold: Number of failures before opening circuit
            timeout: Time in seconds before attempting to close circuit
        """
        self.threshold = threshold
        self.timeout = timeout
        self.failure_count = 0
        self.last_failure_time: Optional[datetime] = None
        self.state = "closed"  # closed, open, half_open
    
    def record_success(self):
        """Record a successful operation."""
        self.failure_count = 0
        self.state = "closed"
    
    def record_failure(self):
        """Record a failed operation."""
        self.failure_count += 1
        self.last_failure_time = datetime.now()
        
        if self.failure_count >= self.threshold:
            self.state = "open"
    
    def can_attempt(self) -> bool:
        """Check if an operation can be attempted.
        
        Returns:
            True if operation can be attempted, False otherwise
        """
        if self.state == "closed":
            return True
        
        if self.state == "open":
            if self.last_failure_time:
                elapsed = (datetime.now() - self.last_failure_time).total_seconds()
                if elapsed >= self.timeout:
                    self.state = "half_open"
                    return True
            return False
        
        # half_open state
        return True
    
    def get_state(self) -> str:
        """Get current circuit breaker state.
        
        Returns:
            State string: "closed", "open", or "half_open"
        """
        return self.state


class RetryHandler:
    """Handles retries with adaptive strategies."""
    
    def __init__(self, config: Optional[Dict[str, Any]] = None):
        """Initialize retry handler.
        
        Args:
            config: Configuration dictionary with retry settings
        """
        self.config = config or {}
        self.max_retries = self.config.get('max_retries', 3)
        self.base_delay = self.config.get('base_delay', 1.0)
        self.max_delay = self.config.get('max_delay', 10.0)
        self.exponential_base = self.config.get('exponential_base', 2.0)
        self.circuit_breaker_threshold = self.config.get('circuit_breaker_threshold', 5)
        self.circuit_breaker_timeout = self.config.get('circuit_breaker_timeout', 60)
        
        self.circuit_breakers: Dict[str, CircuitBreaker] = {}
        self.logger = logging.getLogger(__name__)
    
    def get_circuit_breaker(self, key: str) -> CircuitBreaker:
        """Get or create a circuit breaker for a given key.
        
        Args:
            key: Unique key for the circuit breaker
            
        Returns:
            CircuitBreaker instance
        """
        if key not in self.circuit_breakers:
            self.circuit_breakers[key] = CircuitBreaker(
                threshold=self.circuit_breaker_threshold,
                timeout=self.circuit_breaker_timeout
            )
        return self.circuit_breakers[key]
    
    def retry(
        self,
        func: Callable,
        *args,
        error_key: Optional[str] = None,
        max_retries: Optional[int] = None,
        **kwargs
    ) -> Any:
        """Execute a function with retry logic.
        
        Args:
            func: Function to execute
            *args: Positional arguments for function
            error_key: Key for circuit breaker (optional)
            max_retries: Override max retries (optional)
            **kwargs: Keyword arguments for function
            
        Returns:
            Function return value
            
        Raises:
            Last exception if all retries fail
        """
        max_attempts = max_retries or self.max_retries
        last_error = None
        
        # Check circuit breaker if key provided
        if error_key:
            cb = self.get_circuit_breaker(error_key)
            if not cb.can_attempt():
                raise Exception(f"Circuit breaker is open for {error_key}")
        
        for attempt in range(1, max_attempts + 1):
            try:
                result = func(*args, **kwargs)
                
                # Record success in circuit breaker
                if error_key:
                    self.get_circuit_breaker(error_key).record_success()
                
                return result
                
            except Exception as e:
                last_error = e
                error_type = ErrorClassifier.classify(e)
                
                # Record failure in circuit breaker
                if error_key:
                    self.get_circuit_breaker(error_key).record_failure()
                
                # Check if should retry
                if not ErrorClassifier.should_retry(error_type, attempt, max_attempts):
                    self.logger.error(f"Max retries reached for {error_type.value} error: {e}")
                    raise
                
                # Calculate delay
                delay = ErrorClassifier.get_retry_delay(
                    error_type,
                    attempt,
                    self.base_delay,
                    self.max_delay,
                    self.exponential_base
                )
                
                self.logger.warning(
                    f"Attempt {attempt}/{max_attempts} failed ({error_type.value}): {e}. "
                    f"Retrying in {delay:.2f}s..."
                )
                
                time.sleep(delay)
        
        # All retries exhausted
        if error_key:
            self.get_circuit_breaker(error_key).record_failure()
        
        raise last_error


def retry_on_failure(
    max_retries: int = 3,
    delay: float = 1.0,
    error_key: Optional[str] = None,
    config: Optional[Dict[str, Any]] = None
):
    """Decorator for retrying functions on failure.
    
    Args:
        max_retries: Maximum number of retries
        delay: Base delay between retries
        error_key: Key for circuit breaker (optional)
        config: Additional retry configuration
        
    Returns:
        Decorated function
    """
    def decorator(func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args, **kwargs):
            handler = RetryHandler(config or {})
            handler.max_retries = max_retries
            handler.base_delay = delay
            return handler.retry(func, *args, error_key=error_key, **kwargs)
        return wrapper
    return decorator


def get_error_suggestion(error: Exception) -> str:
    """Get a human-readable suggestion for an error.
    
    Args:
        error: Exception that occurred
        
    Returns:
        Suggestion string
    """
    error_type = ErrorClassifier.classify(error)
    error_str = str(error).lower()
    
    suggestions = {
        ErrorType.NETWORK: "Check your internet connection and try again. The target website may be temporarily unavailable.",
        ErrorType.TIMEOUT: "The operation took too long. The website may be slow. Try increasing the timeout or retrying later.",
        ErrorType.ELEMENT_NOT_FOUND: "The expected element was not found on the page. The website structure may have changed.",
        ErrorType.ELEMENT_STALE: "The page element became stale. This usually resolves on retry.",
        ErrorType.ELEMENT_NOT_INTERACTABLE: "The element exists but cannot be interacted with. It may be hidden or covered by another element.",
        ErrorType.DRIVER_ERROR: "A browser driver error occurred. Try restarting the browser or checking driver compatibility.",
        ErrorType.UNKNOWN: "An unexpected error occurred. Check the logs for more details."
    }
    
    suggestion = suggestions.get(error_type, suggestions[ErrorType.UNKNOWN])
    
    # Add specific suggestions based on error message
    if 'chrome' in error_str or 'chromedriver' in error_str:
        suggestion += " Consider updating Chrome or ChromeDriver."
    elif 'login' in error_str or 'authentication' in error_str:
        suggestion += " Check your login credentials and session status."
    elif 'download' in error_str:
        suggestion += " Check available disk space and download directory permissions."
    
    return suggestion

