//! Solver-level inverse calibration algorithm (Story 19.1 / P4-25).
//!
//! This module provides a higher-level orchestration layer on top of the existing
//! one-shot calibration infrastructure (Story 5.5). It automates the process of
//! estimating Calib Z-factors (z_flow, z_ua, z_power, etc.) from measured data.
//!
//! # Modes
//!
//! - **Sequential** (default): calibrates one factor at a time in the recommended order
//!   z_flow → z_flow_eco → z_dp → z_ua → z_power → z_etav. More stable for nonlinear interactions.
//! - **Simultaneous**: swaps all factors at once for a single One-Shot solve. Faster
//!   but less robust.
//!
//! # Example
//!
//! ```rust,ignore
//! use entropyk_solver::inverse::calibration::{
//!     CalibrationProblem, CalibrationMode, CalibFactor, CalibRequest, CalibrationTarget,
//! };
//!
//! let problem = CalibrationProblem::new()
//!     .with_mode(CalibrationMode::Sequential)
//!     .add_request(CalibRequest::new(
//!         CalibFactor::ZUa, "evaporator", (0.1, 10.0), 1.0,
//!     ))
//!     .add_target(CalibrationTarget::capacity("evaporator", 4015.0));
//!
//! let result = problem.calibrate(&mut sys, &mut solver)?;
//! println!("f_ua = {}", result.estimated_factor("evaporator.z_ua"));
//! ```

use std::collections::{HashMap, HashSet};
use std::fmt;

use serde::{Deserialize, Serialize};
use thiserror::Error;

use super::{
    BoundedVariable, BoundedVariableId, ComponentOutput, Constraint, ConstraintError, ConstraintId,
};
use crate::solver::{Solver, SolverError};
use crate::strategies::NewtonConfig;
use crate::system::System;

// ─────────────────────────────────────────────────────────────────────────────
// CalibFactor - Which calibration factor to estimate
// ─────────────────────────────────────────────────────────────────────────────

/// Calibration factor to estimate during inverse calibration.
///
/// Each variant maps to a field in [`entropyk_core::Calib`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CalibFactor {
    /// z_flow: mass flow multiplier (BOLT Z_flow_suc; Compressor, Expansion Valve)
    ZFlow,
    /// z_flow_eco: economizer injection mass flow multiplier (BOLT Z_flow_eco)
    ZFlowEco,
    /// z_dp: pressure drop multiplier (BOLT Z_dpc; Pipe, Heat Exchanger)
    ZDp,
    /// z_ua: UA multiplier (BOLT Z_UA; Evaporator, Condenser)
    ZUa,
    /// z_power: power multiplier (BOLT Z_power; Compressor)
    ZPower,
    /// z_etav: volumetric efficiency multiplier (Compressor)
    ZEtav,
}

impl CalibFactor {
    /// Returns the canonical Z-factor name (e.g. `"z_flow"`).
    pub fn as_str(&self) -> &'static str {
        match self {
            CalibFactor::ZFlow => entropyk_core::Z_FLOW,
            CalibFactor::ZFlowEco => entropyk_core::Z_FLOW_ECO,
            CalibFactor::ZDp => entropyk_core::Z_DP,
            CalibFactor::ZUa => entropyk_core::Z_UA,
            CalibFactor::ZPower => entropyk_core::Z_POWER,
            CalibFactor::ZEtav => entropyk_core::Z_ETAV,
        }
    }

    /// Recommended calibration order: z_flow → z_flow_eco → z_dp → z_ua → z_power → z_etav.
    pub fn calibration_order() -> &'static [CalibFactor] {
        &[
            CalibFactor::ZFlow,
            CalibFactor::ZFlowEco,
            CalibFactor::ZDp,
            CalibFactor::ZUa,
            CalibFactor::ZPower,
            CalibFactor::ZEtav,
        ]
    }

    /// Returns the default bounds for this factor type.
    pub fn default_bounds(&self) -> (f64, f64) {
        match self {
            CalibFactor::ZFlow => (0.5, 2.0),
            CalibFactor::ZFlowEco => (0.5, 2.0),
            CalibFactor::ZDp => (0.5, 2.0),
            CalibFactor::ZUa => (0.1, 10.0),
            CalibFactor::ZPower => (0.5, 2.0),
            CalibFactor::ZEtav => (0.5, 2.0),
        }
    }
}

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

