"""
Unified Error Handler - Central error handling and recovery system

This module provides a unified error handling mechanism for the entire openfund-agent system.
It categorizes errors, determines appropriate handling strategies, and maintains error history.

Requirements covered:
- 1.3: Data collection error handling and retry
- 2.3: Strategy execution exception capture
- 9.4: Evolution iteration error handling and notification
"""

import logging
import time
import traceback
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Type
from datetime import datetime


class ErrorSeverity(Enum):
    """Error severity levels"""

    LOW = "low"  # Minor issues, can continue
    MEDIUM = "medium"  # Significant issues, may need attention
    HIGH = "high"  # Critical issues, requires immediate action
    CRITICAL = "critical"  # System-breaking issues, must stop


class ErrorCategory(Enum):
    """Error categories for classification"""

    DATA_COLLECTION = "data_collection"  # Data fetching/processing errors
    STRATEGY_EXECUTION = "strategy_execution"  # Strategy execution errors
    DECISION_MAKING = "decision_making"  # AI decision errors
    CODE_MODIFICATION = "code_modification"  # Code generation/modification errors
    VALIDATION = "validation"  # Validation and testing errors
    RISK_CONTROL = "risk_control"  # Risk management errors
    EVOLUTION = "evolution"  # Evolution orchestration errors
    CONFIGURATION = "configuration"  # Configuration errors
    NETWORK = "network"  # Network/API errors
    UNKNOWN = "unknown"  # Unclassified errors


class ErrorAction(Enum):
    """Actions to take when handling errors"""

    RETRY = "retry"  # Retry the operation
    SKIP = "skip"  # Skip and continue
    ROLLBACK = "rollback"  # Rollback changes
    NOTIFY = "notify"  # Notify administrators
    PAUSE = "pause"  # Pause execution
    STOP = "stop"  # Stop execution completely
    ESCALATE = "escalate"  # Escalate to human review


@dataclass
class ErrorRecord:
    """Record of an error occurrence"""

    error_id: str
    timestamp: int
    category: ErrorCategory
    severity: ErrorSeverity
    exception_type: str
    message: str
    traceback_str: str
    context: Dict[str, Any] = field(default_factory=dict)
    action_taken: Optional[ErrorAction] = None
    retry_count: int = 0
    resolved: bool = False
    resolution_notes: Optional[str] = None


@dataclass
class ErrorHandlingStrategy:
    """Strategy for handling a specific error type"""

    error_type: Type[Exception]
    category: ErrorCategory
    severity: ErrorSeverity
    action: ErrorAction
    max_retries: int = 0
    retry_delay: float = 1.0
    notify_on_failure: bool = False
    custom_handler: Optional[Callable] = None


