# -*- coding: utf-8 -*-
"""
Analytics Exceptions
===================

Custom exception classes for the Adtlas Analytics module.
Provides specific exception types for better error handling,
debugging, and user feedback throughout the analytics system.

Exception Categories:
- Base Exceptions: Core exception classes
- Data Exceptions: Data-related errors
- Processing Exceptions: Processing and computation errors
- Integration Exceptions: External service integration errors
- Validation Exceptions: Data validation errors
- Permission Exceptions: Access control errors
- Export Exceptions: Data export and report errors
- Cache Exceptions: Caching system errors

Key Features:
- Hierarchical exception structure
- Detailed error messages
- Error code integration
- Context information preservation
- Logging integration
- User-friendly error messages
- Debug information support
- Recovery suggestions

Exception Hierarchy:
- AnalyticsException (base)
  - AnalyticsDataException
    - DataNotFoundException
    - DataValidationException
    - DataCorruptionException
  - AnalyticsProcessingException
    - ProcessingTimeoutException
    - ProcessingFailedException
    - ResourceExhaustedException
  - AnalyticsIntegrationException
    - APIException
    - NetworkException
    - ServiceUnavailableException
  - AnalyticsPermissionException
    - InsufficientPermissionsException
    - AuthenticationRequiredException
  - AnalyticsExportException
    - ExportFailedException
    - FormatNotSupportedException
    - FileSizeLimitExceededException

Usage Examples:
- Raise specific exceptions: raise DataNotFoundException("Campaign not found")
- Catch exception categories: except AnalyticsDataException
- Access error details: exception.error_code, exception.context
- Log with context: logger.error(exception.get_log_message())

Author: Adtlas Development Team
Version: 1.0.0
Last Updated: 2024
"""

import logging
from typing import Dict, Any, Optional, List, Union
from datetime import datetime

from .constants import ErrorCode, ERROR_MESSAGES

# Configure logging
logger = logging.getLogger(__name__)


# =============================================================================
# BASE EXCEPTIONS
# =============================================================================

class AnalyticsException(Exception):
    """
    Base exception class for all analytics-related errors.
    
    Provides common functionality for all analytics exceptions:
    - Error code management
    - Context information storage
    - Logging integration
    - User-friendly messages
    - Debug information
    
    Attributes:
        message: Human-readable error message
        error_code: Standardized error code
        context: Additional context information
        timestamp: When the error occurred
        user_message: User-friendly error message
        debug_info: Debug information for developers
    """
    
    def __init__(
        self,
        message: str,
        error_code: Optional[ErrorCode] = None,
        context: Optional[Dict[str, Any]] = None,
        user_message: Optional[str] = None,
        debug_info: Optional[Dict[str, Any]] = None
    ):
        """
        Initialize analytics exception.
        
        Args:
            message: Error message for developers
            error_code: Standardized error code
            context: Additional context information
            user_message: User-friendly error message
            debug_info: Debug information
        """
        super().__init__(message)
        
        self.message = message
        self.error_code = error_code or ErrorCode.UNKNOWN_ERROR
        self.context = context or {}
        self.timestamp = datetime.utcnow()
        self.user_message = user_message or self._get_default_user_message()
        self.debug_info = debug_info or {}
        
        # Add exception details to context
        self.context.update({
            'exception_type': self.__class__.__name__,
            'timestamp': self.timestamp.isoformat(),
            'error_code': self.error_code.value if self.error_code else None
        })
        
        # Log the exception
        self._log_exception()
    
    def _get_default_user_message(self) -> str:
        """
        Get default user-friendly message.
        
        Returns:
            User-friendly error message
        """
        if self.error_code:
            return ERROR_MESSAGES.get(
                self.error_code,
                "An error occurred while processing your request."
            )
        return "An unexpected error occurred. Please try again."
    
    def _log_exception(self) -> None:
        """
        Log the exception with appropriate level.
        """
        try:
            log_data = {
                'message': self.message,
                'error_code': self.error_code.value if self.error_code else None,
                'context': self.context,
                'debug_info': self.debug_info
            }
            
            # Log as error by default
            logger.error(
                f"{self.__class__.__name__}: {self.message}",
                extra=log_data
            )
            
        except Exception as e:
            # Fallback logging if structured logging fails
            logger.error(f"Exception logging failed: {e}")
            logger.error(f"{self.__class__.__name__}: {self.message}")
    
    def get_log_message(self) -> str:
        """
        Get formatted log message.
        
        Returns:
            Formatted log message with context
        """
        context_str = ", ".join([
            f"{k}={v}" for k, v in self.context.items()
            if k not in ['exception_type', 'timestamp']
        ])
        
        return f"{self.message} [{context_str}]"
    
    def get_api_response(self) -> Dict[str, Any]:
        """
        Get API response format for this exception.
        
        Returns:
            Dictionary suitable for API error responses
        """
        return {
            'error': True,
            'error_code': self.error_code.value if self.error_code else 'UNKNOWN',
            'message': self.user_message,
            'details': self.message,
            'timestamp': self.timestamp.isoformat(),
            'context': self._get_safe_context()
        }
    
    def _get_safe_context(self) -> Dict[str, Any]:
        """
        Get context information safe for API responses.
        
        Returns:
            Sanitized context dictionary
        """
        # Remove sensitive information from context
        safe_context = {}
        sensitive_keys = {'password', 'token', 'secret', 'key', 'auth'}
        
        for key, value in self.context.items():
            if not any(sensitive in key.lower() for sensitive in sensitive_keys):
                safe_context[key] = value
        
        return safe_context
    
    def add_context(self, key: str, value: Any) -> None:
        """
        Add context information to the exception.
        
        Args:
            key: Context key
            value: Context value
        """
        self.context[key] = value
    
    def __str__(self) -> str:
        """
        String representation of the exception.
        
        Returns:
            Formatted exception string
        """
        return f"{self.__class__.__name__}: {self.message}"
    
    def __repr__(self) -> str:
        """
        Detailed representation of the exception.
        
        Returns:
            Detailed exception representation
        """
        return (
            f"{self.__class__.__name__}("
            f"message='{self.message}', "
            f"error_code={self.error_code}, "
            f"context={self.context}"
            f")"
        )


