"""
Safe collection operations for beginners.

This module provides safe versions of common collection operations
that prevent typical mistakes and provide helpful error messages.

Type safety: Uses TypeVar and overload for better type checking.
"""

from typing import Any, Optional, Union, List, Dict, Tuple, TypeVar, overload

# Type variables for better type safety
T = TypeVar('T')
K = TypeVar('K')
V = TypeVar('V')


# Overloaded signatures for safe_get to provide better type hints
@overload
def safe_get(collection: Dict[K, V], index: K, default: V) -> V: ...

@overload
def safe_get(collection: List[T], index: int, default: T) -> T: ...

@overload
def safe_get(collection: Tuple[T, ...], index: int, default: T) -> T: ...

@overload
def safe_get(collection: str, index: int, default: str) -> str: ...


def safe_get(collection: Union[List, Tuple, Dict, str], 
             index: Union[int, str], 
             default: Any = None) -> Any:
    """
    Safely get an element from a collection by index or key.
    
    Works with lists, tuples, dicts, and strings. Returns default value
    instead of raising KeyError or IndexError.
    
    Args:
        collection: Collection (list, tuple, dict, or string)
        index: Index (for sequences) or key (for dicts)
        default: Value to return if element not found
        
    Returns:
        Element from collection or default value
        
    Raises:
        SafeUtilityError: If collection is None or obviously wrong type
        
    Examples:
        >>> safe_get([1, 2, 3], 1)
        2
        >>> safe_get([1, 2, 3], 10, "not found")
        'not found'
        >>> safe_get({"name": "Ivan"}, "name")
        'Ivan'
        >>> safe_get({"name": "Ivan"}, "age", 0)
        0
        >>> safe_get("hello", 0)
        'h'
        >>> safe_get("hello", 10)
        None
        
    Note:
        We check for obviously wrong types (None, bool, numbers) to help beginners.
        For valid collection types, we use EAFP (Easier to Ask Forgiveness than Permission).
    """
    from ..errors.exceptions import SafeUtilityError
    
    # Check for obviously wrong types that beginners might pass
    if collection is None or isinstance(collection, (bool, int, float, complex)):
        raise SafeUtilityError(
            "Коллекция не может быть None или числом. Передайте список, кортеж, словарь или строку.",
            utility_name="safe_get"
        )
    
    # For everything else, use EAFP - try and handle exceptions
    try:
        return collection[index]
    except (KeyError, IndexError, TypeError):
        # KeyError: dict key not found
        # IndexError: sequence index out of range
        # TypeError: wrong index type or unsupported collection
        return default


def safe_divide(a: Union[int, float], b: Union[int, float], 
                default: Optional[Union[int, float]] = None) -> Optional[Union[int, float]]:
    """
    Safely divide two numbers with zero division handling.
    
    Returns None when dividing by zero (mathematically correct: undefined).
    You can specify a custom default value if needed.
    
    Args:
        a: Dividend (number)
        b: Divisor (number)
        default: Value to return when dividing by zero (default: None)
        
    Returns:
        Result of division, or default value if b is zero
        
    Raises:
        SafeUtilityError: If arguments are obviously wrong types (None, bool, complex, str)
        
    Examples:
        >>> safe_divide(10, 2)
        5.0
        >>> safe_divide(10, 0)  # Mathematically undefined
        None
        >>> safe_divide(10, 0, default=0)  # Explicitly specified
        0
        >>> safe_divide(10, 0, default=float('inf'))
        inf
        
    Note:
        Division by zero is mathematically undefined. Returning 0 by default
        would be incorrect. Use default parameter if you need specific behavior.
    """
    from ..errors.exceptions import SafeUtilityError
    import math
    
    # Check for obviously wrong types that beginners might pass
    if a is None or isinstance(a, (bool, complex, str)):
        raise SafeUtilityError(
            f"Делимое должно быть числом (int или float), получен {type(a).__name__}",
            utility_name="safe_divide"
        )
    
    if b is None or isinstance(b, (bool, complex, str)):
        raise SafeUtilityError(
            f"Делитель должен быть числом (int или float), получен {type(b).__name__}",
            utility_name="safe_divide"
        )
    
    # Check for zero division
    if b == 0:
        return default
    
    try:
        # Perform division
        result = a / b
        
        # Check for infinity or NaN (edge cases)
        if math.isinf(result) or math.isnan(result):
            return default
        
        return result
    except (TypeError, ValueError):
        # Fallback for any other edge cases
        return default


