//! Bounded control variables for inverse control.
//!
//! This module provides types for control variables with physical bounds:
//! - [`BoundedVariable`]: A control variable constrained to [min, max] range
//! - [`BoundedVariableId`]: Type-safe bounded variable identifier
//! - [`BoundedVariableError`]: Errors during bounded variable operations
//! - [`SaturationInfo`]: Information about variable saturation at bounds
//!
//! # Mathematical Foundation
//!
//! Box constraints ensure Newton steps stay physically possible:
//!
//! $$x_{new} = \text{clip}(x_{current} + \Delta x, x_{min}, x_{max})$$
//!
//! where clipping limits the step to stay within bounds.
//!
//! # Example
//!
//! ```rust,ignore
//! use entropyk_solver::inverse::{BoundedVariable, BoundedVariableId};
//!
//! // Valve position: 0.0 (closed) to 1.0 (fully open)
//! let valve = BoundedVariable::new(
//!     BoundedVariableId::new("expansion_valve"),
//!     0.5,   // initial position: 50% open
//!     0.0,   // min: fully closed
//!     1.0,   // max: fully open
//! ).unwrap();
//!
//! // Step clipping keeps value in bounds
//! let clipped = valve.clip_step(0.8);  // tries to move to 1.3
//! assert_eq!(clipped, 1.0);  // clipped to max
//! ```

use std::fmt;
use thiserror::Error;

// ─────────────────────────────────────────────────────────────────────────────
// BoundedVariableId - Type-safe identifier
// ─────────────────────────────────────────────────────────────────────────────

/// Type-safe identifier for a bounded control variable.
///
/// Uses a string internally but provides type safety and clear intent.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BoundedVariableId(String);

impl BoundedVariableId {
    /// Creates a new bounded variable identifier.
    ///
    /// # Arguments
    ///
    /// * `id` - Unique identifier string for the bounded variable
    ///
    /// # Example
    ///
    /// ```rust
    /// use entropyk_solver::inverse::BoundedVariableId;
    ///
    /// let id = BoundedVariableId::new("valve_position");
    /// ```
    pub fn new(id: impl Into<String>) -> Self {
        BoundedVariableId(id.into())
    }

    /// Returns the identifier as a string slice.
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl fmt::Display for BoundedVariableId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl From<&str> for BoundedVariableId {
    fn from(s: &str) -> Self {
        BoundedVariableId::new(s)
    }
}

impl From<String> for BoundedVariableId {
    fn from(s: String) -> Self {
        BoundedVariableId(s)
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// BoundedVariableError - Error handling
// ─────────────────────────────────────────────────────────────────────────────

/// Errors that can occur during bounded variable operations.
#[derive(Error, Debug, Clone, PartialEq)]
pub enum BoundedVariableError {
    /// The bounds are invalid (min >= max).
    #[error("Invalid bounds: min ({min}) must be less than max ({max})")]
    InvalidBounds {
        /// The minimum bound
        min: f64,
        /// The maximum bound
        max: f64,
    },

    /// A bounded variable with this ID already exists.
    #[error("Duplicate bounded variable id: '{id}'")]
    DuplicateId {
        /// The duplicate identifier
        id: BoundedVariableId,
    },

    /// The initial value is outside the bounds.
    #[error("Initial value {value} is outside bounds [{min}, {max}]")]
    ValueOutOfBounds {
        /// The out-of-bounds value
        value: f64,
        /// The minimum bound
        min: f64,
        /// The maximum bound
        max: f64,
    },

    /// The referenced component does not exist in the system.
    #[error("Invalid component reference: '{component_id}' not found")]
    InvalidComponent {
        /// The invalid component identifier
        component_id: String,
    },

    /// Invalid configuration.
    #[error("Invalid bounded variable configuration: {reason}")]
    InvalidConfiguration {
        /// Reason for the validation failure
        reason: String,
    },
}

// ─────────────────────────────────────────────────────────────────────────────
// SaturationType and SaturationInfo - Saturation detection
// ─────────────────────────────────────────────────────────────────────────────

/// Type of bound saturation.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SaturationType {
    /// Variable is at the lower bound (value == min)
    LowerBound,
    /// Variable is at the upper bound (value == max)
    UpperBound,
}

impl fmt::Display for SaturationType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            SaturationType::LowerBound => write!(f, "lower bound"),
            SaturationType::UpperBound => write!(f, "upper bound"),
        }
    }
}