# =============================================================================
# DATA EXCEPTIONS
# =============================================================================

class AnalyticsDataException(AnalyticsException):
    """
    Base exception for data-related errors.
    
    Covers errors related to data access, validation,
    corruption, and other data-specific issues.
    """
    
    def __init__(self, message: str, **kwargs):
        super().__init__(
            message,
            error_code=kwargs.get('error_code', ErrorCode.DATA_INVALID),
            **kwargs
        )


class DataNotFoundException(AnalyticsDataException):
    """
    Exception raised when requested data is not found.
    
    Used when analytics data, reports, or related entities
    cannot be located in the system.
    """
    
    def __init__(
        self,
        message: str = "Requested data not found",
        resource_type: Optional[str] = None,
        resource_id: Optional[Union[str, int]] = None,
        **kwargs
    ):
        context = kwargs.get('context', {})
        if resource_type:
            context['resource_type'] = resource_type
        if resource_id:
            context['resource_id'] = resource_id
        
        super().__init__(
            message,
            error_code=ErrorCode.DATA_NOT_FOUND,
            context=context,
            user_message="The requested information could not be found.",
            **kwargs
        )


class DataValidationException(AnalyticsDataException):
    """
    Exception raised when data validation fails.
    
    Used when analytics data doesn't meet validation
    requirements or business rules.
    """
    
    def __init__(
        self,
        message: str = "Data validation failed",
        validation_errors: Optional[List[str]] = None,
        field_name: Optional[str] = None,
        **kwargs
    ):
        context = kwargs.get('context', {})
        if validation_errors:
            context['validation_errors'] = validation_errors
        if field_name:
            context['field_name'] = field_name
        
        super().__init__(
            message,
            error_code=ErrorCode.VALIDATION_ERROR,
            context=context,
            user_message="The provided data is invalid. Please check your input.",
            **kwargs
        )


class DataCorruptionException(AnalyticsDataException):
    """
    Exception raised when data corruption is detected.
    
    Used when analytics data appears to be corrupted
    or inconsistent.
    """
    
    def __init__(
        self,
        message: str = "Data corruption detected",
        corruption_type: Optional[str] = None,
        affected_records: Optional[int] = None,
        **kwargs
    ):
        context = kwargs.get('context', {})
        if corruption_type:
            context['corruption_type'] = corruption_type
        if affected_records:
            context['affected_records'] = affected_records
        
        super().__init__(
            message,
            error_code=ErrorCode.DATA_CORRUPTED,
            context=context,
            user_message="Data integrity issue detected. Please contact support.",
            **kwargs
        )


