//! Constraint types for inverse control.
//!
//! This module defines the core types for constraint-based control:
//! - [`Constraint`]: A single output constraint (output - target = 0)
//! - [`ConstraintId`]: Type-safe constraint identifier
//! - [`ConstraintError`]: Errors during constraint operations
//! - [`ComponentOutput`]: Reference to a measurable component property

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

// ─────────────────────────────────────────────────────────────────────────────
// ConstraintId - Type-safe identifier
// ─────────────────────────────────────────────────────────────────────────────

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

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

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

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

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

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

// ─────────────────────────────────────────────────────────────────────────────
// ComponentOutput - Reference to measurable property
// ─────────────────────────────────────────────────────────────────────────────

/// Reference to a measurable component output property.
///
/// This enum defines which component properties can be used as constraint targets.
/// Each variant captures the necessary context to compute the property value.
#[derive(Debug, Clone, PartialEq)]
pub enum ComponentOutput {
    /// Saturation temperature at a component (K).
    ///
    /// References the saturation temperature at the component's location
    /// in the thermodynamic cycle.
    SaturationTemperature {
        /// Component identifier (e.g., "evaporator", "condenser")
        component_id: String,
    },

    /// Superheat above saturation (K).
    ///
    /// Superheat = T_actual - T_sat
    Superheat {
        /// Component identifier
        component_id: String,
    },

    /// Subcooling below saturation (K).
    ///
    /// Subcooling = T_sat - T_actual
    Subcooling {
        /// Component identifier
        component_id: String,
    },

    /// Heat transfer rate (W).
    ///
    /// Heat transfer at a heat exchanger component.
    HeatTransferRate {
        /// Component identifier
        component_id: String,
    },

    /// Capacity (W).
    ///
    /// Cooling or heating capacity of a component.
    Capacity {
        /// Component identifier
        component_id: String,
    },

    /// Mass flow rate (kg/s).
    ///
    /// Mass flow through a component.
    MassFlowRate {
        /// Component identifier
        component_id: String,
    },

    /// Pressure at a component (Pa).
    Pressure {
        /// Component identifier
        component_id: String,
    },

    /// Temperature at a component (K).
    Temperature {
        /// Component identifier
        component_id: String,
    },
}

impl ComponentOutput {
    /// Returns the component ID for this output reference.
    pub fn component_id(&self) -> &str {
        match self {
            ComponentOutput::SaturationTemperature { component_id } => component_id,
            ComponentOutput::Superheat { component_id } => component_id,
            ComponentOutput::Subcooling { component_id } => component_id,
            ComponentOutput::HeatTransferRate { component_id } => component_id,
            ComponentOutput::Capacity { component_id } => component_id,
            ComponentOutput::MassFlowRate { component_id } => component_id,
            ComponentOutput::Pressure { component_id } => component_id,
            ComponentOutput::Temperature { component_id } => component_id,
        }
    }