// ─────────────────────────────────────────────────────────────────────────────
// CalibRequest - A single factor to estimate
// ─────────────────────────────────────────────────────────────────────────────

/// A request to estimate a specific calibration factor on a specific component.
#[derive(Debug, Clone, PartialEq)]
pub struct CalibRequest {
    /// Which calibration factor to estimate.
    pub factor: CalibFactor,
    /// Name of the component (must be registered in the System).
    pub component_name: String,
    /// Physical bounds for the factor (min, max).
    pub bounds: (f64, f64),
    /// Initial guess (must be within bounds).
    pub initial: f64,
}

impl CalibRequest {
    /// Creates a new calibration request.
    pub fn new(
        factor: CalibFactor,
        component_name: impl Into<String>,
        bounds: (f64, f64),
        initial: f64,
    ) -> Self {
        CalibRequest {
            factor,
            component_name: component_name.into(),
            bounds,
            initial,
        }
    }

    /// Creates a request with default bounds for the factor type.
    pub fn with_default_bounds(
        factor: CalibFactor,
        component_name: impl Into<String>,
        initial: f64,
    ) -> Self {
        CalibRequest {
            factor,
            component_name: component_name.into(),
            bounds: factor.default_bounds(),
            initial,
        }
    }

    /// Returns a unique key for this request: "component_name.factor".
    pub fn key(&self) -> String {
        format!("{}.{}", self.component_name, self.factor.as_str())
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// CalibrationTarget - A measured value to match
// ─────────────────────────────────────────────────────────────────────────────

/// A measured value that the calibration should match.
///
/// Each target pairs a measurable quantity with its measured value.
/// During calibration, a `Constraint` is automatically created from this target.
#[derive(Debug, Clone, PartialEq)]
pub struct CalibrationTarget {
    /// Which measurable quantity.
    pub output: ComponentOutput,
    /// The measured value to match.
    pub measured_value: f64,
}

impl CalibrationTarget {
    /// Creates a mass flow rate target.
    pub fn mass_flow(component_name: impl Into<String>, measured_value: f64) -> Self {
        CalibrationTarget {
            output: ComponentOutput::MassFlowRate {
                component_id: component_name.into(),
            },
            measured_value,
        }
    }

    /// Creates a capacity (cooling/heating) target.
    pub fn capacity(component_name: impl Into<String>, measured_value: f64) -> Self {
        CalibrationTarget {
            output: ComponentOutput::Capacity {
                component_id: component_name.into(),
            },
            measured_value,
        }
    }

    /// Creates a heat transfer rate target.
    pub fn heat_transfer_rate(component_name: impl Into<String>, measured_value: f64) -> Self {
        CalibrationTarget {
            output: ComponentOutput::HeatTransferRate {
                component_id: component_name.into(),
            },
            measured_value,
        }
    }

    /// Creates a saturation temperature target (K).
    pub fn saturation_temperature(component_name: impl Into<String>, measured_value: f64) -> Self {
        CalibrationTarget {
            output: ComponentOutput::SaturationTemperature {
                component_id: component_name.into(),
            },
            measured_value,
        }
    }

    /// Creates a pressure target (Pa).
    pub fn pressure(component_name: impl Into<String>, measured_value: f64) -> Self {
        CalibrationTarget {
            output: ComponentOutput::Pressure {
                component_id: component_name.into(),
            },
            measured_value,
        }
    }

    /// Creates a temperature target (K).
    pub fn temperature(component_name: impl Into<String>, measured_value: f64) -> Self {
        CalibrationTarget {
            output: ComponentOutput::Temperature {
                component_id: component_name.into(),
            },
            measured_value,
        }
    }

    /// Creates a superheat target (K).
    pub fn superheat(component_name: impl Into<String>, measured_value: f64) -> Self {
        CalibrationTarget {
            output: ComponentOutput::Superheat {
                component_id: component_name.into(),
            },
            measured_value,
        }
    }

    /// Creates a subcooling target (K).
    pub fn subcooling(component_name: impl Into<String>, measured_value: f64) -> Self {
        CalibrationTarget {
            output: ComponentOutput::Subcooling {
                component_id: component_name.into(),
            },
            measured_value,
        }
    }

    /// Converts this target into a Constraint with the given ID.
    pub fn to_constraint(&self, id: ConstraintId) -> Constraint {
        Constraint::new(id, self.output.clone(), self.measured_value)
    }

    fn component_id(&self) -> &str {
        self.output.component_id()
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// CalibrationMode - Sequential vs Simultaneous
// ─────────────────────────────────────────────────────────────────────────────

/// Calibration mode selection.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum CalibrationMode {
    /// Calibrate one factor at a time in the recommended order.
    /// More stable for nonlinear interactions.
    #[default]
    Sequential,
    /// Calibrate all factors simultaneously in a single solve.
    /// Faster but less robust.
    Simultaneous,
}

// ─────────────────────────────────────────────────────────────────────────────
// CalibrationResult - Output of a calibration run
// ─────────────────────────────────────────────────────────────────────────────

/// Result of an inverse calibration run.
///
/// Contains the estimated calibration factors, error metrics, and diagnostics.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CalibrationResult {
    /// Estimated Calib values keyed by "component_name.factor".
    pub estimated_factors: HashMap<String, f64>,
    /// Residual per constraint: measured - computed.
    pub residuals: HashMap<String, f64>,
    /// Mean Absolute Percentage Error across all targets (%).
    pub mape: f64,
    /// Maximum absolute error across all targets.
    pub max_abs_error: f64,
    /// Total solver iterations (sum across sequential steps).
    pub iterations: usize,
    /// Whether the calibration converged.
    pub converged: bool,
    /// Factors that hit their bounds during calibration.
    pub saturated_factors: Vec<String>,
}

impl CalibrationResult {
    fn new() -> Self {
        CalibrationResult {
            estimated_factors: HashMap::new(),
            residuals: HashMap::new(),
            mape: 0.0,
            max_abs_error: 0.0,
            iterations: 0,
            converged: false,
            saturated_factors: Vec::new(),
        }
    }

    /// Returns the estimated value for a given factor key.
    pub fn estimated_factor(&self, key: &str) -> Option<f64> {
        self.estimated_factors.get(key).copied()
    }

    /// Computes MAPE from target-residual pairs.
    fn compute_mape(targets: &[(f64, f64)]) -> f64 {
        if targets.is_empty() {
            return 0.0;
        }
        let sum: f64 = targets
            .iter()
            .map(|(measured, residual)| {
                let denom = measured.abs().max(1e-10);
                (residual.abs() / denom) * 100.0
            })
            .sum();
        sum / targets.len() as f64
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// CalibrationError - Errors during calibration
// ─────────────────────────────────────────────────────────────────────────────

/// Errors that can occur during inverse calibration.
#[derive(Error, Debug, Clone)]
pub enum CalibrationError {
    /// Number of targets does not match number of requests.
    #[error(
        "DoF mismatch: {n_targets} targets for {n_requests} calibration requests \
         (must be equal)"
    )]
    DoFMismatch { n_targets: usize, n_requests: usize },

    /// A referenced component does not exist in the system.
    #[error("Component '{component_id}' not registered in the system")]
    ComponentNotFound { component_id: String },

    /// The solver failed to converge during calibration.
    #[error(
        "Calibration failed to converge at factor '{factor}': \
         {iterations} iterations, final residual norm {residual_norm:.2e} \
         ({partial_count} factors estimated before failure)"
    )]
    ConvergenceFailure {
        factor: String,
        iterations: usize,
        residual_norm: f64,
        partial_count: usize,
    },