class DataIncompleteException(AnalyticsDataException):
    """
    Exception raised when required data is incomplete.
    
    Used when analytics operations cannot proceed
    due to missing required data.
    """
    
    def __init__(
        self,
        message: str = "Required data is incomplete",
        missing_fields: Optional[List[str]] = None,
        completion_percentage: Optional[float] = None,
        **kwargs
    ):
        context = kwargs.get('context', {})
        if missing_fields:
            context['missing_fields'] = missing_fields
        if completion_percentage is not None:
            context['completion_percentage'] = completion_percentage
        
        super().__init__(
            message,
            error_code=ErrorCode.DATA_INCOMPLETE,
            context=context,
            user_message="Some required information is missing.",
            **kwargs
        )


# =============================================================================
# PROCESSING EXCEPTIONS
# =============================================================================

class AnalyticsProcessingException(AnalyticsException):
    """
    Base exception for processing-related errors.
    
    Covers errors that occur during analytics data processing,
    computation, aggregation, and analysis.
    """
    
    def __init__(self, message: str, **kwargs):
        super().__init__(
            message,
            error_code=kwargs.get('error_code', ErrorCode.PROCESSING_FAILED),
            **kwargs
        )


class ProcessingTimeoutException(AnalyticsProcessingException):
    """
    Exception raised when processing operations timeout.
    
    Used when analytics processing takes longer than
    the configured timeout period.
    """
    
    def __init__(
        self,
        message: str = "Processing operation timed out",
        timeout_duration: Optional[int] = None,
        operation_type: Optional[str] = None,
        **kwargs
    ):
        context = kwargs.get('context', {})
        if timeout_duration:
            context['timeout_duration'] = timeout_duration
        if operation_type:
            context['operation_type'] = operation_type
        
        super().__init__(
            message,
            error_code=ErrorCode.TIMEOUT_ERROR,
            context=context,
            user_message="The operation is taking longer than expected. Please try again.",
            **kwargs
        )


class ProcessingFailedException(AnalyticsProcessingException):
    """
    Exception raised when processing operations fail.
    
    Used when analytics processing encounters
    unrecoverable errors.
    """
    
    def __init__(
        self,
        message: str = "Processing operation failed",
        failure_reason: Optional[str] = None,
        retry_possible: bool = True,
        **kwargs
    ):
        context = kwargs.get('context', {})
        if failure_reason:
            context['failure_reason'] = failure_reason
        context['retry_possible'] = retry_possible
        
        user_msg = (
            "Processing failed. Please try again later."
            if retry_possible
            else "Processing failed. Please contact support."
        )
        
        super().__init__(
            message,
            error_code=ErrorCode.PROCESSING_FAILED,
            context=context,
            user_message=user_msg,
            **kwargs
        )


class ResourceExhaustedException(AnalyticsProcessingException):
    """
    Exception raised when system resources are exhausted.
    
    Used when processing cannot continue due to
    insufficient system resources.
    """
    
    def __init__(
        self,
        message: str = "System resources exhausted",
        resource_type: Optional[str] = None,
        current_usage: Optional[float] = None,
        **kwargs
    ):
        context = kwargs.get('context', {})
        if resource_type:
            context['resource_type'] = resource_type
        if current_usage is not None:
            context['current_usage'] = current_usage
        
        super().__init__(
            message,
            error_code=ErrorCode.RESOURCE_EXHAUSTED,
            context=context,
            user_message="System is currently overloaded. Please try again later.",
            **kwargs
        )


# =============================================================================
# INTEGRATION EXCEPTIONS
# =============================================================================

class AnalyticsIntegrationException(AnalyticsException):
    """
    Base exception for integration-related errors.
    
    Covers errors related to external service integration,
    API calls, and third-party system interactions.
    """
    
    def __init__(self, message: str, **kwargs):
        super().__init__(
            message,
            error_code=kwargs.get('error_code', ErrorCode.API_ERROR),
            **kwargs
        )


