"""
Input validation utilities for CAVORA.
Ensures data meets expected formats and ranges.

Author: Samir Baladi
"""

import numpy as np
from typing import Any, Dict, List, Tuple, Optional, Union
import re


class ValidationError(Exception):
    """Custom exception for validation errors."""
    pass


class Validators:
    """
    Input validation utilities for cave survey data.
    """
    
    # Valid ranges for parameters (from paper)
    PARAMETER_RANGES = {
        'V': (0.1, 50.0),  # m - vertical passage
        'L': (0.1, 30.0),  # m - lateral conduit
        'F': (0, 1000),    # mm - flowstone thickness
        'Q': (0, 100),     # m³/s - discharge
        'CO2': (300, 50000),  # ppm - CO₂ concentration
        'temperature': (-10, 30),  # °C - cave temperature
        'humidity': (0, 100),  # % - relative humidity
        'depth': (0, 3000),  # m - depth below surface
        'pressure': (80000, 105000),  # Pa - atmospheric pressure
        'pH': (0, 14),  # pH value
        'calcium': (0, 1000),  # mg/L - calcium concentration
        'alkalinity': (0, 1000)  # mg/L - alkalinity
    }
    
    # Valid string patterns
    STATION_NAME_PATTERN = r'^[A-Za-z0-9\-_]+$'
    SENSOR_ID_PATTERN = r'^[A-Z][0-9]{3}$'  # e.g., T001, H002
    DATE_PATTERN = r'^\d{4}-\d{2}-\d{2}$'  # YYYY-MM-DD
    TIME_PATTERN = r'^\d{2}:\d{2}:\d{2}$'  # HH:MM:SS
    
    @classmethod
    def validate_parameter(cls, value: float, param_name: str, 
                          allow_none: bool = False) -> float:
        """
        Validate a single parameter against its allowed range.
        
        Args:
            value: Parameter value
            param_name: Parameter name
            allow_none: Whether None is allowed
            
        Returns:
            Validated value (or None if allowed)
            
        Raises:
            ValidationError: If validation fails
        """
        if value is None and allow_none:
            return None
        
        if value is None:
            raise ValidationError(f"{param_name} cannot be None")
        
        if not isinstance(value, (int, float, np.number)):
            raise ValidationError(f"{param_name} must be a number, got {type(value)}")
        
        if param_name not in cls.PARAMETER_RANGES:
            # Unknown parameter, just return
            return float(value)
        
        min_val, max_val = cls.PARAMETER_RANGES[param_name]
        
        if value < min_val or value > max_val:
            raise ValidationError(
                f"{param_name} value {value} outside valid range "
                f"[{min_val}, {max_val}]"
            )
        
        return float(value)
    
    @classmethod
    def validate_station_name(cls, name: str) -> str:
        """
        Validate survey station name.
        
        Args:
            name: Station name
            
        Returns:
            Validated station name
            
        Raises:
            ValidationError: If validation fails
        """
        if not isinstance(name, str):
            raise ValidationError(f"Station name must be string, got {type(name)}")
        
        if not name:
            raise ValidationError("Station name cannot be empty")
        
        if not re.match(cls.STATION_NAME_PATTERN, name):
            raise ValidationError(
                f"Invalid station name '{name}'. Must contain only letters, "
                f"numbers, hyphens, and underscores"
            )
        
        return name.strip()
    
    @classmethod
    def validate_sensor_id(cls, sensor_id: str) -> str:
        """
        Validate sensor ID (e.g., T001, H002).
        
        Args:
            sensor_id: Sensor identifier
            
        Returns:
            Validated sensor ID
            
        Raises:
            ValidationError: If validation fails
        """
        if not isinstance(sensor_id, str):
            raise ValidationError(f"Sensor ID must be string, got {type(sensor_id)}")
        
        if not re.match(cls.SENSOR_ID_PATTERN, sensor_id):
            raise ValidationError(
                f"Invalid sensor ID '{sensor_id}'. Must be letter followed by "
                f"3 digits (e.g., T001)"
            )
        
        return sensor_id.upper()
    
    @classmethod
    def validate_datetime(cls, date_str: str, time_str: Optional[str] = None) -> str:
        """
        Validate date and time strings.
        
        Args:
            date_str: Date in YYYY-MM-DD format
            time_str: Time in HH:MM:SS format (optional)
            
        Returns:
            Combined datetime string
            
        Raises:
            ValidationError: If validation fails
        """
        if not re.match(cls.DATE_PATTERN, date_str):
            raise ValidationError(
                f"Invalid date format '{date_str}'. Use YYYY-MM-DD"
            )
        
        if time_str is not None:
            if not re.match(cls.TIME_PATTERN, time_str):
                raise ValidationError(
                    f"Invalid time format '{time_str}'. Use HH:MM:SS"
                )
            return f"{date_str} {time_str}"
        
        return date_str
    
    @classmethod
    def validate_coordinates(cls, x: float, y: float, z: float) -> Tuple[float, float, float]:
        """
        Validate 3D coordinates.
        
        Args:
            x, y, z: Coordinates
            
        Returns:
            Validated coordinates
            
        Raises:
            ValidationError: If validation fails
        """
        for coord, name in [(x, 'X'), (y, 'Y'), (z, 'Z')]:
            if not isinstance(coord, (int, float, np.number)):
                raise ValidationError(f"{name} coordinate must be a number")
        
        # No specific range for coordinates, just ensure they're finite
        if not np.isfinite(x) or not np.isfinite(y) or not np.isfinite(z):
            raise ValidationError("Coordinates must be finite numbers")
        
        return float(x), float(y), float(z)
    
    @classmethod
    def validate_angle(cls, angle: float, angle_type: str = 'azimuth') -> float:
        """
        Validate angular measurement.
        
        Args:
            angle: Angle value
            angle_type: 'azimuth' (0-360) or 'inclination' (-90 to 90)
            
        Returns:
            Validated angle
            
        Raises:
            ValidationError: If validation fails
        """
        if not isinstance(angle, (int, float, np.number)):
            raise ValidationError(f"Angle must be a number, got {type(angle)}")
        
        if angle_type == 'azimuth':
            # Azimuth should be 0-360
            if angle < 0 or angle > 360:
                raise ValidationError(f"Azimuth {angle} outside range [0, 360]")
            return float(angle) % 360
        
        elif angle_type == 'inclination':
            # Inclination should be -90 to 90
            if angle < -90 or angle > 90:
                raise ValidationError(f"Inclination {angle} outside range [-90, 90]")
            return float(angle)
        
        else:
            raise ValidationError(f"Unknown angle type: {angle_type}")
    
    @classmethod
    def validate_probability(cls, prob: float) -> float:
        """
        Validate probability value (0-1).
        
        Args:
            prob: Probability value
            
        Returns:
            Validated probability
            
        Raises:
            ValidationError: If validation fails
        """
        if not isinstance(prob, (int, float, np.number)):
            raise ValidationError(f"Probability must be a number, got {type(prob)}")
        
        if prob < 0 or prob > 1:
            raise ValidationError(f"Probability {prob} outside range [0, 1]")
        
        return float(prob)
    
    @classmethod
    def validate_positive(cls, value: float, name: str = "Value") -> float:
        """
        Validate that a value is positive.
        
        Args:
            value: Value to validate
            name: Parameter name for error message
            
        Returns:
            Validated value
            
        Raises:
            ValidationError: If validation fails
        """
        if not isinstance(value, (int, float, np.number)):
            raise ValidationError(f"{name} must be a number, got {type(value)}")
        
        if value <= 0:
            raise ValidationError(f"{name} must be positive, got {value}")
        
        return float(value)
    
    @classmethod
    def validate_non_negative(cls, value: float, name: str = "Value") -> float:
        """
        Validate that a value is non-negative.
        
        Args:
            value: Value to validate
            name: Parameter name for error message
            
        Returns:
            Validated value
            
        Raises:
            ValidationError: If validation fails
        """
        if not isinstance(value, (int, float, np.number)):
            raise ValidationError(f"{name} must be a number, got {type(value)}")
        
        if value < 0:
            raise ValidationError(f"{name} must be non-negative, got {value}")
        
        return float(value)
    
    @classmethod
    def validate_array(cls, arr: np.ndarray, 
                      min_length: int = 1,
                      max_length: Optional[int] = None,
                      finite_only: bool = True) -> np.ndarray:
        """
        Validate numpy array.
        
        Args:
            arr: Input array
            min_length: Minimum array length
            max_length: Maximum array length
            finite_only: Require all values to be finite
            
        Returns:
            Validated array
            
        Raises:
            ValidationError: If validation fails
        """
        if not isinstance(arr, np.ndarray):
            try:
                arr = np.array(arr)
            except:
                raise ValidationError(f"Cannot convert to numpy array: {type(arr)}")
        
        if len(arr) < min_length:
            raise ValidationError(f"Array length {len(arr)} < minimum {min_length}")
        
        if max_length is not None and len(arr) > max_length:
            raise ValidationError(f"Array length {len(arr)} > maximum {max_length}")
        
        if finite_only and not np.all(np.isfinite(arr)):
            raise ValidationError("Array contains non-finite values")
        
        return arr
    
    @classmethod
    def validate_dictionary(cls, data: Dict, required_keys: List[str]) -> Dict:
        """
        Validate dictionary contains required keys.
        
        Args:
            data: Input dictionary
            required_keys: List of required keys
            
        Returns:
            Validated dictionary
            
        Raises:
            ValidationError: If validation fails
        """
        if not isinstance(data, dict):
            raise ValidationError(f"Expected dictionary, got {type(data)}")
        
        missing = [key for key in required_keys if key not in data]
        if missing:
            raise ValidationError(f"Missing required keys: {missing}")
        
        return data
    
    @classmethod
    def validate_file_extension(cls, filename: str, allowed_extensions: List[str]) -> str:
        """
        Validate file extension.
        
        Args:
            filename: Input filename
            allowed_extensions: List of allowed extensions (e.g., ['.csv', '.txt'])
            
        Returns:
            Validated filename
            
        Raises:
            ValidationError: If validation fails
        """
        import os
        
        ext = os.path.splitext(filename)[1].lower()
        if ext not in allowed_extensions:
            raise ValidationError(
                f"File extension {ext} not allowed. Allowed: {allowed_extensions}"
            )
        
        return filename


# Example usage
if __name__ == "__main__":
    print("="*60)
    print("CAVORA - Validators")
    print("="*60)
    
    v = Validators()
    
    # Test valid values
    try:
        v.validate_parameter(2.5, 'V')
        v.validate_parameter(0.08, 'F')
        v.validate_station_name("A-123")
        v.validate_sensor_id("T001")
        print("\n✅ All validations passed")
    except ValidationError as e:
        print(f"\n❌ Validation error: {e}")
    
    # Test invalid values
    print("\nTesting invalid values:")
    try:
        v.validate_parameter(100, 'V')  # Too large
    except ValidationError as e:
        print(f"  ✓ Caught: {e}")
    
    try:
        v.validate_station_name("A@123")  # Invalid character
    except ValidationError as e:
        print(f"  ✓ Caught: {e}")
    
    print("\n" + "="*60)
