"""
BLOOM LIMS Validation Framework

Provides validation decorators and utilities for input validation.

Usage:
    from bloom_lims.core.validation import validate_euid, validated
    
    # Direct validation
    if validate_euid(euid):
        process(euid)
    
    # Decorator-based validation
    @validated(euid=validate_euid, type_name=validate_type)
    def process_object(euid: str, type_name: str):
        ...
"""

import re
import logging
from functools import wraps
from typing import Any, Callable, Dict, List, Optional, Union, TypeVar, ParamSpec

logger = logging.getLogger(__name__)

# Type variables for generic decorators
P = ParamSpec('P')
T = TypeVar('T')


class ValidationError(ValueError):
    """
    Exception raised when validation fails.
    
    Attributes:
        field: Name of the field that failed validation
        value: The invalid value
        message: Description of the validation failure
    """
    
    def __init__(self, field: str, value: Any, message: str):
        self.field = field
        self.value = value
        self.message = message
        super().__init__(f"Validation failed for '{field}': {message}")


# EUID pattern: Enterprise Unique Identifier
# Format: 2-3 letter prefix + sequence number (no leading zeros)
# Examples: CX1, CX123, WX1000, MRX42, CWX5
EUID_PATTERN = re.compile(r'^[A-Z]{2,3}[1-9][0-9]*$')

def validate_euid(value: Any, field_name: str = "euid") -> bool:
    """
    Validate an EUID (Enterprise Unique Identifier).

    BLOOM EUIDs follow the pattern: PREFIX + SEQUENCE_NUMBER
    - PREFIX: 2-3 uppercase letters identifying object type (e.g., CX, WX, MRX)
    - SEQUENCE_NUMBER: Integer with NO leading zeros (critical LIMS design principle)

    Examples: CX1, CX123, WX1000, MRX42, CWX5

    Args:
        value: Value to validate
        field_name: Name of the field for error messages

    Returns:
        True if valid

    Raises:
        ValidationError: If validation fails
    """
    if value is None:
        raise ValidationError(field_name, value, "EUID cannot be None")

    if not isinstance(value, str):
        raise ValidationError(field_name, value, f"EUID must be a string, got {type(value).__name__}")

    if not value:
        raise ValidationError(field_name, value, "EUID cannot be empty")

    value = value.upper().strip()

    if not EUID_PATTERN.match(value):
        raise ValidationError(
            field_name,
            value,
            "EUID must be PREFIX + sequence number (e.g., CX123, WX1000). No leading zeros allowed."
        )

    return True
def validate_json_addl(value: Any, field_name: str = "json_addl") -> bool:
    """
    Validate json_addl field structure.
    
    Args:
        value: Value to validate
        field_name: Name of the field for error messages
        
    Returns:
        True if valid
        
    Raises:
        ValidationError: If validation fails
    """
    if value is None:
        return True  # None is acceptable for json_addl
    
    if not isinstance(value, dict):
        raise ValidationError(field_name, value, f"json_addl must be a dict, got {type(value).__name__}")
    
    return True


def validate_type(value: Any, field_name: str = "type") -> bool:
    """
    Validate type field.

    Args:
        value: Value to validate
        field_name: Name of the field for error messages

    Returns:
        True if valid

    Raises:
        ValidationError: If validation fails
    """
    if value is None:
        raise ValidationError(field_name, value, "type cannot be None")

    if not isinstance(value, str):
        raise ValidationError(field_name, value, f"type must be a string, got {type(value).__name__}")

    if not value:
        raise ValidationError(field_name, value, "type cannot be empty")

    return True
def validate_not_empty(value: Any, field_name: str = "value") -> bool:
    """
    Validate that a value is not empty.

    Args:
        value: Value to validate
        field_name: Name of the field for error messages

    Returns:
        True if valid

    Raises:
        ValidationError: If validation fails
    """
    if value is None:
        raise ValidationError(field_name, value, f"{field_name} cannot be None")

    if isinstance(value, str) and not value.strip():
        raise ValidationError(field_name, value, f"{field_name} cannot be empty")

    if isinstance(value, (list, dict)) and len(value) == 0:
        raise ValidationError(field_name, value, f"{field_name} cannot be empty")

    return True


def validate_positive_int(value: Any, field_name: str = "value") -> bool:
    """
    Validate that a value is a positive integer.

    Args:
        value: Value to validate
        field_name: Name of the field for error messages

    Returns:
        True if valid

    Raises:
        ValidationError: If validation fails
    """
    if value is None:
        raise ValidationError(field_name, value, f"{field_name} cannot be None")

    if not isinstance(value, int) or isinstance(value, bool):
        raise ValidationError(field_name, value, f"{field_name} must be an integer, got {type(value).__name__}")

    if value <= 0:
        raise ValidationError(field_name, value, f"{field_name} must be positive, got {value}")

    return True


def validated(**validators: Callable[[Any, str], bool]) -> Callable[[Callable[P, T]], Callable[P, T]]:
    """
    Decorator to validate function arguments.

    Usage:
        @validated(euid=validate_euid, type_name=validate_type)
        def process_object(euid: str, type_name: str):
            ...

    Args:
        **validators: Mapping of argument names to validator functions

    Returns:
        Decorated function that validates arguments before execution
    """
    def decorator(func: Callable[P, T]) -> Callable[P, T]:
        @wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
            # Get function signature to map positional args to names
            import inspect
            sig = inspect.signature(func)
            params = list(sig.parameters.keys())

            # Build dict of all arguments
            all_args = {}
            for i, arg in enumerate(args):
                if i < len(params):
                    all_args[params[i]] = arg
            all_args.update(kwargs)

            # Run validators
            for arg_name, validator in validators.items():
                if arg_name in all_args:
                    try:
                        validator(all_args[arg_name], arg_name)
                    except ValidationError:
                        raise
                    except Exception as e:
                        raise ValidationError(arg_name, all_args[arg_name], str(e))

            return func(*args, **kwargs)
        return wrapper
    return decorator


def validate_schema(data: Dict[str, Any], schema: Dict[str, Dict[str, Any]]) -> List[ValidationError]:
    """
    Validate a dictionary against a schema.

    Schema format:
        {
            "field_name": {
                "type": str,  # Expected type
                "required": True,  # Whether field is required
                "validator": validate_euid,  # Optional validator function
                "default": None,  # Default value if not provided
            }
        }

    Args:
        data: Dictionary to validate
        schema: Schema definition

    Returns:
        List of ValidationError objects (empty if valid)
    """
    errors = []

    for field_name, field_schema in schema.items():
        value = data.get(field_name)
        required = field_schema.get("required", False)
        expected_type = field_schema.get("type")
        validator = field_schema.get("validator")

        # Check required fields
        if required and value is None:
            errors.append(ValidationError(field_name, value, f"{field_name} is required"))
            continue

        # Skip validation for None values on optional fields
        if value is None:
            continue

        # Type check
        if expected_type and not isinstance(value, expected_type):
            errors.append(ValidationError(
                field_name, value,
                f"Expected {expected_type.__name__}, got {type(value).__name__}"
            ))
            continue

        # Custom validator
        if validator:
            try:
                validator(value, field_name)
            except ValidationError as e:
                errors.append(e)

    return errors