class APIException(AnalyticsIntegrationException):
    """
    Exception raised for external API errors.
    
    Used when external API calls fail or return
    unexpected responses.
    """
    
    def __init__(
        self,
        message: str = "External API error",
        api_name: Optional[str] = None,
        status_code: Optional[int] = None,
        response_data: Optional[Dict[str, Any]] = None,
        **kwargs
    ):
        context = kwargs.get('context', {})
        if api_name:
            context['api_name'] = api_name
        if status_code:
            context['status_code'] = status_code
        if response_data:
            context['response_data'] = response_data
        
        super().__init__(
            message,
            error_code=ErrorCode.API_ERROR,
            context=context,
            user_message="External service is temporarily unavailable.",
            **kwargs
        )


class NetworkException(AnalyticsIntegrationException):
    """
    Exception raised for network connectivity errors.
    
    Used when network issues prevent communication
    with external services.
    """
    
    def __init__(
        self,
        message: str = "Network connectivity error",
        endpoint: Optional[str] = None,
        retry_after: Optional[int] = None,
        **kwargs
    ):
        context = kwargs.get('context', {})
        if endpoint:
            context['endpoint'] = endpoint
        if retry_after:
            context['retry_after'] = retry_after
        
        super().__init__(
            message,
            error_code=ErrorCode.NETWORK_ERROR,
            context=context,
            user_message="Network connection issue. Please check your connection.",
            **kwargs
        )


class ServiceUnavailableException(AnalyticsIntegrationException):
    """
    Exception raised when external services are unavailable.
    
    Used when external services are down or
    temporarily unavailable.
    """
    
    def __init__(
        self,
        message: str = "External service unavailable",
        service_name: Optional[str] = None,
        estimated_recovery: Optional[str] = None,
        **kwargs
    ):
        context = kwargs.get('context', {})
        if service_name:
            context['service_name'] = service_name
        if estimated_recovery:
            context['estimated_recovery'] = estimated_recovery
        
        super().__init__(
            message,
            error_code=ErrorCode.SERVICE_UNAVAILABLE,
            context=context,
            user_message="Service is temporarily unavailable. Please try again later.",
            **kwargs
        )


class RateLimitExceededException(AnalyticsIntegrationException):
    """
    Exception raised when API rate limits are exceeded.
    
    Used when too many requests are made to external
    APIs within the rate limit window.
    """
    
    def __init__(
        self,
        message: str = "Rate limit exceeded",
        limit: Optional[int] = None,
        reset_time: Optional[datetime] = None,
        **kwargs
    ):
        context = kwargs.get('context', {})
        if limit:
            context['limit'] = limit
        if reset_time:
            context['reset_time'] = reset_time.isoformat()
        
        super().__init__(
            message,
            error_code=ErrorCode.RATE_LIMIT_EXCEEDED,
            context=context,
            user_message="Too many requests. Please wait before trying again.",
            **kwargs
        )


# =============================================================================
# PERMISSION EXCEPTIONS
# =============================================================================

class AnalyticsPermissionException(AnalyticsException):
    """
    Base exception for permission-related errors.
    
    Covers errors related to access control,
    authentication, and authorization.
    """
    
    def __init__(self, message: str, **kwargs):
        super().__init__(
            message,
            error_code=kwargs.get('error_code', ErrorCode.PERMISSION_ERROR),
            **kwargs
        )


class InsufficientPermissionsException(AnalyticsPermissionException):
    """
    Exception raised when user lacks required permissions.
    
    Used when users attempt to access resources or
    perform actions they don't have permission for.
    """
    
    def __init__(
        self,
        message: str = "Insufficient permissions",
        required_permission: Optional[str] = None,
        resource_type: Optional[str] = None,
        **kwargs
    ):
        context = kwargs.get('context', {})
        if required_permission:
            context['required_permission'] = required_permission
        if resource_type:
            context['resource_type'] = resource_type
        
        super().__init__(
            message,
            error_code=ErrorCode.PERMISSION_ERROR,
            context=context,
            user_message="You don't have permission to access this resource.",
            **kwargs
        )