    /// The solver returned an error during calibration.
    #[error("Solver error during calibration of '{factor}': {source}")]
    SolverError {
        factor: String,
        #[source]
        source: SolverError,
    },

    /// A constraint error occurred during calibration setup.
    #[error("Constraint setup error: {0}")]
    ConstraintError(#[from] ConstraintError),

    /// The system has not been finalized.
    #[error("System must be finalized before calibration")]
    SystemNotFinalized,
}

// ─────────────────────────────────────────────────────────────────────────────
// CalibrationProblem - The main calibration orchestrator
// ─────────────────────────────────────────────────────────────────────────────

/// Defines an inverse calibration problem.
///
/// Specifies which calibration factors to estimate and which measured values
/// to match. Call [`calibrate()`](Self::calibrate) to execute the calibration.
#[derive(Debug, Clone, Default)]
pub struct CalibrationProblem {
    targets: Vec<CalibrationTarget>,
    requests: Vec<CalibRequest>,
    mode: CalibrationMode,
}

impl CalibrationProblem {
    /// Creates a new empty calibration problem.
    pub fn new() -> Self {
        CalibrationProblem::default()
    }

    /// Sets the calibration mode (default: Sequential).
    pub fn with_mode(mut self, mode: CalibrationMode) -> Self {
        self.mode = mode;
        self
    }

