"""
Parameter validation for laser control module.

Validates all input parameters against physical constraints
and protocol limits before sending to device.
"""

import math
from typing import Dict, Any, Tuple

from .constants import (
    TEMP_MIN_C, TEMP_MAX_C,
    CURRENT_MIN_MA, CURRENT_MAX_MA,
    CURRENT_STEP_MIN_MA, CURRENT_STEP_MAX_MA,
    TEMP_STEP_MIN_C, TEMP_STEP_MAX_C,
    TIME_STEP_MIN_US, TIME_STEP_MAX_US,
    DELAY_TIME_MIN_MS, DELAY_TIME_MAX_MS,
)
from .exceptions import (
    ValidationError,
    TemperatureOutOfRangeError,
    CurrentOutOfRangeError,
    InvalidParameterError,
)
from .models import VariationType


class ParameterValidator:
    """Validates all input parameters for the laser controller."""

    @staticmethod
    def _check_numeric(value: Any, param_name: str) -> float:
        """Check that value is a valid finite number. Returns float."""
        if value is None:
            raise InvalidParameterError(param_name, "Value must not be None")
        if not isinstance(value, (int, float)):
            raise InvalidParameterError(param_name, "Value must be a number")
        if math.isnan(value):
            raise InvalidParameterError(param_name, "Value must not be NaN")
        if math.isinf(value):
            raise InvalidParameterError(param_name, "Value must not be infinite")
        return float(value)

    @staticmethod
    def validate_temperature(value: Any, param_name: str) -> float:
        """
        Validate a laser temperature value.

        Args:
            value: Temperature in °C.
            param_name: Parameter name for error messages.

        Returns:
            Validated temperature as float.

        Raises:
            InvalidParameterError: If value is not a valid number.
            TemperatureOutOfRangeError: If value is outside [TEMP_MIN_C, TEMP_MAX_C].
        """
        value = ParameterValidator._check_numeric(value, param_name)
        if value < TEMP_MIN_C or value > TEMP_MAX_C:
            raise TemperatureOutOfRangeError(
                param_name, value, TEMP_MIN_C, TEMP_MAX_C
            )
        return value

    @staticmethod
    def validate_current(value: Any, param_name: str) -> float:
        """
        Validate a laser drive current value.

        Args:
            value: Current in mA.
            param_name: Parameter name for error messages.

        Returns:
            Validated current as float.

        Raises:
            InvalidParameterError: If value is not a valid number.
            CurrentOutOfRangeError: If value is outside [CURRENT_MIN_MA, CURRENT_MAX_MA].
        """
        value = ParameterValidator._check_numeric(value, param_name)
        if value < CURRENT_MIN_MA or value > CURRENT_MAX_MA:
            raise CurrentOutOfRangeError(
                param_name, value, CURRENT_MIN_MA, CURRENT_MAX_MA
            )
        return value

    @staticmethod
    def validate_time_params(time_step: Any, delay_time: Any) -> Tuple[int, int]:
        """
        Validate time parameters for variation mode.

        Args:
            time_step: Discretisation time step in microseconds.
            delay_time: Delay between pulses in milliseconds.

        Returns:
            Tuple (time_step, delay_time) as integers.

        Raises:
            InvalidParameterError: If values are not numeric.
            ValidationError: If values are outside allowed ranges.
        """
        if not isinstance(time_step, (int, float)):
            raise InvalidParameterError("time_step", "Value must be a number")
        if not isinstance(delay_time, (int, float)):
            raise InvalidParameterError("delay_time", "Value must be a number")

        time_step_int = int(time_step)
        delay_time_int = int(delay_time)

        if time_step_int < TIME_STEP_MIN_US or time_step_int > TIME_STEP_MAX_US:
            raise ValidationError(
                f"time step {time_step_int} µs is out of range "
                f"[{TIME_STEP_MIN_US} - {TIME_STEP_MAX_US}] µs"
            )
        if delay_time_int < DELAY_TIME_MIN_MS or delay_time_int > DELAY_TIME_MAX_MS:
            raise ValidationError(
                f"delay time {delay_time_int} ms is out of range "
                f"[{DELAY_TIME_MIN_MS} - {DELAY_TIME_MAX_MS}] ms"
            )
        return time_step_int, delay_time_int

    @staticmethod
    def validate_variation_params(
        params: Dict[str, Any],
        variation_type: Any
    ) -> Dict[str, Any]:
        """
        Validate parameters for variation mode.

        Args:
            params: Dictionary with keys:
                min_value, max_value, step, time_step, delay_time.
            variation_type: A VariationType enum value.

        Returns:
            Dictionary with validated and type-coerced values.

        Raises:
            ValidationError: For any constraint violation.
            InvalidParameterError: For wrong types.
        """
        # Validate variation type
        if not isinstance(variation_type, VariationType):
            try:
                variation_type = VariationType(variation_type)
            except (ValueError, KeyError):
                raise ValidationError(
                    f"Invalid variation type '{variation_type}'. "
                    f"Must be one of {[e.name for e in VariationType]}"
                )

        # Check required keys
        required_keys = {'min_value', 'max_value', 'step', 'time_step', 'delay_time'}
        missing = required_keys - params.keys()
        if missing:
            raise ValidationError(
                f"Missing required parameters: {sorted(missing)}"
            )

        # Validate min/max
        min_val = ParameterValidator._check_numeric(params['min_value'], 'min_value')
        max_val = ParameterValidator._check_numeric(params['max_value'], 'max_value')

        if min_val >= max_val:
            raise ValidationError(
                f"min_value ({min_val}) must be less than max_value ({max_val})"
            )

        # Validate step based on variation type
        step = ParameterValidator._check_numeric(params['step'], 'step')

        is_current_variation = variation_type in (
            VariationType.CHANGE_CURRENT_LD1,
            VariationType.CHANGE_CURRENT_LD2
        )
        is_temp_variation = variation_type in (
            VariationType.CHANGE_TEMPERATURE_LD1,
            VariationType.CHANGE_TEMPERATURE_LD2
        )

        if is_current_variation:
            step_min, step_max = CURRENT_STEP_MIN_MA, CURRENT_STEP_MAX_MA
            unit = "mA"
            # Also validate range against current limits
            ParameterValidator.validate_current(min_val, 'min_value')
            ParameterValidator.validate_current(max_val, 'max_value')
        elif is_temp_variation:
            step_min, step_max = TEMP_STEP_MIN_C, TEMP_STEP_MAX_C
            unit = "°C"
            # Also validate range against temperature limits
            ParameterValidator.validate_temperature(min_val, 'min_value')
            ParameterValidator.validate_temperature(max_val, 'max_value')
        else:
            raise ValidationError(
                f"Variation type {variation_type.name} cannot be used in variation mode"
            )

        if step <= 0:
            raise ValidationError(
                f"step must be positive, got {step} {unit}"
            )
        if step < step_min:
            raise ValidationError(
                f"step {step} {unit} is too small (minimum {step_min} {unit})"
            )
        if step > step_max:
            raise ValidationError(
                f"step {step} {unit} is too large (maximum {step_max} {unit})"
            )

        # Validate time parameters
        time_step, delay_time = ParameterValidator.validate_time_params(
            params['time_step'], params['delay_time']
        )

        return {
            'variation_type': variation_type,
            'min_value': min_val,
            'max_value': max_val,
            'step': step,
            'time_step': time_step,
            'delay_time': delay_time,
        }

    @staticmethod
    def validate_manual_mode_params(
        temp1: Any,
        temp2: Any,
        current1: Any,
        current2: Any,
    ) -> Dict[str, float]:
        """
        Validate all four manual mode parameters.

        Args:
            temp1: Laser 1 temperature, °C.
            temp2: Laser 2 temperature, °C.
            current1: Laser 1 current, mA.
            current2: Laser 2 current, mA.

        Returns:
            Dict with validated floats: temp1, temp2, current1, current2.

        Raises:
            ValidationError: For any out-of-range value.
            InvalidParameterError: For wrong types.
        """
        return {
            'temp1':    ParameterValidator.validate_temperature(temp1, 'temp1'),
            'temp2':    ParameterValidator.validate_temperature(temp2, 'temp2'),
            'current1': ParameterValidator.validate_current(current1, 'current1'),
            'current2': ParameterValidator.validate_current(current2, 'current2'),
        }