class AuthenticationRequiredException(AnalyticsPermissionException):
    """
    Exception raised when authentication is required.
    
    Used when unauthenticated users attempt to access
    protected resources.
    """
    
    def __init__(
        self,
        message: str = "Authentication required",
        login_url: Optional[str] = None,
        **kwargs
    ):
        context = kwargs.get('context', {})
        if login_url:
            context['login_url'] = login_url
        
        super().__init__(
            message,
            error_code=ErrorCode.AUTHENTICATION_ERROR,
            context=context,
            user_message="Please log in to access this resource.",
            **kwargs
        )


# =============================================================================
# EXPORT EXCEPTIONS
# =============================================================================

class AnalyticsExportException(AnalyticsException):
    """
    Base exception for export-related errors.
    
    Covers errors related to data export, report generation,
    and file operations.
    """
    
    def __init__(self, message: str, **kwargs):
        super().__init__(
            message,
            error_code=kwargs.get('error_code', ErrorCode.EXPORT_FAILED),
            **kwargs
        )


class ExportFailedException(AnalyticsExportException):
    """
    Exception raised when data export operations fail.
    
    Used when export processes encounter errors
    during data generation or file creation.
    """
    
    def __init__(
        self,
        message: str = "Data export failed",
        export_format: Optional[str] = None,
        record_count: Optional[int] = None,
        **kwargs
    ):
        context = kwargs.get('context', {})
        if export_format:
            context['export_format'] = export_format
        if record_count:
            context['record_count'] = record_count
        
        super().__init__(
            message,
            error_code=ErrorCode.EXPORT_FAILED,
            context=context,
            user_message="Export failed. Please try again or contact support.",
            **kwargs
        )


class FormatNotSupportedException(AnalyticsExportException):
    """
    Exception raised when export format is not supported.
    
    Used when users request exports in unsupported
    file formats.
    """
    
    def __init__(
        self,
        message: str = "Export format not supported",
        requested_format: Optional[str] = None,
        supported_formats: Optional[List[str]] = None,
        **kwargs
    ):
        context = kwargs.get('context', {})
        if requested_format:
            context['requested_format'] = requested_format
        if supported_formats:
            context['supported_formats'] = supported_formats
        
        super().__init__(
            message,
            error_code=ErrorCode.FORMAT_ERROR,
            context=context,
            user_message="The requested export format is not supported.",
            **kwargs
        )


class FileSizeLimitExceededException(AnalyticsExportException):
    """
    Exception raised when export file size exceeds limits.
    
    Used when export operations would generate files
    larger than the configured size limits.
    """
    
    def __init__(
        self,
        message: str = "File size limit exceeded",
        file_size: Optional[int] = None,
        size_limit: Optional[int] = None,
        **kwargs
    ):
        context = kwargs.get('context', {})
        if file_size:
            context['file_size'] = file_size
        if size_limit:
            context['size_limit'] = size_limit
        
        super().__init__(
            message,
            error_code=ErrorCode.SIZE_LIMIT_EXCEEDED,
            context=context,
            user_message="Export file would be too large. Please reduce the data range.",
            **kwargs
        )


# =============================================================================
# CACHE EXCEPTIONS
# =============================================================================

class AnalyticsCacheException(AnalyticsException):
    """
    Base exception for cache-related errors.
    
    Covers errors related to caching operations,
    cache invalidation, and cache storage.
    """
    
    def __init__(self, message: str, **kwargs):
        super().__init__(
            message,
            error_code=kwargs.get('error_code', ErrorCode.UNKNOWN_ERROR),
            **kwargs
        )


class CacheUnavailableException(AnalyticsCacheException):
    """
    Exception raised when cache system is unavailable.
    
    Used when cache operations fail due to cache
    system being down or unreachable.
    """
    
    def __init__(
        self,
        message: str = "Cache system unavailable",
        cache_backend: Optional[str] = None,
        **kwargs
    ):
        context = kwargs.get('context', {})
        if cache_backend:
            context['cache_backend'] = cache_backend
        
        super().__init__(
            message,
            context=context,
            user_message="Temporary performance degradation may occur.",
            **kwargs
        )


class CacheInvalidationException(AnalyticsCacheException):
    """
    Exception raised when cache invalidation fails.
    
    Used when cache invalidation operations
    encounter errors.
    """
    
    def __init__(
        self,
        message: str = "Cache invalidation failed",
        cache_key: Optional[str] = None,
        **kwargs
    ):
        context = kwargs.get('context', {})
        if cache_key:
            context['cache_key'] = cache_key
        
        super().__init__(
            message,
            context=context,
            user_message="Data may not reflect latest changes immediately.",
            **kwargs
        )