    /// Adds a calibration request (factor to estimate).
    pub fn add_request(mut self, request: CalibRequest) -> Self {
        self.requests.push(request);
        self
    }

    /// Adds a calibration target (measured value to match).
    pub fn add_target(mut self, target: CalibrationTarget) -> Self {
        self.targets.push(target);
        self
    }

    /// Returns the calibration mode.
    pub fn mode(&self) -> CalibrationMode {
        self.mode
    }

    /// Returns the number of targets.
    pub fn n_targets(&self) -> usize {
        self.targets.len()
    }

    /// Returns the number of requests.
    pub fn n_requests(&self) -> usize {
        self.requests.len()
    }

    /// Validates the problem before calibration.
    ///
    /// Checks:
    /// - Number of targets equals number of requests (DoF balance)
    /// - All referenced components exist in the system
    pub fn validate(&self, system: &System) -> Result<(), CalibrationError> {
        if self.targets.len() != self.requests.len() {
            return Err(CalibrationError::DoFMismatch {
                n_targets: self.targets.len(),
                n_requests: self.requests.len(),
            });
        }

        for target in &self.targets {
            if system.get_component_node(target.component_id()).is_none() {
                return Err(CalibrationError::ComponentNotFound {
                    component_id: target.component_id().to_string(),
                });
            }
        }

        for request in &self.requests {
            if system.get_component_node(&request.component_name).is_none() {
                return Err(CalibrationError::ComponentNotFound {
                    component_id: request.component_name.clone(),
                });
            }
        }

        // Check for duplicate request keys
        let mut seen_keys = HashSet::new();
        for request in &self.requests {
            let key = request.key();
            if !seen_keys.insert(key.clone()) {
                return Err(CalibrationError::ConstraintError(
                    ConstraintError::InvalidConfiguration {
                        reason: format!("Duplicate calibration request for key '{key}'"),
                    },
                ));
            }
        }

        Ok(())
    }

    /// Executes the calibration algorithm.
    ///
    /// Takes a `System` and a base `NewtonConfig`. For each calibration step
    /// (or the single simultaneous step), the config is cloned and its initial
    /// state is set appropriately. After calibration completes, all constraints,
    /// bounded variables, and inverse control mappings are cleaned up.
    pub fn calibrate(
        &self,
        system: &mut System,
        base_config: &NewtonConfig,
    ) -> Result<CalibrationResult, CalibrationError> {
        self.validate(system)?;

        match self.mode {
            CalibrationMode::Sequential => self.calibrate_sequential(system, base_config),
            CalibrationMode::Simultaneous => self.calibrate_simultaneous(system, base_config),
        }
    }

