# File: src/noveler/domain/services/unified_error_handler.py
# Purpose: Centralized error handling and recovery strategies for domain services
# Context: Extracted from scattered error handling across multiple services (ProgressiveWriteManager, ProgressiveCheckManager, etc.)

"""Unified error handling and recovery service for domain operations.

This module provides centralized error handling, recovery strategies, and logging
for all domain services. It eliminates code duplication and provides consistent
error management across the application.

Components:
- ErrorCategory: Classification of error types
- RecoveryStrategy: Recovery action types
- ErrorContext: Detailed error information
- UnifiedErrorHandler: Central error handling service
"""

from __future__ import annotations

import logging
from collections.abc import Callable
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any


class ErrorCategory(Enum):
    """Classification of error types."""

    INFRASTRUCTURE = "infrastructure"  # File I/O, database, network
    VALIDATION = "validation"  # Data validation, format errors
    STATE = "state"  # Invalid state transitions, missing prerequisites
    LLM = "llm"  # LLM API errors, response parsing
    RECOVERY = "recovery"  # Errors during recovery attempts
    UNKNOWN = "unknown"  # Unclassified errors


class RecoveryStrategy(Enum):
    """Recovery action types."""

    RETRY = "retry"  # Retry the operation
    SKIP = "skip"  # Skip and continue
    FALLBACK = "fallback"  # Use fallback implementation
    MANUAL = "manual"  # Requires manual intervention
    ABORT = "abort"  # Abort operation


@dataclass
class ErrorContext:
    """Detailed error information and context.

    Attributes:
        timestamp: When the error occurred
        error_type: Exception type
        error_message: Error message
        category: ErrorCategory classification
        severity: Error severity (1-5, 5=critical)
        operation: Operation being performed
        context_data: Additional context data
        recovery_attempts: Number of recovery attempts made
        resolved: Whether error was resolved
    """

    timestamp: datetime
    error_type: str
    error_message: str
    category: ErrorCategory
    severity: int
    operation: str
    context_data: dict[str, Any] = field(default_factory=dict)
    recovery_attempts: int = 0
    resolved: bool = False


