//! Convergence criteria for multi-circuit thermodynamic systems.
//!
//! This module implements multi-dimensional convergence checking with per-circuit
//! granularity, as required by FR20 and FR21:
//!
//! - **FR20**: Convergence criterion: max |ΔP| < 1 Pa (1e-5 bar)
//! - **FR21**: Global multi-circuit convergence: ALL circuits must converge
//!
//! # Proxy Approach (Story 4.7)
//!
//! Full mass and energy balance validation requires component-level metadata
//! that does not exist until Epic 7 (Stories 7-1, 7-2). For Story 4.7, the
//! mass and energy balance checks use the **per-circuit residual L2 norm** as
//! a proxy: when all residual equations within a circuit satisfy the tolerance,
//! the circuit is considered mass- and energy-balanced. This is a valid
//! approximation because the residuals encode both pressure continuity and
//! enthalpy balance equations simultaneously.
//!
//! # Example
//!
//! ```rust,no_run
//! use entropyk_solver::criteria::{ConvergenceCriteria, ConvergenceReport};
//! use entropyk_solver::system::System;
//!
//! let criteria = ConvergenceCriteria::default();
//! // let report = criteria.check(&state, Some(&prev_state), &residuals, &system);
//! // assert!(report.is_globally_converged());
//! ```

use crate::system::System;

// ─────────────────────────────────────────────────────────────────────────────
// Public types
// ─────────────────────────────────────────────────────────────────────────────

/// Configurable convergence thresholds for multi-circuit systems.
///
/// Controls the three convergence dimensions checked per circuit:
/// 1. **Pressure**: max |ΔP| across pressure state variables
/// 2. **Mass balance**: per-circuit residual L2 norm (proxy for Story 4.7)
/// 3. **Energy balance**: per-circuit residual L2 norm (proxy for Story 4.7)
///
/// # Default values
///
/// | Field | Default | Rationale |
/// |-------|---------|-----------|
/// | `pressure_tolerance_pa` | 1.0 Pa | FR20: 1 Pa = 1e-5 bar |
/// | `mass_balance_tolerance_kgs` | 1e-9 kg/s | Architecture requirement |
/// | `energy_balance_tolerance_w` | 1e-3 W | = 1e-6 kW architecture requirement |
#[derive(Debug, Clone, PartialEq)]
pub struct ConvergenceCriteria {
    /// Maximum allowed |ΔP| across any pressure state variable.
    ///
    /// Convergence requires: `max |state[p_idx] - prev_state[p_idx]| < pressure_tolerance_pa`
    ///
    /// Default: 1.0 Pa (FR20).
    pub pressure_tolerance_pa: f64,

    /// Mass balance tolerance per circuit (default: 1e-9 kg/s).
    ///
    /// **Story 4.7 proxy**: Uses per-circuit residual L2 norm instead of
    /// explicit mass flow balance. Full mass balance is implemented in Epic 7 (Story 7-1).
    pub mass_balance_tolerance_kgs: f64,

    /// Energy balance tolerance per circuit (default: 1e-3 W = 1e-6 kW).
    ///
    /// **Story 4.7 proxy**: Uses per-circuit residual L2 norm instead of
    /// explicit enthalpy balance. Full energy balance is implemented in Epic 7 (Story 7-2).
    pub energy_balance_tolerance_w: f64,
}

impl Default for ConvergenceCriteria {
    fn default() -> Self {
        Self {
            pressure_tolerance_pa: 1.0,
            mass_balance_tolerance_kgs: 1e-9,
            energy_balance_tolerance_w: 1e-3,
        }
    }
}

/// Per-circuit convergence breakdown.
///
/// Each instance represents the convergence status of a single circuit
/// in a multi-circuit system. All three sub-checks must pass for the
/// circuit to be considered converged.
#[derive(Debug, Clone, PartialEq)]
pub struct CircuitConvergence {
    /// The circuit identifier (0-indexed).
    pub circuit_id: u16,