    fn calibrate_sequential(
        &self,
        system: &mut System,
        base_config: &NewtonConfig,
    ) -> Result<CalibrationResult, CalibrationError> {
        let mut result = CalibrationResult::new();
        let mut all_residual_pairs: Vec<(f64, f64)> = Vec::new();

        // Sort requests by calibration order, keeping paired targets together
        let order = CalibFactor::calibration_order();
        let mut pairs: Vec<(&CalibRequest, &CalibrationTarget)> =
            self.requests.iter().zip(self.targets.iter()).collect();
        pairs.sort_by_key(|(req, _)| {
            order
                .iter()
                .position(|f| *f == req.factor)
                .unwrap_or(usize::MAX)
        });

        for (req, target) in pairs {
            let constraint_id = ConstraintId::new(format!("calib_{}", req.key()));
            let var_id = BoundedVariableId::new(req.key());

            // Set up constraint
            let constraint = target.to_constraint(constraint_id.clone());
            system.add_constraint(constraint)?;

            // Set up bounded variable
            let bv = BoundedVariable::with_component(
                var_id.clone(),
                &req.component_name,
                req.initial,
                req.bounds.0,
                req.bounds.1,
            )
            .map_err(|e| {
                let _ = system.remove_constraint(&constraint_id);
                CalibrationError::ConstraintError(ConstraintError::InvalidConfiguration {
                    reason: format!("Bounded variable error for {}: {e}", req.key()),
                })
            })?;
            system.add_bounded_variable(bv).map_err(|e| {
                let _ = system.remove_constraint(&constraint_id);
                CalibrationError::ConstraintError(ConstraintError::InvalidConfiguration {
                    reason: format!("Failed to add bounded variable: {e}"),
                })
            })?;

            system
                .link_constraint_to_control(&constraint_id, &var_id)
                .map_err(|e| {
                    let _ = system.remove_constraint(&constraint_id);
                    let _ = system.remove_bounded_variable(&var_id);
                    CalibrationError::ConstraintError(ConstraintError::InvalidConfiguration {
                        reason: format!("Link error: {e}"),
                    })
                })?;

            // Re-finalize to update state vector layout
            system
                .finalize()
                .map_err(|_| CalibrationError::SystemNotFinalized)?;

            // Create solver with correct initial state
            let initial_state = vec![0.0; system.full_state_vector_len()];
            let mut solver = base_config.clone().with_initial_state(initial_state);

            let solve_result = solver.solve(system);
            match solve_result {
                Ok(converged) => {
                    result.iterations += converged.iterations;

                    // P-4: Extract the estimated factor value using correct index lookup
                    let ctrl_indices = system.control_variable_indices();
                    let var_idx = ctrl_indices
                        .iter()
                        .find(|(id, _)| *id == &var_id)
                        .map(|(_, idx)| *idx);
                    let estimated = match var_idx {
                        Some(idx) if idx < converged.state.len() => converged.state[idx],
                        _ => {
                            let _ = system.remove_constraint(&constraint_id);
                            let _ = system.remove_bounded_variable(&var_id);
                            let _ = system.finalize();
                            return Err(CalibrationError::SolverError {
                                factor: req.key(),
                                source: SolverError::InvalidSystem {
                                    message: "Control variable index not found in state vector"
                                        .to_string(),
                                },
                            });
                        }
                    };
                    result.estimated_factors.insert(req.key(), estimated);

                    // P-1: Compute residual: measured - computed_output
                    let base_len = ctrl_indices
                        .first()
                        .map(|(_, idx)| *idx)
                        .unwrap_or(converged.state.len());
                    let control_values: Vec<f64> = ctrl_indices
                        .iter()
                        .filter_map(|(_, idx)| {
                            if *idx < converged.state.len() {
                                Some(converged.state[*idx])
                            } else {
                                None
                            }
                        })
                        .collect();
                    // Release immutable borrow on ctrl_indices before mutable borrow below
                    let computed_outputs = system.extract_constraint_values_with_controls(
                        &converged.state[..base_len],
                        &control_values,
                    );
                    let computed_output =
                        computed_outputs.get(&constraint_id).copied().unwrap_or(0.0);
                    let residual = target.measured_value - computed_output;

                    // P-6: Populate residuals HashMap
                    result.residuals.insert(req.key(), residual);
                    all_residual_pairs.push((target.measured_value, residual));

                    // P-2: Update component's Calib struct with estimated factor for next step
                    if let Some(node_idx) = system.get_component_node(&req.component_name) {
                        if let Some(comp) = system.component_mut(node_idx) {
                            comp.update_calib_factor(req.factor.as_str(), estimated);
                        }
                    }

                    // Check saturation
                    if (estimated - req.bounds.0).abs() < 1e-9
                        || (estimated - req.bounds.1).abs() < 1e-9
                    {
                        result.saturated_factors.push(req.key());
                    }
                }
                Err(SolverError::NonConvergence {
                    iterations,
                    final_residual,
                }) => {
                    let _ = system.remove_constraint(&constraint_id);
                    let _ = system.remove_bounded_variable(&var_id);
                    let _ = system.finalize();

                    return Err(CalibrationError::ConvergenceFailure {
                        factor: req.key(),
                        iterations,
                        residual_norm: final_residual,
                        partial_count: result.estimated_factors.len(),
                    });
                }
                Err(SolverError::Divergence { reason: _ }) => {
                    let _ = system.remove_constraint(&constraint_id);
                    let _ = system.remove_bounded_variable(&var_id);
                    let _ = system.finalize();

                    return Err(CalibrationError::ConvergenceFailure {
                        factor: req.key(),
                        iterations: 0,
                        residual_norm: f64::NAN,
                        partial_count: result.estimated_factors.len(),
                    });
                }
                Err(e) => {
                    let _ = system.remove_constraint(&constraint_id);
                    let _ = system.remove_bounded_variable(&var_id);
                    let _ = system.finalize();

                    return Err(CalibrationError::SolverError {
                        factor: req.key(),
                        source: e,
                    });
                }
            }

            // P-8: Clean up this step's constraint and bounded variable,
            // then re-finalize to keep state layout consistent before next iteration
            system.remove_constraint(&constraint_id);
            system.remove_bounded_variable(&var_id);
            let _ = system.finalize();
        }

        result.mape = CalibrationResult::compute_mape(&all_residual_pairs);
        result.max_abs_error = all_residual_pairs
            .iter()
            .map(|(_, r)| r.abs())
            .fold(0.0_f64, f64::max);
        result.converged = true;

        Ok(result)
    }

