"""Telemetry attribute validation module for DoS prevention.

This module validates attributes passed to telemetry recording methods
(e.g., record_prediction) to prevent oversized or malformed attributes
from causing system instability.

Separate from config attribute validation in validator.py (Story 2.5).
"""

import itertools
import logging
import re
import sys
from typing import Any, Dict, List, Union

from ..utils.exceptions import ValidationError

logger = logging.getLogger(__name__)


class TelemetryAttributeValidator:
    """Validates telemetry attributes to prevent DoS and ensure compatibility.

    This validator checks:
    - Attribute value length (max 1024 chars by default, configurable)
    - Attribute name format (alphanumeric + underscore/dot, starts with letter or underscore)
    - Attribute count (max 100 attributes by default, configurable)

    Preserves OpenTelemetry-native types (int, float, bool, arrays) instead of
    converting everything to strings.

    Modes:
    - Strict mode: Raises ValidationError on violations
    - Lenient mode: Logs warnings and truncates/drops invalid attributes
    """

    MAX_VALUE_LENGTH = 1024
    MAX_ATTRIBUTE_COUNT = 100
    # Relaxed naming: allow underscore prefix and numbers after first char
    VALID_NAME_PATTERN = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_.]*$")

    # Bounds for numeric types to prevent memory exhaustion
    # These limits correspond to reasonable JSON serialization sizes
    MAX_INT_VALUE = 2**53  # JavaScript safe integer limit (fits in 64-bit float)
    MIN_INT_VALUE = -(2**53)
    MAX_FLOAT_ABS = 1e308  # Near Python float max, well within JSON spec

    # Size estimates for non-string types (bytes)
    _SIZE_ESTIMATES = {
        int: 8,
        float: 8,
        bool: 1,
        type(None): 0,
    }

    @classmethod
    def validate(
        cls,
        attributes: Dict[str, Any],
        strict: bool = True,
        max_value_length: int = MAX_VALUE_LENGTH,
        max_attribute_count: int = MAX_ATTRIBUTE_COUNT,
    ) -> Dict[str, Union[str, int, float, bool, List]]:
        """Validate and optionally sanitize attributes.

        Args:
            attributes: Dictionary of attribute name-value pairs
            strict: If True, raise exception on validation failure.
                   If False, log warnings and sanitize invalid attributes.
            max_value_length: Maximum length for string values (chars)
            max_attribute_count: Maximum number of attributes

        Returns:
            Validated (and potentially sanitized) attributes dictionary
            with OpenTelemetry-native types preserved

        Raises:
            ValidationError: If strict=True and validation fails
        """
        if not isinstance(attributes, dict):
            if strict:
                raise ValidationError(
                    f"Attributes must be a dictionary, got {type(attributes).__name__}"
                )
            return {}

        # Validate attribute count FIRST (before processing values)
        if len(attributes) > max_attribute_count:
            if strict:
                raise ValidationError(
                    f"Attribute count ({len(attributes)}) exceeds maximum "
                    f"({max_attribute_count}). Reduce the number of attributes."
                )
            else:
                logger.warning(
                    f"Dropping {len(attributes) - max_attribute_count} excess attributes"
                )
                # FIXED (Round 8 Issue #4): Sort keys for deterministic attribute dropping
                sorted_items = sorted(attributes.items())
                attributes = dict(itertools.islice(sorted_items, max_attribute_count))

        validated: Dict[str, Union[str, int, float, bool, List[Any]]] = {}
        for name, value in attributes.items():
            # Validate name
            if not cls.VALID_NAME_PATTERN.match(name):
                if strict:
                    raise ValidationError(
                        f"Invalid attribute name: '{name}'. Must match pattern: "
                        f"{cls.VALID_NAME_PATTERN.pattern}"
                    )
                else:
                    logger.warning(f"Skipping invalid attribute name: {name}")
                    continue

            # Handle None
            if value is None:
                validated[name] = ""
                continue

            # Preserve native OpenTelemetry types (int, float, bool)
            if isinstance(value, bool):
                validated[name] = value
                continue

            if isinstance(value, int):
                # Validate integer bounds to prevent memory exhaustion
                if value < cls.MIN_INT_VALUE or value > cls.MAX_INT_VALUE:
                    if strict:
                        raise ValidationError(
                            f"Attribute '{name}' integer value out of safe bounds "
                            f"({cls.MIN_INT_VALUE} to {cls.MAX_INT_VALUE}). "
                            f"Got: {value}. Large integers can cause memory exhaustion "
                            f"during JSON serialization."
                        )
                    else:
                        logger.warning(
                            f"Attribute '{name}' integer out of bounds, clamping to safe range"
                        )
                        validated[name] = max(cls.MIN_INT_VALUE, min(value, cls.MAX_INT_VALUE))
                else:
                    validated[name] = value
                continue

            if isinstance(value, float):
                # Validate float bounds to prevent memory exhaustion
                # Handle inf/nan specially - they should be clamped in lenient mode
                import math
                if math.isinf(value) or math.isnan(value):
                    if strict:
                        raise ValidationError(
                            f"Attribute '{name}' float value is inf or nan. "
                            f"Use finite values only."
                        )
                    else:
                        logger.warning(
                            f"Attribute '{name}' float is inf/nan, clamping to safe range"
                        )
                        # Clamp inf to max bounds, nan to 0
                        if math.isinf(value) and value > 0:
                            validated[name] = cls.MAX_FLOAT_ABS
                        elif math.isinf(value) and value < 0:
                            validated[name] = -cls.MAX_FLOAT_ABS
                        else:  # nan
                            validated[name] = 0.0
                    continue

                abs_value = abs(value)
                if abs_value > cls.MAX_FLOAT_ABS:
                    if strict:
                        raise ValidationError(
                            f"Attribute '{name}' float value exceeds safe bounds "
                            f"(abs value > {cls.MAX_FLOAT_ABS}). "
                            f"Got: {value}. Large floats can cause issues during serialization."
                        )
                    else:
                        logger.warning(
                            f"Attribute '{name}' float out of bounds, clamping to safe range"
                        )
                        # Preserve sign, clamp magnitude
                        validated[name] = cls.MAX_FLOAT_ABS if value > 0 else -cls.MAX_FLOAT_ABS
                else:
                    validated[name] = value
                continue

            # Handle lists/arrays (OpenTelemetry supports arrays of primitives)
            if isinstance(value, (list, tuple)):
                try:
                    validated_list = cls._validate_list(name, value, strict, max_value_length)
                    validated[name] = validated_list
                except ValidationError:
                    if strict:
                        raise
                    # Lenient mode: skip invalid lists
                    continue
                continue

            # For other types, estimate size BEFORE string conversion (DoS prevention)
            if isinstance(value, str):
                size = len(value)
            else:
                # Estimate size before conversion to prevent memory exhaustion
                estimated_size = cls._estimate_size(value)

                if estimated_size > max_value_length:
                    if strict:
                        raise ValidationError(
                            f"Attribute '{name}' estimated size ({estimated_size} chars) "
                            f"exceeds maximum ({max_value_length}). "
                            f"Type: {type(value).__name__}. "
                            f"Refusing to convert to string to prevent memory exhaustion."
                        )
                    else:
                        logger.warning(
                            f"Attribute '{name}' estimated size exceeds limit, using placeholder"
                        )
                        validated[name] = f"<{type(value).__name__}:too_large>"
                        continue

                # Safe to convert after size check
                try:
                    str_value = str(value)
                    size = len(str_value)
                except Exception as e:
                    if strict:
                        raise ValidationError(
                            f"Attribute '{name}' cannot be converted to string: {e}"
                        )
                    else:
                        logger.warning(f"Attribute '{name}' conversion failed, using placeholder")
                        validated[name] = f"<{type(value).__name__}:error>"
                        continue

            # Validate string length
            if size > max_value_length:
                if strict:
                    raise ValidationError(
                        f"Attribute '{name}' value length ({size}) exceeds maximum "
                        f"({max_value_length}). Truncate the value before passing to SDK."
                    )
                else:
                    # Truncate to (max_value_length - len("[truncated]")) to ensure final length <= max_value_length
                    truncate_suffix = "[truncated]"
                    suffix_len = len(truncate_suffix)

                    # Handle edge case: if max_value_length < suffix length, truncate hard without suffix
                    if max_value_length < suffix_len:
                        truncate_at = max_value_length
                        logger.warning(
                            f"Truncating attribute '{name}' to {max_value_length} chars (no suffix - limit too small)"
                        )
                        if isinstance(value, str):
                            validated[name] = value[:truncate_at]
                        else:
                            validated[name] = str_value[:truncate_at]
                    else:
                        truncate_at = max_value_length - suffix_len
                        logger.warning(
                            f"Truncating attribute '{name}' from {size} to {max_value_length} chars"
                        )
                        if isinstance(value, str):
                            validated[name] = value[:truncate_at] + truncate_suffix
                        else:
                            validated[name] = str_value[:truncate_at] + truncate_suffix
            else:
                if isinstance(value, str):
                    validated[name] = value
                else:
                    validated[name] = str_value

        return validated

    @classmethod
    def _estimate_size(cls, value: Any) -> int:
        """Estimate the size of a value before string conversion.

        This prevents DoS attacks via memory exhaustion by checking size
        BEFORE calling str(value) on potentially massive objects.

        Args:
            value: Value to estimate

        Returns:
            Estimated size in characters
        """
        value_type = type(value)

        # Use predefined estimates for common types
        if value_type in cls._SIZE_ESTIMATES:
            return cls._SIZE_ESTIMATES[value_type]

        # For objects with __sizeof__, use that as approximation
        if hasattr(value, "__sizeof__"):
            try:
                # sys.getsizeof includes object overhead
                return sys.getsizeof(value)
            except Exception:
                pass

        # For collections, estimate based on length
        if hasattr(value, "__len__"):
            try:
                length = len(value)
                # Rough estimate: collection overhead + items
                return length * 10
            except Exception:
                pass

        # Default conservative estimate for unknown types
        return 100

    @classmethod
    def _validate_list(
        cls,
        name: str,
        value: Union[list, tuple],
        strict: bool,
        max_value_length: int,
    ) -> List[Union[str, int, float, bool, None]]:
        """Validate a list of primitives (OpenTelemetry array support).

        Args:
            name: Attribute name (for error messages)
            value: List or tuple to validate
            strict: If True, raise ValidationError on violation
            max_value_length: Maximum length for string values

        Returns:
            Validated list of primitives

        Raises:
            ValidationError: If validation fails in strict mode
        """
        if len(value) == 0:
            return []

        validated_list: List[Union[str, int, float, bool, None]] = []
        for i, item in enumerate(value):
            # OpenTelemetry supports arrays of primitives
            if isinstance(item, (str, int, float, bool)) or item is None:
                # Validate string items
                if isinstance(item, str) and len(item) > max_value_length:
                    if strict:
                        raise ValidationError(
                            f"Attribute '{name}' list item [{i}] exceeds maximum length "
                            f"({len(item)} > {max_value_length})"
                        )
                    else:
                        # Truncate to (max_value_length - len("[truncated]")) to ensure final length <= max_value_length
                        truncate_suffix = "[truncated]"
                        suffix_len = len(truncate_suffix)

                        # Handle edge case: if max_value_length < suffix length, truncate hard without suffix
                        if max_value_length < suffix_len:
                            truncate_at = max_value_length
                            logger.warning(
                                f"Truncating list item '{name}[{i}]' (no suffix - limit too small)"
                            )
                            validated_list.append(item[:truncate_at])
                        else:
                            truncate_at = max_value_length - suffix_len
                            logger.warning(f"Truncating list item '{name}[{i}]'")
                            validated_list.append(item[:truncate_at] + truncate_suffix)
                else:
                    validated_list.append(item)
            else:
                if strict:
                    raise ValidationError(
                        f"Attribute '{name}' list contains non-primitive type at index {i}: "
                        f"{type(item).__name__}. Only str, int, float, bool, None are supported."
                    )
                else:
                    logger.warning(f"Skipping non-primitive in list '{name}[{i}]'")
                    # Lenient mode: skip non-primitives
                    continue

        return validated_list