class ErrorHandler:
    """
    Unified error handler for the openfund-agent system

    This class provides centralized error handling with:
    - Error classification and categorization
    - Automatic retry logic for transient errors
    - Error history tracking
    - Notification for critical errors
    - Recovery strategies based on error type
    """

    def __init__(
        self,
        notification_service: Optional[Any] = None,
        max_history_size: int = 1000,
    ):
        """
        Initialize error handler

        Args:
            notification_service: Service for sending error notifications
            max_history_size: Maximum number of error records to keep
        """
        self.logger = logging.getLogger(__name__)
        self.notification_service = notification_service
        self.max_history_size = max_history_size

        # Error history
        self.error_history: List[ErrorRecord] = []

        # Error handling strategies
        self.strategies: Dict[Type[Exception], ErrorHandlingStrategy] = {}

        # Initialize default strategies
        self._init_default_strategies()

        self.logger.info("ErrorHandler initialized")

    def _init_default_strategies(self) -> None:
        """Initialize default error handling strategies"""

        # Import error types
        try:
            from openfund_agent.datas.exceptions import (
                DataCollectionError,
                RetryableError,
                NetworkError,
                RateLimitError,
                ValidationError,
            )

            # Data collection errors - retryable
            self.register_strategy(
                ErrorHandlingStrategy(
                    error_type=RetryableError,
                    category=ErrorCategory.DATA_COLLECTION,
                    severity=ErrorSeverity.MEDIUM,
                    action=ErrorAction.RETRY,
                    max_retries=3,
                    retry_delay=2.0,
                    notify_on_failure=False,
                )
            )

            self.register_strategy(
                ErrorHandlingStrategy(
                    error_type=NetworkError,
                    category=ErrorCategory.NETWORK,
                    severity=ErrorSeverity.MEDIUM,
                    action=ErrorAction.RETRY,
                    max_retries=3,
                    retry_delay=5.0,
                    notify_on_failure=True,
                )
            )

            self.register_strategy(
                ErrorHandlingStrategy(
                    error_type=RateLimitError,
                    category=ErrorCategory.NETWORK,
                    severity=ErrorSeverity.LOW,
                    action=ErrorAction.RETRY,
                    max_retries=5,
                    retry_delay=10.0,
                    notify_on_failure=False,
                )
            )

            # Validation errors - not retryable
            self.register_strategy(
                ErrorHandlingStrategy(
                    error_type=ValidationError,
                    category=ErrorCategory.DATA_COLLECTION,
                    severity=ErrorSeverity.HIGH,
                    action=ErrorAction.STOP,
                    notify_on_failure=True,
                )
            )

            # Data collection base error
            self.register_strategy(
                ErrorHandlingStrategy(
                    error_type=DataCollectionError,
                    category=ErrorCategory.DATA_COLLECTION,
                    severity=ErrorSeverity.MEDIUM,
                    action=ErrorAction.SKIP,
                    notify_on_failure=True,
                )
            )

        except ImportError:
            self.logger.warning("Could not import data collection exceptions")

        # Configuration errors
        try:
            from openfund_agent.config.loader import ConfigValidationError, ConfigLoadError

            self.register_strategy(
                ErrorHandlingStrategy(
                    error_type=ConfigValidationError,
                    category=ErrorCategory.CONFIGURATION,
                    severity=ErrorSeverity.CRITICAL,
                    action=ErrorAction.STOP,
                    notify_on_failure=True,
                )
            )

            self.register_strategy(
                ErrorHandlingStrategy(
                    error_type=ConfigLoadError,
                    category=ErrorCategory.CONFIGURATION,
                    severity=ErrorSeverity.CRITICAL,
                    action=ErrorAction.STOP,
                    notify_on_failure=True,
                )
            )

        except ImportError:
            self.logger.warning("Could not import config exceptions")

        # Generic exceptions
        self.register_strategy(
            ErrorHandlingStrategy(
                error_type=ValueError,
                category=ErrorCategory.UNKNOWN,
                severity=ErrorSeverity.MEDIUM,
                action=ErrorAction.SKIP,
                notify_on_failure=False,
            )
        )

        self.register_strategy(
            ErrorHandlingStrategy(
                error_type=KeyError,
                category=ErrorCategory.UNKNOWN,
                severity=ErrorSeverity.MEDIUM,
                action=ErrorAction.SKIP,
                notify_on_failure=False,
            )
        )

        self.register_strategy(
            ErrorHandlingStrategy(
                error_type=Exception,
                category=ErrorCategory.UNKNOWN,
                severity=ErrorSeverity.HIGH,
                action=ErrorAction.NOTIFY,
                notify_on_failure=True,
            )
        )

    def register_strategy(self, strategy: ErrorHandlingStrategy) -> None:
        """
        Register an error handling strategy

        Args:
            strategy: Error handling strategy to register
        """
        self.strategies[strategy.error_type] = strategy
        self.logger.debug(
            f"Registered error strategy for {strategy.error_type.__name__}: "
            f"{strategy.action.value}"
        )

    def handle_error(
        self,
        exception: Exception,
        context: Optional[Dict[str, Any]] = None,
        retry_count: int = 0,
    ) -> ErrorAction:
        """
        Handle an error according to its type and severity

        This is the main entry point for error handling. It:
        1. Classifies the error
        2. Determines the appropriate action
        3. Records the error
        4. Executes the handling strategy
        5. Sends notifications if needed

        Args:
            exception: The exception to handle
            context: Additional context about the error
            retry_count: Number of times this error has been retried

        Returns:
            ErrorAction indicating what action was taken
        """
        context = context or {}

        # Find matching strategy
        strategy = self._find_strategy(exception)

        # Create error record
        error_record = self._create_error_record(
            exception=exception,
            strategy=strategy,
            context=context,
            retry_count=retry_count,
        )

        # Log the error
        self._log_error(error_record, strategy)

        # Determine action
        action = self._determine_action(strategy, retry_count)
        error_record.action_taken = action

        # Record error in history
        self._record_error(error_record)

        # Execute custom handler if provided
        if strategy.custom_handler:
            try:
                strategy.custom_handler(exception, context)
            except Exception as e:
                self.logger.error(f"Custom error handler failed: {str(e)}")

        # Send notification if needed
        if strategy.notify_on_failure or action in [ErrorAction.STOP, ErrorAction.ESCALATE]:
            self._send_notification(error_record, strategy)

        return action

    def _find_strategy(self, exception: Exception) -> ErrorHandlingStrategy:
        """
        Find the appropriate error handling strategy

        Args:
            exception: Exception to find strategy for

        Returns:
            ErrorHandlingStrategy for the exception
        """
        # Try exact match first
        exception_type = type(exception)
        if exception_type in self.strategies:
            return self.strategies[exception_type]

        # Try parent classes in MRO order (most specific first)
        for parent_class in exception_type.__mro__[1:]:
            if parent_class in self.strategies:
                return self.strategies[parent_class]

        # Return default strategy for unknown exceptions
        return self.strategies.get(
            Exception,
            ErrorHandlingStrategy(
                error_type=Exception,
                category=ErrorCategory.UNKNOWN,
                severity=ErrorSeverity.HIGH,
                action=ErrorAction.NOTIFY,
                notify_on_failure=True,
            ),
        )

    def _create_error_record(
        self,
        exception: Exception,
        strategy: ErrorHandlingStrategy,
        context: Dict[str, Any],
        retry_count: int,
    ) -> ErrorRecord:
        """
        Create an error record

        Args:
            exception: The exception
            strategy: Error handling strategy
            context: Error context
            retry_count: Retry count

        Returns:
            ErrorRecord object
        """
        import uuid

        return ErrorRecord(
            error_id=str(uuid.uuid4()),
            timestamp=int(time.time()),
            category=strategy.category,
            severity=strategy.severity,
            exception_type=type(exception).__name__,
            message=str(exception),
            traceback_str=traceback.format_exc(),
            context=context,
            retry_count=retry_count,
        )

    def _determine_action(self, strategy: ErrorHandlingStrategy, retry_count: int) -> ErrorAction:
        """
        Determine the action to take based on strategy and retry count

        Args:
            strategy: Error handling strategy
            retry_count: Current retry count

        Returns:
            ErrorAction to take
        """
        # If retry action and retries remaining, retry
        if strategy.action == ErrorAction.RETRY:
            if retry_count < strategy.max_retries:
                return ErrorAction.RETRY
            else:
                # Max retries exceeded, escalate
                self.logger.warning(f"Max retries ({strategy.max_retries}) exceeded, escalating")
                return ErrorAction.ESCALATE

        return strategy.action

    def _log_error(self, error_record: ErrorRecord, strategy: ErrorHandlingStrategy) -> None:
        """
        Log error with appropriate level

        Args:
            error_record: Error record to log
            strategy: Error handling strategy
        """
        log_message = (
            f"[{error_record.category.value}] {error_record.exception_type}: "
            f"{error_record.message}"
        )

        if error_record.context:
            log_message += f" | Context: {error_record.context}"

        # Log based on severity
        if error_record.severity == ErrorSeverity.CRITICAL:
            self.logger.critical(log_message)
            self.logger.critical(f"Traceback:\n{error_record.traceback_str}")
        elif error_record.severity == ErrorSeverity.HIGH:
            self.logger.error(log_message)
            self.logger.debug(f"Traceback:\n{error_record.traceback_str}")
        elif error_record.severity == ErrorSeverity.MEDIUM:
            self.logger.warning(log_message)
        else:
            self.logger.info(log_message)

    def _record_error(self, error_record: ErrorRecord) -> None:
        """
        Record error in history

        Args:
            error_record: Error record to store
        """
        self.error_history.append(error_record)

        # Trim history if needed
        if len(self.error_history) > self.max_history_size:
            self.error_history = self.error_history[-self.max_history_size :]

    def _send_notification(
        self, error_record: ErrorRecord, strategy: ErrorHandlingStrategy
    ) -> None:
        """
        Send error notification

        Args:
            error_record: Error record
            strategy: Error handling strategy
        """
        if not self.notification_service:
            self.logger.debug("No notification service configured, skipping notification")
            return

        try:
            notification_data = {
                "error_id": error_record.error_id,
                "timestamp": datetime.fromtimestamp(error_record.timestamp).isoformat(),
                "category": error_record.category.value,
                "severity": error_record.severity.value,
                "exception_type": error_record.exception_type,
                "message": error_record.message,
                "action_taken": (
                    error_record.action_taken.value if error_record.action_taken else None
                ),
                "retry_count": error_record.retry_count,
                "context": error_record.context,
            }

            # Send notification based on severity
            if error_record.severity in [ErrorSeverity.CRITICAL, ErrorSeverity.HIGH]:
                self.notification_service.send_error_alert(notification_data)
            else:
                self.notification_service.send_error_warning(notification_data)

            self.logger.info(f"Sent error notification for {error_record.error_id}")

        except Exception as e:
            self.logger.error(f"Failed to send error notification: {str(e)}")

    def get_error_history(
        self,
        category: Optional[ErrorCategory] = None,
        severity: Optional[ErrorSeverity] = None,
        limit: Optional[int] = None,
        unresolved_only: bool = False,
    ) -> List[ErrorRecord]:
        """
        Get error history with optional filtering

        Args:
            category: Filter by error category
            severity: Filter by error severity
            limit: Limit number of records returned
            unresolved_only: Only return unresolved errors

        Returns:
            List of error records matching criteria
        """
        filtered = self.error_history

        if category:
            filtered = [e for e in filtered if e.category == category]

        if severity:
            filtered = [e for e in filtered if e.severity == severity]

        if unresolved_only:
            filtered = [e for e in filtered if not e.resolved]

        if limit:
            filtered = filtered[-limit:]

        return filtered

    def get_error_statistics(self) -> Dict[str, Any]:
        """
        Get error statistics

        Returns:
            Dictionary containing error statistics
        """
        total_errors = len(self.error_history)

        if total_errors == 0:
            return {
                "total_errors": 0,
                "by_category": {},
                "by_severity": {},
                "unresolved_count": 0,
                "recent_errors": [],
            }

        # Count by category
        by_category = {}
        for category in ErrorCategory:
            count = sum(1 for e in self.error_history if e.category == category)
            if count > 0:
                by_category[category.value] = count

        # Count by severity
        by_severity = {}
        for severity in ErrorSeverity:
            count = sum(1 for e in self.error_history if e.severity == severity)
            if count > 0:
                by_severity[severity.value] = count

        # Count unresolved
        unresolved_count = sum(1 for e in self.error_history if not e.resolved)

        # Get recent errors (last 10)
        recent_errors = [
            {
                "error_id": e.error_id,
                "timestamp": datetime.fromtimestamp(e.timestamp).isoformat(),
                "category": e.category.value,
                "severity": e.severity.value,
                "message": e.message,
                "resolved": e.resolved,
            }
            for e in self.error_history[-10:]
        ]

        return {
            "total_errors": total_errors,
            "by_category": by_category,
            "by_severity": by_severity,
            "unresolved_count": unresolved_count,
            "recent_errors": recent_errors,
        }

    def mark_error_resolved(self, error_id: str, resolution_notes: Optional[str] = None) -> bool:
        """
        Mark an error as resolved

        Args:
            error_id: ID of the error to mark as resolved
            resolution_notes: Optional notes about the resolution

        Returns:
            True if error was found and marked resolved, False otherwise
        """
        for error in self.error_history:
            if error.error_id == error_id:
                error.resolved = True
                error.resolution_notes = resolution_notes
                self.logger.info(f"Marked error {error_id} as resolved")
                return True

        self.logger.warning(f"Error {error_id} not found in history")
        return False

    def clear_error_history(self, resolved_only: bool = True) -> int:
        """
        Clear error history

        Args:
            resolved_only: If True, only clear resolved errors

        Returns:
            Number of errors cleared
        """
        if resolved_only:
            original_count = len(self.error_history)
            self.error_history = [e for e in self.error_history if not e.resolved]
            cleared_count = original_count - len(self.error_history)
        else:
            cleared_count = len(self.error_history)
            self.error_history.clear()

        self.logger.info(f"Cleared {cleared_count} error records")
        return cleared_count