    fn calibrate_simultaneous(
        &self,
        system: &mut System,
        base_config: &NewtonConfig,
    ) -> Result<CalibrationResult, CalibrationError> {
        let mut result = CalibrationResult::new();
        let mut constraint_ids: Vec<ConstraintId> = Vec::new();
        let mut var_ids: Vec<BoundedVariableId> = Vec::new();

        // Set up all constraints and bounded variables
        for (i, req) in self.requests.iter().enumerate() {
            let target = &self.targets[i];
            let constraint_id = ConstraintId::new(format!("calib_{}", req.key()));
            let var_id = BoundedVariableId::new(req.key());

            let constraint = target.to_constraint(constraint_id.clone());
            system.add_constraint(constraint)?;

            let bv = BoundedVariable::with_component(
                var_id.clone(),
                &req.component_name,
                req.initial,
                req.bounds.0,
                req.bounds.1,
            )
            .map_err(|e| {
                CalibrationError::ConstraintError(ConstraintError::InvalidConfiguration {
                    reason: format!("Bounded variable error for {}: {e}", req.key()),
                })
            })?;
            system.add_bounded_variable(bv).map_err(|e| {
                CalibrationError::ConstraintError(ConstraintError::InvalidConfiguration {
                    reason: format!("Failed to add bounded variable: {e}"),
                })
            })?;

            system
                .link_constraint_to_control(&constraint_id, &var_id)
                .map_err(|e| {
                    CalibrationError::ConstraintError(ConstraintError::InvalidConfiguration {
                        reason: format!("Link error: {e}"),
                    })
                })?;

            constraint_ids.push(constraint_id);
            var_ids.push(var_id);
        }

        // Re-finalize
        system
            .finalize()
            .map_err(|_| CalibrationError::SystemNotFinalized)?;

        // Create solver
        let initial_state = vec![0.0; system.full_state_vector_len()];
        let mut solver = base_config.clone().with_initial_state(initial_state);

        let solve_result = solver.solve(system);
        match solve_result {
            Ok(converged) => {
                result.iterations = converged.iterations;
                result.converged = true;

                // P-4: Extract control variable values using correct indices
                let ctrl_indices = system.control_variable_indices();
                let base_len = ctrl_indices
                    .first()
                    .map(|(_, idx)| *idx)
                    .unwrap_or(converged.state.len());
                let control_values: Vec<f64> = ctrl_indices
                    .iter()
                    .filter_map(|(_, idx)| {
                        if *idx < converged.state.len() {
                            Some(converged.state[*idx])
                        } else {
                            None
                        }
                    })
                    .collect();

                for (i, req) in self.requests.iter().enumerate() {
                    let estimated = ctrl_indices
                        .iter()
                        .find(|(id, _)| *id == &var_ids[i])
                        .and_then(|(_, idx)| converged.state.get(*idx).copied())
                        .unwrap_or(0.0);
                    result.estimated_factors.insert(req.key(), estimated);

                    if (estimated - req.bounds.0).abs() < 1e-9
                        || (estimated - req.bounds.1).abs() < 1e-9
                    {
                        result.saturated_factors.push(req.key());
                    }
                }

                // P-5: Compute residuals using computed outputs (not factor values)
                let computed_outputs = system.extract_constraint_values_with_controls(
                    &converged.state[..base_len],
                    &control_values,
                );
                for (i, req) in self.requests.iter().enumerate() {
                    let constraint_id = ConstraintId::new(format!("calib_{}", req.key()));
                    let computed_output =
                        computed_outputs.get(&constraint_id).copied().unwrap_or(0.0);
                    let residual = self.targets[i].measured_value - computed_output;
                    result.residuals.insert(req.key(), residual);
                }
            }
            Err(SolverError::NonConvergence {
                iterations,
                final_residual,
            }) => {
                for cid in &constraint_ids {
                    system.remove_constraint(cid);
                }
                for vid in &var_ids {
                    system.remove_bounded_variable(vid);
                }
                let _ = system.finalize();

                return Err(CalibrationError::ConvergenceFailure {
                    factor: "all".to_string(),
                    iterations,
                    residual_norm: final_residual,
                    partial_count: 0,
                });
            }
            Err(SolverError::Divergence { reason: _ }) => {
                for cid in &constraint_ids {
                    system.remove_constraint(cid);
                }
                for vid in &var_ids {
                    system.remove_bounded_variable(vid);
                }
                let _ = system.finalize();

                return Err(CalibrationError::ConvergenceFailure {
                    factor: "all".to_string(),
                    iterations: 0,
                    residual_norm: f64::NAN,
                    partial_count: 0,
                });
            }
            Err(e) => {
                for cid in &constraint_ids {
                    system.remove_constraint(cid);
                }
                for vid in &var_ids {
                    system.remove_bounded_variable(vid);
                }
                let _ = system.finalize();

                return Err(CalibrationError::SolverError {
                    factor: "all".to_string(),
                    source: e,
                });
            }
        }

        // Clean up
        for cid in &constraint_ids {
            system.remove_constraint(cid);
        }
        for vid in &var_ids {
            system.remove_bounded_variable(vid);
        }
        let _ = system.finalize();

        // Compute error metrics from residuals
        let residual_pairs: Vec<(f64, f64)> = self
            .requests
            .iter()
            .enumerate()
            .map(|(i, req)| {
                let measured = self.targets[i].measured_value;
                let residual = result.residuals.get(&req.key()).copied().unwrap_or(0.0);
                (measured, residual)
            })
            .collect();
        result.mape = CalibrationResult::compute_mape(&residual_pairs);
        result.max_abs_error = residual_pairs
            .iter()
            .map(|(_, r)| r.abs())
            .fold(0.0_f64, f64::max);

        Ok(result)
    }
}

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

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