    /// Returns a stable string identifier for this output type.
    pub fn constraint_type_name(&self) -> &'static str {
        match self {
            ComponentOutput::SaturationTemperature { .. } => "saturationTemperature",
            ComponentOutput::Superheat { .. } => "superheat",
            ComponentOutput::Subcooling { .. } => "subcooling",
            ComponentOutput::HeatTransferRate { .. } => "heatTransferRate",
            ComponentOutput::Capacity { .. } => "capacity",
            ComponentOutput::MassFlowRate { .. } => "massFlowRate",
            ComponentOutput::Pressure { .. } => "pressure",
            ComponentOutput::Temperature { .. } => "temperature",
        }
    }

    /// Creates a Superheat output for the given component.
    pub fn superheat_for(component_id: &str) -> Self {
        ComponentOutput::Superheat {
            component_id: component_id.to_string(),
        }
    }

    /// Creates a Subcooling output for the given component.
    pub fn subcooling_for(component_id: &str) -> Self {
        ComponentOutput::Subcooling {
            component_id: component_id.to_string(),
        }
    }

    /// Creates a Capacity output for the given component.
    pub fn capacity_for(component_id: &str) -> Self {
        ComponentOutput::Capacity {
            component_id: component_id.to_string(),
        }
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// ConstraintError - Error handling
// ─────────────────────────────────────────────────────────────────────────────

/// Errors that can occur during constraint operations.
#[derive(Error, Debug, Clone, PartialEq)]
pub enum ConstraintError {
    /// The referenced component does not exist in the system.
    #[error("Invalid component reference: '{component_id}' not found")]
    InvalidReference {
        /// The invalid component identifier
        component_id: String,
    },

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

    /// The constraint value is outside valid bounds.
    #[error("Constraint value {value} is out of bounds: {reason}")]
    OutOfBounds {
        /// The out-of-bounds value
        value: f64,
        /// Reason for the bounds violation
        reason: String,
    },

    /// The component output is not available or computable.
    #[error("Component output not available: {output:?} for component '{component_id}'")]
    OutputNotAvailable {
        /// Component identifier
        component_id: String,
        /// The unavailable output type
        output: String,
    },

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

    /// A constraint has no measured value — the referenced component is not registered
    /// or has no associated edges.
    #[error("No measured value for constraint '{constraint_id}': component '{component_id}' may not be registered or has no associated edges")]
    UnmeasuredConstraint {
        /// The constraint identifier
        constraint_id: String,
        /// The component identifier referenced by the constraint
        component_id: String,
    },

    /// The residual slice provided is too short for the number of constraints.
    #[error("Residual slice too short: index {index}, length {len}, need at least {required}")]
    ResidualSliceTooShort {
        /// The index that would have been accessed
        index: usize,
        /// The actual slice length
        len: usize,
        /// The minimum required length
        required: usize,
    },

    /// Invalid finite-difference epsilon value.
    #[error("Invalid finite difference epsilon: {value}. Must be finite and in (0, 1]. {reason}")]
    InvalidEpsilon {
        /// The invalid epsilon value
        value: f64,
        /// Reason for the validation failure
        reason: String,
    },
}

// ─────────────────────────────────────────────────────────────────────────────
// Constraint - Core constraint type
// ─────────────────────────────────────────────────────────────────────────────

/// An output constraint for inverse control.
///
/// A constraint specifies a desired relationship:
///
/// $$r = f(x) - y_{target} = 0$$
///
/// where:
/// - $f(x)$ is the measured output (via [`ComponentOutput`])
/// - $y_{target}$ is the target value
/// - $r$ is the constraint residual
///
/// # Tolerance Guidance
///
/// The tolerance determines when a constraint is considered "satisfied". Choose based on:
///
/// | Property | Recommended Tolerance | Notes |
/// |----------|----------------------|-------|
/// | Superheat/Subcooling (K) | 0.01 - 0.1 | Temperature precision typically 0.1K |
/// | Pressure (Pa) | 100 - 1000 | ~0.1-1% of typical operating pressure |
/// | Heat Transfer (W) | 10 - 100 | Depends on system capacity |
/// | Mass Flow (kg/s) | 0.001 - 0.01 | ~1% of typical flow rate |
///
/// Default tolerance is `1e-6`, which may be too tight for some applications.
/// Use [`Constraint::with_tolerance()`] to customize.
///
/// # Example
///
/// ```rust,ignore
/// use entropyk_solver::inverse::{Constraint, ConstraintId, ComponentOutput};
///
/// // Superheat constraint: maintain 5K superheat at evaporator outlet
/// let constraint = Constraint::new(
///     ConstraintId::new("evap_superheat"),
///     ComponentOutput::Superheat {
///         component_id: "evaporator".to_string()
///     },
///     5.0,  // target: 5K superheat
/// );
/// ```
#[derive(Debug, Clone, PartialEq)]
pub struct Constraint {
    /// Unique identifier for this constraint
    id: ConstraintId,

    /// Which component output to constrain
    output: ComponentOutput,

    /// Target value for the constraint
    target_value: f64,

    /// Tolerance for convergence.
    ///
    /// A constraint is satisfied when `|measured - target| < tolerance`.
    /// See struct documentation for guidance on choosing appropriate values.
    tolerance: f64,
}

impl Constraint {
    /// Creates a new constraint with default tolerance.
    ///
    /// # Arguments
    ///
    /// * `id` - Unique identifier for this constraint
    /// * `output` - Component output to constrain
    /// * `target_value` - Desired target value
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// let constraint = Constraint::new(
    ///     ConstraintId::new("superheat_control"),
    ///     ComponentOutput::Superheat {
    ///         component_id: "evaporator".to_string()
    ///     },
    ///     5.0,
    /// );
    /// ```
    pub fn new(id: ConstraintId, output: ComponentOutput, target_value: f64) -> Self {
        Constraint {
            id,
            output,
            target_value,
            tolerance: 1e-6,
        }
    }

    /// Creates a new constraint with custom tolerance.
    ///
    /// # Arguments
    ///
    /// * `id` - Unique identifier for this constraint
    /// * `output` - Component output to constrain
    /// * `target_value` - Desired target value
    /// * `tolerance` - Convergence tolerance
    pub fn with_tolerance(
        id: ConstraintId,
        output: ComponentOutput,
        target_value: f64,
        tolerance: f64,
    ) -> Result<Self, ConstraintError> {
        if tolerance <= 0.0 {
            return Err(ConstraintError::InvalidConfiguration {
                reason: format!("Tolerance must be positive, got {}", tolerance),
            });
        }

        Ok(Constraint {
            id,
            output,
            target_value,
            tolerance,
        })
    }

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

    /// Returns the component output reference.
    pub fn output(&self) -> &ComponentOutput {
        &self.output
    }

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

    /// Returns the convergence tolerance.
    pub fn tolerance(&self) -> f64 {
        self.tolerance
    }

    /// Computes the constraint residual.
    ///
    /// The residual is:
    ///
    /// $$r = f(x) - y_{target}$$
    ///
    /// where $f(x)$ is the measured output value.
    ///
    /// # Arguments
    ///
    /// * `measured_value` - The current value of the constrained output
    ///
    /// # Returns
    ///
    /// The constraint residual (measured - target)
    pub fn compute_residual(&self, measured_value: f64) -> f64 {
        measured_value - self.target_value
    }

    /// Checks if the constraint is satisfied.
    ///
    /// # Arguments
    ///
    /// * `measured_value` - The current value of the constrained output
    ///
    /// # Returns
    ///
    /// `true` if |residual| < tolerance
    pub fn is_satisfied(&self, measured_value: f64) -> bool {
        let residual = self.compute_residual(measured_value);
        residual.abs() < self.tolerance
    }
}

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

    #[test]
    fn test_constraint_id_creation() {
        let id = ConstraintId::new("test_constraint");
        assert_eq!(id.as_str(), "test_constraint");
        assert_eq!(id.to_string(), "test_constraint");
    }

    #[test]
    fn test_constraint_id_from_string() {
        let id = ConstraintId::from("my_constraint".to_string());
        assert_eq!(id.as_str(), "my_constraint");
    }

    #[test]
    fn test_constraint_id_from_str() {
        let id = ConstraintId::from("constraint_1");
        assert_eq!(id.as_str(), "constraint_1");
    }

    #[test]
    fn test_component_output_component_id() {
        let output = ComponentOutput::Superheat {
            component_id: "evaporator".to_string(),
        };
        assert_eq!(output.component_id(), "evaporator");
    }

    #[test]
    fn test_constraint_creation() {
        let constraint = Constraint::new(
            ConstraintId::new("superheat_control"),
            ComponentOutput::Superheat {
                component_id: "evaporator".to_string(),
            },
            5.0,
        );

        assert_eq!(constraint.target_value(), 5.0);
        assert_eq!(constraint.tolerance(), 1e-6);
    }

    #[test]
    fn test_constraint_with_tolerance() {
        let constraint = Constraint::with_tolerance(
            ConstraintId::new("superheat"),
            ComponentOutput::Superheat {
                component_id: "evaporator".to_string(),
            },
            5.0,
            1e-4,
        )
        .unwrap();

        assert_eq!(constraint.tolerance(), 1e-4);
    }

    #[test]
    fn test_constraint_invalid_tolerance() {
        let result = Constraint::with_tolerance(
            ConstraintId::new("superheat"),
            ComponentOutput::Superheat {
                component_id: "evaporator".to_string(),
            },
            5.0,
            -1e-4,
        );

        assert!(matches!(
            result,
            Err(ConstraintError::InvalidConfiguration { .. })
        ));
    }

    #[test]
    fn test_constraint_compute_residual() {
        let constraint = Constraint::new(
            ConstraintId::new("superheat"),
            ComponentOutput::Superheat {
                component_id: "evaporator".to_string(),
            },
            5.0,
        );

        // Measured = 6.0, target = 5.0, residual = 1.0
        let residual = constraint.compute_residual(6.0);
        approx::assert_relative_eq!(residual, 1.0);

        // Measured = 4.5, target = 5.0, residual = -0.5
        let residual = constraint.compute_residual(4.5);
        approx::assert_relative_eq!(residual, -0.5);
    }

    #[test]
    fn test_constraint_is_satisfied() {
        let constraint = Constraint::new(
            ConstraintId::new("superheat"),
            ComponentOutput::Superheat {
                component_id: "evaporator".to_string(),
            },
            5.0,
        );

        // Exactly at target
        assert!(constraint.is_satisfied(5.0));

        // Within tolerance
        assert!(constraint.is_satisfied(5.0 + 0.5e-6));
        assert!(constraint.is_satisfied(5.0 - 0.5e-6));

        // Outside tolerance
        assert!(!constraint.is_satisfied(5.0 + 2e-6));
        assert!(!constraint.is_satisfied(5.0 - 2e-6));
    }

    #[test]
    fn test_constraint_error_display() {
        let error = ConstraintError::InvalidReference {
            component_id: "unknown".to_string(),
        };
        assert!(error.to_string().contains("unknown"));

        let error = ConstraintError::DuplicateId {
            id: ConstraintId::new("dup"),
        };
        assert!(error.to_string().contains("dup"));
    }
}