def safe_max(collection: Union[List, Tuple], default: Any = None) -> Any:
    """
    Safely find maximum value in a collection.
    
    Returns default value for empty collections instead of raising ValueError.
    
    Args:
        collection: Collection of comparable items
        default: Value to return for empty collection
        
    Returns:
        Maximum value or default value
        
    Examples:
        >>> safe_max([1, 5, 3])
        5
        >>> safe_max([])
        None
        >>> safe_max([], 0)
        0
        >>> safe_max(['a', 'z', 'b'])
        'z'
    """
    try:
        return max(collection)
    except (ValueError, TypeError):
        # ValueError: empty sequence
        # TypeError: items not comparable
        return default


def safe_min(collection: Union[List, Tuple], default: Any = None) -> Any:
    """
    Safely find minimum value in a collection.
    
    Returns default value for empty collections instead of raising ValueError.
    
    Args:
        collection: Collection of comparable items
        default: Value to return for empty collection
        
    Returns:
        Minimum value or default value
        
    Examples:
        >>> safe_min([1, 5, 3])
        1
        >>> safe_min([])
        None
        >>> safe_min([], 0)
        0
        >>> safe_min(['a', 'z', 'b'])
        'a'
    """
    try:
        return min(collection)
    except (ValueError, TypeError):
        # ValueError: empty sequence
        # TypeError: items not comparable
        return default


def safe_sum(collection: Union[List, Tuple], default: Union[int, float] = 0) -> Union[int, float]:
    """
    Safely calculate sum of a collection.
    
    Returns default value for empty collections or if items can't be summed.
    
    Args:
        collection: Collection of numbers
        default: Value to return for empty collection or on error
        
    Returns:
        Sum of elements or default value
        
    Examples:
        >>> safe_sum([1, 2, 3])
        6
        >>> safe_sum([])
        0
        >>> safe_sum([], 10)
        10
        >>> safe_sum([1.5, 2.5, 3.0])
        7.0
    """
    # Handle empty collection explicitly
    if not collection:
        return default
    
    try:
        return sum(collection)
    except (TypeError, ValueError):
        # TypeError: items not numbers
        # ValueError: other calculation errors
        return default


def safe_average(numbers: List[Union[int, float]], 
                 default: Union[int, float] = 0) -> float:
    """
    Safely calculate average of a collection with error protection.
    
    Filters out non-numeric values and handles empty collections gracefully.
    Returns default value instead of raising division by zero errors.
    
    Args:
        numbers: List of numbers (may contain non-numeric values)
        default: Value to return for empty list or when no valid numbers found
        
    Returns:
        Average of numeric values as float, or default value
        
    Examples:
        >>> safe_average([1, 2, 3])
        2.0
        >>> safe_average([])
        0
        >>> safe_average([], default=0)
        0
        >>> safe_average([1, 2, "invalid", 3])
        2.0
        >>> safe_average([1.5, 2.5, 3.0])
        2.3333333333333335
        >>> safe_average(["a", "b", "c"], default=-1)
        -1
        
    Note:
        Non-numeric values are silently filtered out. If all values are
        non-numeric or the list is empty, returns the default value.
    """
    # Handle empty collection explicitly
    if not numbers:
        return default
    
    # Filter out non-numeric values
    valid_numbers = []
    for num in numbers:
        if isinstance(num, (int, float)) and not isinstance(num, bool):
            valid_numbers.append(num)
    
    # If no valid numbers found, return default
    if not valid_numbers:
        return default
    
    try:
        # Calculate average
        return sum(valid_numbers) / len(valid_numbers)
    except (TypeError, ValueError, ZeroDivisionError):
        # Fallback for any edge cases
        return default