# =============================================================================
# UTILITY FUNCTIONS
# =============================================================================

def handle_exception(
    exception: Exception,
    context: Optional[Dict[str, Any]] = None,
    reraise: bool = True
) -> Optional[AnalyticsException]:
    """
    Handle and convert generic exceptions to analytics exceptions.
    
    Args:
        exception: Original exception
        context: Additional context information
        reraise: Whether to reraise the converted exception
        
    Returns:
        Converted analytics exception or None
        
    Raises:
        AnalyticsException: If reraise is True
    """
    try:
        # If already an analytics exception, just add context
        if isinstance(exception, AnalyticsException):
            if context:
                for key, value in context.items():
                    exception.add_context(key, value)
            
            if reraise:
                raise exception
            return exception
        
        # Convert common exception types
        if isinstance(exception, (ValueError, TypeError)):
            analytics_exception = DataValidationException(
                str(exception),
                context=context
            )
        elif isinstance(exception, FileNotFoundError):
            analytics_exception = DataNotFoundException(
                str(exception),
                context=context
            )
        elif isinstance(exception, PermissionError):
            analytics_exception = InsufficientPermissionsException(
                str(exception),
                context=context
            )
        elif isinstance(exception, TimeoutError):
            analytics_exception = ProcessingTimeoutException(
                str(exception),
                context=context
            )
        else:
            # Generic conversion
            analytics_exception = AnalyticsException(
                str(exception),
                context=context,
                debug_info={'original_exception': type(exception).__name__}
            )
        
        if reraise:
            raise analytics_exception
        return analytics_exception
        
    except Exception as e:
        # Fallback if conversion fails
        logger.error(f"Exception handling failed: {e}")
        if reraise:
            raise exception
        return None


def log_exception_context(
    exception: AnalyticsException,
    additional_context: Optional[Dict[str, Any]] = None
) -> None:
    """
    Log exception with full context information.
    
    Args:
        exception: Analytics exception to log
        additional_context: Additional context to include
    """
    try:
        context = exception.context.copy()
        if additional_context:
            context.update(additional_context)
        
        logger.error(
            f"Analytics Exception: {exception.message}",
            extra={
                'exception_type': type(exception).__name__,
                'error_code': exception.error_code.value if exception.error_code else None,
                'context': context,
                'debug_info': exception.debug_info,
                'timestamp': exception.timestamp.isoformat()
            }
        )
        
    except Exception as e:
        logger.error(f"Exception logging failed: {e}")


def create_error_response(
    exception: AnalyticsException,
    include_debug: bool = False
) -> Dict[str, Any]:
    """
    Create standardized error response from exception.
    
    Args:
        exception: Analytics exception
        include_debug: Whether to include debug information
        
    Returns:
        Standardized error response dictionary
    """
    response = exception.get_api_response()
    
    if include_debug and exception.debug_info:
        response['debug'] = exception.debug_info
    
    return response


# =============================================================================
# EXPORTS
# =============================================================================

# Export all exception classes
__all__ = [
    # Base exceptions
    'AnalyticsException',
    
    # Data exceptions
    'AnalyticsDataException',
    'DataNotFoundException',
    'DataValidationException',
    'DataCorruptionException',
    'DataIncompleteException',
    
    # Processing exceptions
    'AnalyticsProcessingException',
    'ProcessingTimeoutException',
    'ProcessingFailedException',
    'ResourceExhaustedException',
    
    # Integration exceptions
    'AnalyticsIntegrationException',
    'APIException',
    'NetworkException',
    'ServiceUnavailableException',
    'RateLimitExceededException',
    
    # Permission exceptions
    'AnalyticsPermissionException',
    'InsufficientPermissionsException',
    'AuthenticationRequiredException',
    
    # Export exceptions
    'AnalyticsExportException',
    'ExportFailedException',
    'FormatNotSupportedException',
    'FileSizeLimitExceededException',
    
    # Cache exceptions
    'AnalyticsCacheException',
    'CacheUnavailableException',
    'CacheInvalidationException',
    
    # Utility functions
    'handle_exception',
    'log_exception_context',
    'create_error_response',
]