    #[test]
    fn test_calib_factor_order() {
        let order = CalibFactor::calibration_order();
        assert_eq!(order.len(), 6);
        assert_eq!(order[0], CalibFactor::ZFlow);
        assert_eq!(order[1], CalibFactor::ZFlowEco);
        assert_eq!(order[2], CalibFactor::ZDp);
        assert_eq!(order[3], CalibFactor::ZUa);
        assert_eq!(order[4], CalibFactor::ZPower);
        assert_eq!(order[5], CalibFactor::ZEtav);
    }

    #[test]
    fn test_calib_factor_default_bounds() {
        let (lo, hi) = CalibFactor::ZUa.default_bounds();
        assert_eq!(lo, 0.1);
        assert_eq!(hi, 10.0);

        let (lo, hi) = CalibFactor::ZFlow.default_bounds();
        assert_eq!(lo, 0.5);
        assert_eq!(hi, 2.0);

        let (lo, hi) = CalibFactor::ZFlowEco.default_bounds();
        assert_eq!(lo, 0.5);
        assert_eq!(hi, 2.0);
    }

    #[test]
    fn test_calib_factor_display() {
        assert_eq!(format!("{}", CalibFactor::ZFlow), "z_flow");
        assert_eq!(format!("{}", CalibFactor::ZFlowEco), "z_flow_eco");
        assert_eq!(format!("{}", CalibFactor::ZUa), "z_ua");
    }

    #[test]
    fn test_calib_request_key() {
        let req = CalibRequest::new(CalibFactor::ZUa, "evaporator", (0.1, 10.0), 1.0);
        assert_eq!(req.key(), "evaporator.z_ua");
    }