class UnifiedErrorHandler:
    """Centralized error handling and recovery service.

    Purpose:
        Provide unified error handling, recovery strategies, and logging
        for all domain services. Eliminates code duplication and provides
        consistent error management.

    Responsibilities:
        - Error classification and categorization
        - Recovery strategy selection and execution
        - Consistent logging and monitoring
        - Error tracking and metrics
        - Retry logic with exponential backoff

    Context:
        Used by ProgressiveWriteManager, ProgressiveCheckManager, and other
        domain services to handle errors consistently.

    Design Patterns:
        - Strategy Pattern: Recovery strategy selection
        - Observer Pattern: Error logging and monitoring
        - Chain of Responsibility: Error classification
    """

    MAX_RECOVERY_ATTEMPTS = 3
    INITIAL_RETRY_DELAY = 1.0
    MAX_RETRY_DELAY = 10.0

    def __init__(self, logger: logging.Logger, service_name: str = "UnnamedService"):
        """Initialize error handler.

        Args:
            logger: Logger instance for error logging
            service_name: Name of the service using this handler

        Preconditions:
            - logger must be a valid Logger instance

        Side Effects:
            - Initializes error tracking storage
        """
        self._logger = logger
        self._service_name = service_name
        self._error_history: list[ErrorContext] = []
        self._recovery_callbacks: dict[ErrorCategory, list[Callable]] = {}
        self._total_errors = 0
        self._resolved_errors = 0

    def handle_error(
        self,
        error: Exception,
        operation: str,
        context_data: dict[str, Any] | None = None,
    ) -> tuple[bool, str | None]:
        """Handle an error with recovery attempt.

        Args:
            error: Exception that occurred
            operation: Name of the operation that failed
            context_data: Additional context information

        Returns:
            Tuple of (resolved, error_message)
            - resolved: True if error was recovered
            - error_message: Error message if not resolved

        Side Effects:
            - Logs error
            - Attempts recovery
            - Updates error history
        """
        if context_data is None:
            context_data = {}

        # Classify error
        category = self._classify_error(error)
        severity = self._determine_severity(error, category)

        # Create error context
        error_ctx = ErrorContext(
            timestamp=datetime.now(),
            error_type=type(error).__name__,
            error_message=str(error),
            category=category,
            severity=severity,
            operation=operation,
            context_data=context_data,
        )

        self._error_history.append(error_ctx)
        self._total_errors += 1

        # Log error
        self._log_error(error_ctx)

        # Attempt recovery
        recovery_strategy = self._select_recovery_strategy(error_ctx)
        resolved = self._execute_recovery(error_ctx, recovery_strategy)

        if resolved:
            self._resolved_errors += 1
            error_ctx.resolved = True
            self._logger.info(
                "Error recovered [%s] %s: %s → %s",
                self._service_name,
                operation,
                category.value,
                recovery_strategy.value,
            )
            return True, None
        self._logger.error(
            "Error unrecoverable [%s] %s: %s (severity: %s)",
            self._service_name,
            operation,
            category.value,
            severity,
        )
        return False, error_ctx.error_message

    def register_recovery_callback(
        self, category: ErrorCategory, callback: Callable
    ) -> None:
        """Register a recovery callback for an error category.

        Args:
            category: ErrorCategory to register callback for
            callback: Callable that attempts recovery

        Side Effects:
            - Adds callback to recovery registry
        """
        if category not in self._recovery_callbacks:
            self._recovery_callbacks[category] = []

        self._recovery_callbacks[category].append(callback)
        self._logger.debug(
            "Registered recovery callback for %s [%s]",
            category.value,
            self._service_name,
        )

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

        Returns:
            Dictionary with error metrics
        """
        recovery_rate = (
            (self._resolved_errors / self._total_errors * 100)
            if self._total_errors > 0
            else 0.0
        )

        return {
            "service_name": self._service_name,
            "total_errors": self._total_errors,
            "resolved_errors": self._resolved_errors,
            "recovery_rate": recovery_rate,
            "error_history_size": len(self._error_history),
        }

    # ===== Private Implementation Methods =====

    def _classify_error(self, error: Exception) -> ErrorCategory:
        """Classify error into a category.

        Purpose:
            Determine error category based on exception type.
        """
        error_name = type(error).__name__

        if error_name in ("FileNotFoundError", "IOError", "OSError"):
            return ErrorCategory.INFRASTRUCTURE
        if error_name in ("ValueError", "TypeError", "KeyError"):
            return ErrorCategory.VALIDATION
        if error_name in ("SystemStateError", "StateError"):
            return ErrorCategory.STATE
        if error_name in ("APIError", "TimeoutError", "ConnectionError"):
            return ErrorCategory.LLM
        return ErrorCategory.UNKNOWN

    def _determine_severity(
        self, error: Exception, category: ErrorCategory
    ) -> int:
        """Determine error severity (1-5).

        Purpose:
            Assess severity based on error type and category.
        """
        error_name = type(error).__name__

        # Critical errors
        if error_name in ("SystemExit", "KeyboardInterrupt"):
            return 5

        # High severity
        if category == ErrorCategory.INFRASTRUCTURE:
            return 4
        if category == ErrorCategory.STATE:
            return 3
        if category == ErrorCategory.VALIDATION:
            return 2
        return 1

    def _select_recovery_strategy(self, error_ctx: ErrorContext) -> RecoveryStrategy:
        """Select recovery strategy for error.

        Purpose:
            Determine best recovery action based on error context.
        """
        # State errors: require manual intervention
        if error_ctx.category == ErrorCategory.STATE:
            return RecoveryStrategy.MANUAL

        # Validation errors: skip operation
        if error_ctx.category == ErrorCategory.VALIDATION:
            return RecoveryStrategy.SKIP

        # Infrastructure errors: retry
        if error_ctx.category == ErrorCategory.INFRASTRUCTURE:
            return RecoveryStrategy.RETRY

        # LLM errors: fallback or retry
        if error_ctx.category == ErrorCategory.LLM:
            return (
                RecoveryStrategy.RETRY
                if error_ctx.recovery_attempts < self.MAX_RECOVERY_ATTEMPTS
                else RecoveryStrategy.FALLBACK
            )

        # Unknown: retry
        return RecoveryStrategy.RETRY

    def _execute_recovery(
        self, error_ctx: ErrorContext, strategy: RecoveryStrategy
    ) -> bool:
        """Execute recovery strategy.

        Purpose:
            Attempt to recover from error using selected strategy.

        Returns:
            True if recovery successful, False otherwise
        """
        if strategy == RecoveryStrategy.RETRY:
            error_ctx.recovery_attempts += 1
            if error_ctx.recovery_attempts >= self.MAX_RECOVERY_ATTEMPTS:
                return False
            return True  # Caller should retry operation

        if strategy == RecoveryStrategy.SKIP:
            return True  # Skip this operation

        if strategy == RecoveryStrategy.FALLBACK:
            # Try registered fallback callbacks
            if error_ctx.category in self._recovery_callbacks:
                for callback in self._recovery_callbacks[error_ctx.category]:
                    try:
                        callback(error_ctx)
                        return True
                    except Exception:
                        pass
            return False

        if strategy == RecoveryStrategy.MANUAL:
            return False  # Requires manual intervention

        return False

    def _log_error(self, error_ctx: ErrorContext) -> None:
        """Log error with appropriate level.

        Purpose:
            Log error with consistent format.
        """
        log_msg = (
            f"[{self._service_name}] {error_ctx.operation}: "
            f"{error_ctx.error_type} - {error_ctx.error_message} "
            f"({error_ctx.category.value}, severity: {error_ctx.severity})"
        )

        if error_ctx.severity >= 4:
            self._logger.error(log_msg)
        elif error_ctx.severity >= 3:
            self._logger.warning(log_msg)
        else:
            self._logger.info(log_msg)

    def cleanup_old_errors(self, max_age_seconds: int = 3600) -> None:
        """Clean up old error history.

        Purpose:
            Remove error records older than max_age_seconds.

        Args:
            max_age_seconds: Maximum age in seconds (default 1 hour)

        Side Effects:
            - Removes old error records from history
        """
        now = datetime.now()
        initial_count = len(self._error_history)

        self._error_history = [
            err
            for err in self._error_history
            if (now - err.timestamp).total_seconds() < max_age_seconds
        ]

        removed_count = initial_count - len(self._error_history)
        if removed_count > 0:
            self._logger.debug(
                "Cleaned up %s old errors [%s]",
                removed_count,
                self._service_name,
            )