    /// Pressure convergence satisfied: `max |ΔP| < pressure_tolerance_pa`.
    pub pressure_ok: bool,

    /// Mass balance convergence satisfied (proxy: per-circuit residual norm).
    /// Full mass balance validation is deferred to Epic 7 (Story 7-1).
    pub mass_ok: bool,

    /// Energy balance convergence satisfied (proxy: per-circuit residual norm).
    /// Full energy balance validation is deferred to Epic 7 (Story 7-2).
    pub energy_ok: bool,

    /// `true` iff `pressure_ok && mass_ok && energy_ok`.
    pub converged: bool,
}

/// Aggregated convergence result for all circuits in the system.
///
/// Contains one [`CircuitConvergence`] entry per active circuit,
/// plus a cached global flag.
#[derive(Debug, Clone, PartialEq)]
pub struct ConvergenceReport {
    /// Per-circuit breakdown (one entry per circuit, ordered by circuit ID).
    pub per_circuit: Vec<CircuitConvergence>,

    /// `true` iff every circuit in `per_circuit` has `converged == true`.
    pub globally_converged: bool,
}

impl ConvergenceReport {
    /// Returns `true` if ALL circuits are converged.
    pub fn is_globally_converged(&self) -> bool {
        self.globally_converged
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// ConvergenceCriteria implementation
// ─────────────────────────────────────────────────────────────────────────────

impl ConvergenceCriteria {
    /// Evaluate convergence for all circuits in the system.
    ///
    /// # Arguments
    ///
    /// * `state` — Current full state vector (length = `system.state_vector_len()`).
    ///   Layout: `[P_edge0, h_edge0, P_edge1, h_edge1, ...]`
    /// * `prev_state` — Previous iteration state (same length). Used to compute ΔP.
    ///   When `None` (first call), the residuals at pressure indices are used as proxy.
    /// * `residuals` — Current residual vector from `system.compute_residuals()`.
    ///   Used as mass/energy proxy and as ΔP fallback on first iteration.
    /// * `system` — Finalized `System` for circuit decomposition.
    ///
    /// # Panics
    ///
    /// Does not panic. Length mismatches trigger `debug_assert!` in debug builds
    /// and fall back to conservative (not-converged) results in release builds.
    pub fn check(
        &self,
        state: &[f64],
        prev_state: Option<&[f64]>,
        residuals: &[f64],
        system: &System,
    ) -> ConvergenceReport {
        debug_assert!(
            state.len() == system.state_vector_len(),
            "state length {} != system state length {}",
            state.len(),
            system.state_vector_len()
        );

        if let Some(prev) = prev_state {
            debug_assert!(
                prev.len() == state.len(),
                "prev_state length {} != state length {}",
                prev.len(),
                state.len()
            );
        }

        let n_circuits = system.circuit_count();
        let mut per_circuit = Vec::with_capacity(n_circuits);

        // Build per-circuit equation index mapping.
        // The residual vector is ordered by traverse_for_jacobian(), which
        // visits components in circuit order. We track which residual equation
        // indices belong to which circuit by matching state indices.
        //
        // Equation ordering heuristic: residual equations are paired with
        // state variables — equation 2*i is the pressure equation for edge i,
        // equation 2*i+1 is the enthalpy equation for edge i.
        // This matches the state vector layout [P_edge0, h_edge0, ...].

        for circuit_idx in 0..n_circuits {
            let circuit_id = circuit_idx as u16;

            // Collect pressure-variable indices for this circuit
            let pressure_indices: Vec<usize> = system
                .circuit_edges(crate::CircuitId(circuit_id.into()))
                .map(|edge| {
                    let (p_idx, _h_idx) = system.edge_state_indices(edge);
                    p_idx
                })
                .collect();

            if pressure_indices.is_empty() {
                // Empty circuit — conservatively mark as not converged
                tracing::debug!(circuit_id = circuit_id, "Empty circuit — skipping");
                per_circuit.push(CircuitConvergence {
                    circuit_id: circuit_id as u16,
                    pressure_ok: false,
                    mass_ok: false,
                    energy_ok: false,
                    converged: false,
                });
                continue;
            }

            // ── Pressure check ────────────────────────────────────────────────
            // max |ΔP| = max |state[p_idx] - prev[p_idx]|
            // Fallback on first iteration: use |residuals[p_idx]| as proxy for ΔP.
            let max_delta_p = pressure_indices
                .iter()
                .map(|&p_idx| {
                    let p = if p_idx < state.len() {
                        state[p_idx]
                    } else {
                        0.0
                    };
                    if let Some(prev) = prev_state {
                        let pp = if p_idx < prev.len() { prev[p_idx] } else { 0.0 };
                        (p - pp).abs()
                    } else {
                        // First-call fallback: residual at pressure index
                        let r = if p_idx < residuals.len() {
                            residuals[p_idx]
                        } else {
                            0.0
                        };
                        r.abs()
                    }
                })
                .fold(0.0_f64, f64::max);

            let pressure_ok = max_delta_p < self.pressure_tolerance_pa;

            tracing::debug!(
                circuit_id = circuit_id,
                max_delta_p = max_delta_p,
                threshold = self.pressure_tolerance_pa,
                pressure_ok = pressure_ok,
                "Pressure convergence check"
            );

            // ── Mass/Energy balance check (proxy: per-circuit residual L2 norm) ──
            // Partition residuals by circuit: residual equations are interleaved
            // with state variables. Pressure equation index = p_idx, enthalpy
            // equation index = h_idx (= p_idx + 1 by layout convention).
            let circuit_residual_norm_sq: f64 = system
                .circuit_edges(crate::CircuitId(circuit_id.into()))
                .map(|edge| {
                    let (p_idx, h_idx) = system.edge_state_indices(edge);
                    let rp = if p_idx < residuals.len() {
                        residuals[p_idx]
                    } else {
                        0.0
                    };
                    let rh = if h_idx < residuals.len() {
                        residuals[h_idx]
                    } else {
                        0.0
                    };
                    rp * rp + rh * rh
                })
                .sum();

            let circuit_residual_norm = circuit_residual_norm_sq.sqrt();

            let mass_ok = circuit_residual_norm < self.mass_balance_tolerance_kgs;
            let energy_ok = circuit_residual_norm < self.energy_balance_tolerance_w;

            tracing::debug!(
                circuit_id = circuit_id,
                residual_norm = circuit_residual_norm,
                mass_threshold = self.mass_balance_tolerance_kgs,
                energy_threshold = self.energy_balance_tolerance_w,
                mass_ok = mass_ok,
                energy_ok = energy_ok,
                "Mass/Energy convergence check (proxy)"
            );

            let converged = pressure_ok && mass_ok && energy_ok;

            per_circuit.push(CircuitConvergence {
                circuit_id,
                pressure_ok,
                mass_ok,
                energy_ok,
                converged,
            });
        }

        let globally_converged = !per_circuit.is_empty() && per_circuit.iter().all(|c| c.converged);

        tracing::debug!(
            n_circuits = n_circuits,
            globally_converged = globally_converged,
            "Global convergence check complete"
        );

        ConvergenceReport {
            per_circuit,
            globally_converged,
        }
    }
}

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

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

    #[test]
    fn test_default_thresholds() {
        let c = ConvergenceCriteria::default();
        assert_relative_eq!(c.pressure_tolerance_pa, 1.0);
        assert_relative_eq!(c.mass_balance_tolerance_kgs, 1e-9);
        assert_relative_eq!(c.energy_balance_tolerance_w, 1e-3);
    }

    #[test]
    fn test_convergence_report_is_globally_converged_all_true() {
        let report = ConvergenceReport {
            per_circuit: vec![
                CircuitConvergence {
                    circuit_id: 0,
                    pressure_ok: true,
                    mass_ok: true,
                    energy_ok: true,
                    converged: true,
                },
                CircuitConvergence {
                    circuit_id: 1,
                    pressure_ok: true,
                    mass_ok: true,
                    energy_ok: true,
                    converged: true,
                },
            ],
            globally_converged: true,
        };
        assert!(report.is_globally_converged());
    }

    #[test]
    fn test_convergence_report_is_globally_converged_one_fails() {
        let report = ConvergenceReport {
            per_circuit: vec![
                CircuitConvergence {
                    circuit_id: 0,
                    pressure_ok: true,
                    mass_ok: true,
                    energy_ok: true,
                    converged: true,
                },
                CircuitConvergence {
                    circuit_id: 1,
                    pressure_ok: false, // fails
                    mass_ok: true,
                    energy_ok: true,
                    converged: false,
                },
            ],
            globally_converged: false,
        };
        assert!(!report.is_globally_converged());
    }

    #[test]
    fn test_convergence_report_empty_circuits_not_globally_converged() {
        // Empty per_circuit → not globally converged (no circuits = not proven converged)
        let report = ConvergenceReport {
            per_circuit: vec![],
            globally_converged: false,
        };
        assert!(!report.is_globally_converged());
    }

    #[test]
    fn test_circuit_convergence_converged_field() {
        // converged = pressure_ok && mass_ok && energy_ok
        let all_ok = CircuitConvergence {
            circuit_id: 0,
            pressure_ok: true,
            mass_ok: true,
            energy_ok: true,
            converged: true,
        };
        assert!(all_ok.converged);

        let pressure_fail = CircuitConvergence {
            circuit_id: 0,
            pressure_ok: false,
            mass_ok: true,
            energy_ok: true,
            converged: false,
        };
        assert!(!pressure_fail.converged);
    }

    #[test]
    fn test_custom_thresholds() {
        let criteria = ConvergenceCriteria {
            pressure_tolerance_pa: 0.1,
            mass_balance_tolerance_kgs: 1e-12,
            energy_balance_tolerance_w: 1e-6,
        };
        assert_relative_eq!(criteria.pressure_tolerance_pa, 0.1);
        assert_relative_eq!(criteria.mass_balance_tolerance_kgs, 1e-12);
        assert_relative_eq!(criteria.energy_balance_tolerance_w, 1e-6);
    }

    #[test]
    fn test_multi_circuit_global_needs_all() {
        // 2 circuits, circuit 1 fails → not globally converged
        let report = ConvergenceReport {
            per_circuit: vec![
                CircuitConvergence {
                    circuit_id: 0,
                    pressure_ok: true,
                    mass_ok: true,
                    energy_ok: true,
                    converged: true,
                },
                CircuitConvergence {
                    circuit_id: 1,
                    pressure_ok: true,
                    mass_ok: false,
                    energy_ok: true,
                    converged: false,
                },
            ],
            globally_converged: false,
        };
        assert!(!report.is_globally_converged());
    }

    #[test]
    fn test_multi_circuit_all_converged() {
        // 2 circuits both converged → globally converged
        let report = ConvergenceReport {
            per_circuit: vec![
                CircuitConvergence {
                    circuit_id: 0,
                    pressure_ok: true,
                    mass_ok: true,
                    energy_ok: true,
                    converged: true,
                },
                CircuitConvergence {
                    circuit_id: 1,
                    pressure_ok: true,
                    mass_ok: true,
                    energy_ok: true,
                    converged: true,
                },
            ],
            globally_converged: true,
        };
        assert!(report.is_globally_converged());
    }

    #[test]
    fn test_report_per_circuit_count() {
        // N circuits → report has N entries
        let n = 5;
        let per_circuit: Vec<CircuitConvergence> = (0..n)
            .map(|i| CircuitConvergence {
                circuit_id: i as u16,
                pressure_ok: true,
                mass_ok: true,
                energy_ok: true,
                converged: true,
            })
            .collect();
        let report = ConvergenceReport {
            globally_converged: per_circuit.iter().all(|c| c.converged),
            per_circuit,
        };
        assert_eq!(report.per_circuit.len(), n);
    }
}