    #[test]
    fn test_calib_request_default_bounds() {
        let req = CalibRequest::with_default_bounds(CalibFactor::ZUa, "evaporator", 1.0);
        assert_eq!(req.bounds, (0.1, 10.0));
    }

    #[test]
    fn test_calibration_target_constructors() {
        let t = CalibrationTarget::capacity("evaporator", 4015.0);
        assert_eq!(t.measured_value, 4015.0);
        assert_eq!(t.output.component_id(), "evaporator");

        let t = CalibrationTarget::mass_flow("compressor", 0.05);
        assert_eq!(t.measured_value, 0.05);

        let t = CalibrationTarget::superheat("evaporator", 5.0);
        assert_eq!(t.measured_value, 5.0);
    }

    #[test]
    fn test_calibration_target_to_constraint() {
        let t = CalibrationTarget::capacity("evaporator", 4015.0);
        let c = t.to_constraint(ConstraintId::new("test"));
        assert_eq!(c.target_value(), 4015.0);
    }

    #[test]
    fn test_calibration_result_mape() {
        let pairs = vec![(100.0, 2.0), (200.0, -4.0)];
        let mape = CalibrationResult::compute_mape(&pairs);
        // (2/100 + 4/200) * 100 / 2 = (0.02 + 0.02) * 100 / 2 = 2.0
        approx::assert_relative_eq!(mape, 2.0, epsilon = 1e-10);
    }

    #[test]
    fn test_calibration_result_mape_empty() {
        let mape = CalibrationResult::compute_mape(&[]);
        approx::assert_relative_eq!(mape, 0.0);
    }

    #[test]
    fn test_calibration_mode_default() {
        assert_eq!(CalibrationMode::default(), CalibrationMode::Sequential);
    }

    #[test]
    fn test_calibration_problem_builder() {
        let p = CalibrationProblem::new()
            .with_mode(CalibrationMode::Simultaneous)
            .add_request(CalibRequest::new(
                CalibFactor::ZUa,
                "evaporator",
                (0.1, 10.0),
                1.0,
            ))
            .add_target(CalibrationTarget::capacity("evaporator", 4015.0));

        assert_eq!(p.n_targets(), 1);
        assert_eq!(p.n_requests(), 1);
        assert_eq!(p.mode(), CalibrationMode::Simultaneous);
    }

    #[test]
    fn test_calibration_error_dof_mismatch() {
        let p = CalibrationProblem::new().add_request(CalibRequest::new(
            CalibFactor::ZUa,
            "evaporator",
            (0.1, 10.0),
            1.0,
        ));

        let sys = System::new();
        let err = p.validate(&sys).unwrap_err();
        assert!(matches!(err, CalibrationError::DoFMismatch { .. }));
    }

    #[test]
    fn test_calibration_error_component_not_found() {
        let p = CalibrationProblem::new()
            .add_request(CalibRequest::new(
                CalibFactor::ZUa,
                "nonexistent",
                (0.1, 10.0),
                1.0,
            ))
            .add_target(CalibrationTarget::capacity("nonexistent", 4015.0));

        let sys = System::new();
        let err = p.validate(&sys).unwrap_err();
        assert!(matches!(err, CalibrationError::ComponentNotFound { .. }));
    }

    #[test]
    fn test_calibration_result_json_roundtrip() {
        let mut result = CalibrationResult::new();
        result
            .estimated_factors
            .insert("evaporator.z_ua".to_string(), 1.15);
        result
            .estimated_factors
            .insert("compressor.z_flow".to_string(), 0.95);
        result.residuals.insert("evaporator.z_ua".to_string(), 0.02);
        result.mape = 1.5;
        result.max_abs_error = 0.05;
        result.iterations = 42;
        result.converged = true;
        result
            .saturated_factors
            .push("compressor.z_flow".to_string());

        let json = serde_json::to_string(&result).unwrap();
        let result2: CalibrationResult = serde_json::from_str(&json).unwrap();
        assert_eq!(result, result2);
    }

    #[test]
    fn test_calib_factor_serialize_roundtrip() {
        let factor = CalibFactor::ZUa;
        let json = serde_json::to_string(&factor).unwrap();
        let factor2: CalibFactor = serde_json::from_str(&json).unwrap();
        assert_eq!(factor, factor2);
    }
}