/// Information about a saturated bounded variable.
///
/// When a control variable reaches a bound during solving, this struct
/// captures the details for diagnostic purposes.
#[derive(Debug, Clone, PartialEq)]
pub struct SaturationInfo {
    /// The saturated variable's identifier
    pub variable_id: BoundedVariableId,
    /// Which bound is active
    pub saturation_type: SaturationType,
    /// The bound value (min or max)
    pub bound_value: f64,
    /// The constraint target that couldn't be achieved (if applicable)
    pub constraint_target: Option<f64>,
}

impl SaturationInfo {
    /// Creates a new SaturationInfo.
    pub fn new(
        variable_id: BoundedVariableId,
        saturation_type: SaturationType,
        bound_value: f64,
    ) -> Self {
        SaturationInfo {
            variable_id,
            saturation_type,
            bound_value,
            constraint_target: None,
        }
    }

    /// Adds constraint target information.
    pub fn with_constraint_target(mut self, target: f64) -> Self {
        self.constraint_target = Some(target);
        self
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// BoundedVariable - Core bounded variable type
// ─────────────────────────────────────────────────────────────────────────────

/// A control variable with physical bounds for inverse control.
///
/// Bounded variables ensure that Newton-Raphson steps stay within
/// physically meaningful ranges (e.g., valve position 0.0 to 1.0).
///
/// # Bounds Validation
///
/// Bounds must satisfy `min < max`. The initial value must be within
/// bounds: `min <= value <= max`.
///
/// # Step Clipping
///
/// When applying a Newton step Δx, the new value is clipped:
///
/// $$x_{new} = \text{clamp}(x + \Delta x, x_{min}, x_{max})$$
///
/// # Saturation Detection
///
/// After convergence, check if the variable is saturated (at a bound)
/// using [`is_saturated`](Self::is_saturated).
///
/// # Example
///
/// ```rust,ignore
/// // VFD speed: 30% to 100% (minimum speed for compressor)
/// let vfd = BoundedVariable::new(
///     BoundedVariableId::new("compressor_vfd"),
///     0.8,   // initial: 80% speed
///     0.3,   // min: 30%
///     1.0,   // max: 100%
/// )?;
///
/// // Apply Newton step
/// let new_value = vfd.clip_step(0.3);  // tries to go to 1.1
/// assert_eq!(new_value, 1.0);  // clipped to max
/// ```
#[derive(Debug, Clone, PartialEq)]
pub struct BoundedVariable {
    /// Unique identifier for this bounded variable
    id: BoundedVariableId,
    /// Current value of the variable
    value: f64,
    /// Lower bound (inclusive)
    min: f64,
    /// Upper bound (inclusive)
    max: f64,
    /// Original initial value (before solver modification)
    initial_value: f64,
    /// Optional component this variable controls
    component_id: Option<String>,
}

impl BoundedVariable {
    /// Tolerance for saturation detection (relative to bound range).
    const SATURATION_TOL: f64 = 1e-9;

    /// Creates a new bounded variable.
    ///
    /// # Arguments
    ///
    /// * `id` - Unique identifier for this variable
    /// * `value` - Initial value
    /// * `min` - Lower bound (inclusive)
    /// * `max` - Upper bound (inclusive)
    ///
    /// # Errors
    ///
    /// Returns `BoundedVariableError::InvalidBounds` if `min >= max`.
    /// Returns `BoundedVariableError::ValueOutOfBounds` if `value` is outside bounds.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// let valve = BoundedVariable::new(
    ///     BoundedVariableId::new("valve"),
    ///     0.5, 0.0, 1.0
    /// )?;
    /// ```
    pub fn new(
        id: BoundedVariableId,
        value: f64,
        min: f64,
        max: f64,
    ) -> Result<Self, BoundedVariableError> {
        if min >= max {
            return Err(BoundedVariableError::InvalidBounds { min, max });
        }

        if value < min || value > max {
            return Err(BoundedVariableError::ValueOutOfBounds { value, min, max });
        }

        Ok(BoundedVariable {
            id,
            value,
            min,
            max,
            initial_value: value,
            component_id: None,
        })
    }

    /// Creates a new bounded variable associated with a component.
    ///
    /// # Arguments
    ///
    /// * `id` - Unique identifier for this variable
    /// * `component_id` - Component this variable controls
    /// * `value` - Initial value
    /// * `min` - Lower bound (inclusive)
    /// * `max` - Upper bound (inclusive)
    pub fn with_component(
        id: BoundedVariableId,
        component_id: impl Into<String>,
        value: f64,
        min: f64,
        max: f64,
    ) -> Result<Self, BoundedVariableError> {
        let mut var = Self::new(id, value, min, max)?;
        var.component_id = Some(component_id.into());
        Ok(var)
    }

    /// Returns the variable identifier.
    pub fn id(&self) -> &BoundedVariableId {
        &self.id
    }

    /// Returns the current value.
    pub fn value(&self) -> f64 {
        self.value
    }

    /// Returns the original initial value (before solver modification).
    pub fn initial_value(&self) -> f64 {
        self.initial_value
    }

    /// Returns the lower bound.
    pub fn min(&self) -> f64 {
        self.min
    }

    /// Returns the upper bound.
    pub fn max(&self) -> f64 {
        self.max
    }

    /// Returns the component ID if associated with a component.
    pub fn component_id(&self) -> Option<&str> {
        self.component_id.as_deref()
    }

    /// Sets the current value.
    ///
    /// # Errors
    ///
    /// Returns `BoundedVariableError::ValueOutOfBounds` if the value
    /// is outside the bounds.
    pub fn set_value(&mut self, value: f64) -> Result<(), BoundedVariableError> {
        let range = self.max - self.min;
        let tol = range * Self::SATURATION_TOL;

        if value < self.min - tol || value > self.max + tol {
            return Err(BoundedVariableError::ValueOutOfBounds {
                value,
                min: self.min,
                max: self.max,
            });
        }
        self.value = value.clamp(self.min, self.max);
        Ok(())
    }

    /// Clips a proposed step to stay within bounds.
    ///
    /// Given a delta from the current position, returns the clipped value
    /// that stays within [min, max].
    ///
    /// # Arguments
    ///
    /// * `delta` - Proposed change from current value
    ///
    /// # Returns
    ///
    /// The clipped value: `clamp(current + delta, min, max)`
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// let var = BoundedVariable::new(id, 0.5, 0.0, 1.0)?;
    ///
    /// // Step that would exceed max
    /// assert_eq!(var.clip_step(0.6), 1.0);
    ///
    /// // Step that would exceed min
    /// assert_eq!(var.clip_step(-0.7), 0.0);
    ///
    /// // Step within bounds
    /// assert_eq!(var.clip_step(0.3), 0.8);
    /// ```
    pub fn clip_step(&self, delta: f64) -> f64 {
        clip_step(self.value, delta, self.min, self.max)
    }

    /// Applies a clipped step and updates the internal value.
    ///
    /// # Arguments
    ///
    /// * `delta` - Proposed change from current value
    ///
    /// # Returns
    ///
    /// The new (clipped) value.
    pub fn apply_step(&mut self, delta: f64) -> f64 {
        self.value = self.clip_step(delta);
        self.value
    }

    /// Checks if the variable is at a bound (saturated).
    ///
    /// # Returns
    ///
    /// `Some(SaturationInfo)` if at a bound, `None` if not saturated.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// let var = BoundedVariable::new(id, 1.0, 0.0, 1.0)?;
    /// let sat = var.is_saturated();
    /// assert!(sat.is_some());
    /// assert_eq!(sat.unwrap().saturation_type, SaturationType::UpperBound);
    /// ```
    pub fn is_saturated(&self) -> Option<SaturationInfo> {
        let range = self.max - self.min;
        let tol = range * Self::SATURATION_TOL;

        if (self.value - self.min).abs() <= tol {
            Some(SaturationInfo::new(
                self.id.clone(),
                SaturationType::LowerBound,
                self.min,
            ))
        } else if (self.value - self.max).abs() <= tol {
            Some(SaturationInfo::new(
                self.id.clone(),
                SaturationType::UpperBound,
                self.max,
            ))
        } else {
            None
        }
    }

    /// Checks if a value is within the bounds.
    pub fn is_within_bounds(&self, value: f64) -> bool {
        value >= self.min && value <= self.max
    }

    /// Returns the distance to the nearest bound.
    ///
    /// Returns 0.0 if at a bound, positive otherwise.
    pub fn distance_to_bound(&self) -> f64 {
        let dist_to_min = self.value - self.min;
        let dist_to_max = self.max - self.value;
        dist_to_min.min(dist_to_max)
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Step Clipping Function
// ─────────────────────────────────────────────────────────────────────────────

/// Clips a step to stay within bounds.
///
/// This is a standalone function for use in solver loops where you have
/// raw values rather than a `BoundedVariable` struct.
///
/// # Arguments
///
/// * `current` - Current value
/// * `delta` - Proposed change
/// * `min` - Lower bound
/// * `max` - Upper bound
///
/// # Returns
///
/// The clipped value: `clamp(current + delta, min, max)`
///
/// # Edge Cases
///
/// - NaN delta returns the clamped current value (NaN propagates to proposed, then clamped)
/// - Infinite delta clips to the appropriate bound
/// - If min == max, returns min (degenerate case)
///
/// # Example
///
/// ```rust
/// use entropyk_solver::inverse::clip_step;
///
/// // Normal clipping
/// assert_eq!(clip_step(0.5, 0.6, 0.0, 1.0), 1.0);
/// assert_eq!(clip_step(0.5, -0.7, 0.0, 1.0), 0.0);
///
/// // Within bounds
/// assert_eq!(clip_step(0.5, 0.3, 0.0, 1.0), 0.8);
/// ```
pub fn clip_step(current: f64, delta: f64, min: f64, max: f64) -> f64 {
    let proposed = current + delta;

    // Handle NaN: if proposed is NaN, clamp will return min
    // This is intentional behavior - NaN steps should not corrupt state
    if proposed.is_nan() {
        // Return current if it's valid, otherwise min
        return if current.is_nan() {
            min
        } else {
            current.clamp(min, max)
        };
    }

    proposed.clamp(min, max)
}

// ─────────────────────────────────────────────────────────────────────────────
// Tests
// ─────────────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_bounded_variable_id_creation() {
        let id = BoundedVariableId::new("valve_position");
        assert_eq!(id.as_str(), "valve_position");
        assert_eq!(id.to_string(), "valve_position");
    }

    #[test]
    fn test_bounded_variable_id_from_impls() {
        let id1 = BoundedVariableId::from("valve");
        assert_eq!(id1.as_str(), "valve");

        let id2 = BoundedVariableId::from("valve".to_string());
        assert_eq!(id2.as_str(), "valve");
    }

    #[test]
    fn test_bounded_variable_creation() {
        let var = BoundedVariable::new(BoundedVariableId::new("valve"), 0.5, 0.0, 1.0).unwrap();

        assert_eq!(var.value(), 0.5);
        assert_eq!(var.min(), 0.0);
        assert_eq!(var.max(), 1.0);
        assert!(var.component_id().is_none());
    }

    #[test]
    fn test_bounded_variable_with_component() {
        let var = BoundedVariable::with_component(
            BoundedVariableId::new("valve"),
            "expansion_valve",
            0.5,
            0.0,
            1.0,
        )
        .unwrap();

        assert_eq!(var.component_id(), Some("expansion_valve"));
    }

    #[test]
    fn test_invalid_bounds_min_eq_max() {
        let result = BoundedVariable::new(BoundedVariableId::new("valve"), 0.5, 1.0, 1.0);

        assert!(matches!(
            result,
            Err(BoundedVariableError::InvalidBounds { min: 1.0, max: 1.0 })
        ));
    }

    #[test]
    fn test_invalid_bounds_min_gt_max() {
        let result = BoundedVariable::new(BoundedVariableId::new("valve"), 0.5, 1.0, 0.0);

        assert!(matches!(
            result,
            Err(BoundedVariableError::InvalidBounds { min: 1.0, max: 0.0 })
        ));
    }

    #[test]
    fn test_value_out_of_bounds_below() {
        let result = BoundedVariable::new(BoundedVariableId::new("valve"), -0.1, 0.0, 1.0);

        assert!(matches!(
            result,
            Err(BoundedVariableError::ValueOutOfBounds {
                value: -0.1,
                min: 0.0,
                max: 1.0
            })
        ));
    }

    #[test]
    fn test_value_out_of_bounds_above() {
        let result = BoundedVariable::new(BoundedVariableId::new("valve"), 1.1, 0.0, 1.0);

        assert!(matches!(
            result,
            Err(BoundedVariableError::ValueOutOfBounds {
                value: 1.1,
                min: 0.0,
                max: 1.0
            })
        ));
    }

    #[test]
    fn test_clip_step_within_bounds() {
        let var = BoundedVariable::new(BoundedVariableId::new("valve"), 0.5, 0.0, 1.0).unwrap();

        assert_eq!(var.clip_step(0.3), 0.8);
        assert_eq!(var.clip_step(-0.3), 0.2);
    }

    #[test]
    fn test_clip_step_exceeds_max() {
        let var = BoundedVariable::new(BoundedVariableId::new("valve"), 0.5, 0.0, 1.0).unwrap();

        assert_eq!(var.clip_step(0.6), 1.0);
        assert_eq!(var.clip_step(1.0), 1.0);
    }

    #[test]
    fn test_clip_step_exceeds_min() {
        let var = BoundedVariable::new(BoundedVariableId::new("valve"), 0.5, 0.0, 1.0).unwrap();

        assert_eq!(var.clip_step(-0.6), 0.0);
        assert_eq!(var.clip_step(-1.0), 0.0);
    }

    #[test]
    fn test_clip_step_standalone() {
        // Normal cases
        approx::assert_relative_eq!(clip_step(0.5, 0.6, 0.0, 1.0), 1.0);
        approx::assert_relative_eq!(clip_step(0.5, -0.7, 0.0, 1.0), 0.0);
        approx::assert_relative_eq!(clip_step(0.5, 0.3, 0.0, 1.0), 0.8);

        // At bounds
        approx::assert_relative_eq!(clip_step(0.0, -0.1, 0.0, 1.0), 0.0);
        approx::assert_relative_eq!(clip_step(1.0, 0.1, 0.0, 1.0), 1.0);
    }

    #[test]
    fn test_clip_step_nan_handling() {
        // NaN delta should not propagate; return clamped current
        let result = clip_step(0.5, f64::NAN, 0.0, 1.0);
        approx::assert_relative_eq!(result, 0.5);
    }

    #[test]
    fn test_clip_step_infinity() {
        // Positive infinity clips to max
        approx::assert_relative_eq!(clip_step(0.5, f64::INFINITY, 0.0, 1.0), 1.0);

        // Negative infinity clips to min
        approx::assert_relative_eq!(clip_step(0.5, f64::NEG_INFINITY, 0.0, 1.0), 0.0);
    }

    #[test]
    fn test_saturation_at_min() {
        let var = BoundedVariable::new(BoundedVariableId::new("valve"), 0.0, 0.0, 1.0).unwrap();

        let sat = var.is_saturated();
        assert!(sat.is_some());
        let info = sat.unwrap();
        assert_eq!(info.saturation_type, SaturationType::LowerBound);
        approx::assert_relative_eq!(info.bound_value, 0.0);
    }

    #[test]
    fn test_saturation_at_max() {
        let var = BoundedVariable::new(BoundedVariableId::new("valve"), 1.0, 0.0, 1.0).unwrap();

        let sat = var.is_saturated();
        assert!(sat.is_some());
        let info = sat.unwrap();
        assert_eq!(info.saturation_type, SaturationType::UpperBound);
        approx::assert_relative_eq!(info.bound_value, 1.0);
    }

    #[test]
    fn test_no_saturation_in_middle() {
        let var = BoundedVariable::new(BoundedVariableId::new("valve"), 0.5, 0.0, 1.0).unwrap();

        assert!(var.is_saturated().is_none());
    }

    #[test]
    fn test_saturation_info_with_constraint_target() {
        let info = SaturationInfo::new(
            BoundedVariableId::new("valve"),
            SaturationType::UpperBound,
            1.0,
        )
        .with_constraint_target(1.5);

        assert_eq!(info.constraint_target, Some(1.5));
    }

    #[test]
    fn test_set_value_valid() {
        let mut var = BoundedVariable::new(BoundedVariableId::new("valve"), 0.5, 0.0, 1.0).unwrap();

        var.set_value(0.7).unwrap();
        approx::assert_relative_eq!(var.value(), 0.7);

        var.set_value(0.0).unwrap();
        approx::assert_relative_eq!(var.value(), 0.0);

        var.set_value(1.0).unwrap();
        approx::assert_relative_eq!(var.value(), 1.0);
    }

    #[test]
    fn test_set_value_invalid() {
        let mut var = BoundedVariable::new(BoundedVariableId::new("valve"), 0.5, 0.0, 1.0).unwrap();

        assert!(var.set_value(-0.1).is_err());
        assert!(var.set_value(1.1).is_err());

        // Value should remain unchanged
        approx::assert_relative_eq!(var.value(), 0.5);
    }

    #[test]
    fn test_apply_step() {
        let mut var = BoundedVariable::new(BoundedVariableId::new("valve"), 0.5, 0.0, 1.0).unwrap();

        let new_val = var.apply_step(0.6);
        approx::assert_relative_eq!(new_val, 1.0);
        approx::assert_relative_eq!(var.value(), 1.0);

        let new_val = var.apply_step(-0.3);
        approx::assert_relative_eq!(new_val, 0.7);
        approx::assert_relative_eq!(var.value(), 0.7);
    }

    #[test]
    fn test_is_within_bounds() {
        let var = BoundedVariable::new(BoundedVariableId::new("valve"), 0.5, 0.0, 1.0).unwrap();

        assert!(var.is_within_bounds(0.0));
        assert!(var.is_within_bounds(0.5));
        assert!(var.is_within_bounds(1.0));
        assert!(!var.is_within_bounds(-0.1));
        assert!(!var.is_within_bounds(1.1));
    }

    #[test]
    fn test_distance_to_bound() {
        let var = BoundedVariable::new(BoundedVariableId::new("valve"), 0.5, 0.0, 1.0).unwrap();

        approx::assert_relative_eq!(var.distance_to_bound(), 0.5);

        let var_at_min =
            BoundedVariable::new(BoundedVariableId::new("valve"), 0.0, 0.0, 1.0).unwrap();
        approx::assert_relative_eq!(var_at_min.distance_to_bound(), 0.0);

        let var_at_max =
            BoundedVariable::new(BoundedVariableId::new("valve"), 1.0, 0.0, 1.0).unwrap();
        approx::assert_relative_eq!(var_at_max.distance_to_bound(), 0.0);
    }

    #[test]
    fn test_saturation_type_display() {
        assert_eq!(format!("{}", SaturationType::LowerBound), "lower bound");
        assert_eq!(format!("{}", SaturationType::UpperBound), "upper bound");
    }

    #[test]
    fn test_error_display() {
        let err = BoundedVariableError::InvalidBounds { min: 0.0, max: 0.0 };
        assert!(err.to_string().contains("0"));

        let err = BoundedVariableError::DuplicateId {
            id: BoundedVariableId::new("dup"),
        };
        assert!(err.to_string().contains("dup"));

        let err = BoundedVariableError::InvalidComponent {
            component_id: "unknown".to_string(),
        };
        assert!(err.to_string().contains("unknown"));
    }
}
