//! Sequential Substitution (Picard iteration) solver implementation.
//!
//! Provides [`PicardConfig`] which implements Picard iteration for solving
//! systems of non-linear equations. Slower than Newton-Raphson but more robust.

use std::collections::VecDeque;
use std::time::{Duration, Instant};

use nalgebra::{DMatrix, DVector};

use crate::criteria::ConvergenceCriteria;
use crate::metadata::SimulationMetadata;
use crate::solver::{
    dominant_residual, ConvergedState, ConvergenceDiagnostics, ConvergenceStatus,
    IterationDiagnostics, Solver, SolverError, SolverType, TimeoutConfig, VerboseConfig,
};
use crate::system::System;
use entropyk_components::ComponentError;

/// Configuration for the Sequential Substitution (Picard iteration) solver.
///
/// Solves x = G(x) by iterating: x_{k+1} = (1-ω)·x_k + ω·G(x_k)
/// where ω ∈ (0,1] is the relaxation factor.
#[derive(Debug, Clone, PartialEq)]
pub struct PicardConfig {
    /// Maximum iterations. Default: 100.
    pub max_iterations: usize,
    /// Convergence tolerance (L2 norm). Default: 1e-6.
    pub tolerance: f64,
    /// Relaxation factor ω ∈ (0,1]. Default: 0.5.
    pub relaxation_factor: f64,
    /// Optional time budget.
    pub timeout: Option<Duration>,
    /// Divergence threshold. Default: 1e10.
    pub divergence_threshold: f64,
    /// Consecutive increases before divergence. Default: 5.
    pub divergence_patience: usize,
    /// Timeout behavior configuration.
    pub timeout_config: TimeoutConfig,
    /// Previous state for ZOH fallback.
    pub previous_state: Option<Vec<f64>>,
    /// Residual for previous_state.
    pub previous_residual: Option<f64>,
    /// Smart initial state for cold-start.
    pub initial_state: Option<Vec<f64>>,
    /// Multi-circuit convergence criteria.
    pub convergence_criteria: Option<ConvergenceCriteria>,
    /// Verbose mode configuration for diagnostics.
    pub verbose_config: VerboseConfig,
    /// Anderson acceleration depth `m` (history window). `0` disables acceleration
    /// and the solver behaves as plain relaxed Picard (default). Typical useful
    /// values are 3–5. See [`PicardConfig::with_anderson`].
    pub anderson_depth: usize,
    /// Tikhonov regularization added to the Anderson least-squares normal matrix
    /// for numerical stability. Default: 1e-10. Only used when `anderson_depth > 0`.
    pub anderson_regularization: f64,
}

impl Default for PicardConfig {
    fn default() -> Self {
        Self {
            max_iterations: 100,
            tolerance: 1e-6,
            relaxation_factor: 0.5,
            timeout: None,
            divergence_threshold: 1e10,
            divergence_patience: 5,
            timeout_config: TimeoutConfig::default(),
            previous_state: None,
            previous_residual: None,
            initial_state: None,
            convergence_criteria: None,
            verbose_config: VerboseConfig::default(),
            anderson_depth: 0,
            anderson_regularization: 1e-10,
        }
    }
}

impl PicardConfig {
    /// Sets the initial state for cold-start solving (Story 4.6 — builder pattern).
    ///
    /// The solver will start from `state` instead of the zero vector.
    /// Use [`SmartInitializer::populate_state`] to generate a physically reasonable
    /// initial guess.
    pub fn with_initial_state(mut self, state: Vec<f64>) -> Self {
        self.initial_state = Some(state);
        self
    }

    /// Sets multi-circuit convergence criteria (Story 4.7 — builder pattern).
    ///
    /// When set, the solver uses [`ConvergenceCriteria::check()`] instead of the
    /// raw L2-norm `tolerance` check.
    pub fn with_convergence_criteria(mut self, criteria: ConvergenceCriteria) -> Self {
        self.convergence_criteria = Some(criteria);
        self
    }

    /// Enables verbose mode for diagnostics.
    pub fn with_verbose(mut self, config: VerboseConfig) -> Self {
        self.verbose_config = config;
        self
    }

    /// Enables Anderson acceleration with history depth `m` (Story: solver speed).
    ///
    /// Anderson acceleration (Walker & Ni, 2011) turns the linearly-convergent
    /// relaxed Picard fixed-point iteration into a super-linearly convergent one by
    /// extrapolating from the last `m` residual/map-value pairs via a small
    /// least-squares problem. `m = 0` disables it (plain relaxed Picard). Values of
    /// 3–5 typically cut the iteration count by 2–3× on stiff refrigeration cycles
    /// while adding only an `O(m² · n)` least-squares solve per iteration.
    ///
    /// # Reference
    /// Walker, H.F., Ni, P. (2011). "Anderson acceleration for fixed-point
    /// iterations." *SIAM J. Numerical Analysis*, 49(4):1715–1735.
    pub fn with_anderson(mut self, depth: usize) -> Self {
        self.anderson_depth = depth;
        self
    }

    /// Computes the residual norm (L2 norm of the residual vector).
    fn residual_norm(residuals: &[f64]) -> f64 {
        residuals.iter().map(|r| r * r).sum::<f64>().sqrt()
    }

    /// Handles timeout based on configuration (Story 4.5).
    ///
    /// Returns either:
    /// - `Ok(ConvergedState)` with `TimedOutWithBestState` status (default)
    /// - `Err(SolverError::Timeout)` if `return_best_state_on_timeout` is false
    /// - Previous state (ZOH) if `zoh_fallback` is true and previous state available
    fn handle_timeout(
        &self,
        best_state: &[f64],
        best_residual: f64,
        iterations: usize,
        timeout: Duration,
        system: &System,
    ) -> Result<ConvergedState, SolverError> {
        // If configured to return error on timeout
        if !self.timeout_config.return_best_state_on_timeout {
            return Err(SolverError::Timeout {
                timeout_ms: timeout.as_millis() as u64,
            });
        }

        // If ZOH fallback is enabled and previous state is available
        if self.timeout_config.zoh_fallback {
            if let Some(ref prev_state) = self.previous_state {
                let residual = self.previous_residual.unwrap_or(best_residual);
                tracing::info!(
                    iterations = iterations,
                    residual = residual,
                    "Returning previous state (ZOH fallback) on timeout"
                );
                return Ok(ConvergedState::new(
                    prev_state.clone(),
                    iterations,
                    residual,
                    ConvergenceStatus::TimedOutWithBestState,
                    SimulationMetadata::new(system.input_hash()),
                ));
            }
        }

        // Default: return best state encountered during iteration
        tracing::info!(
            iterations = iterations,
            best_residual = best_residual,
            "Returning best state on timeout"
        );
        Ok(ConvergedState::new(
            best_state.to_vec(),
            iterations,
            best_residual,
            ConvergenceStatus::TimedOutWithBestState,
            SimulationMetadata::new(system.input_hash()),
        ))
    }

    /// Checks for divergence based on residual growth pattern.
    ///
    /// Returns `Some(SolverError::Divergence)` if:
    /// - Residual norm exceeds `divergence_threshold`, or
    /// - Residual has increased for `divergence_patience`+ consecutive iterations
    fn check_divergence(
        &self,
        current_norm: f64,
        previous_norm: f64,
        divergence_count: &mut usize,
    ) -> Option<SolverError> {
        // Check absolute threshold
        if current_norm > self.divergence_threshold {
            return Some(SolverError::Divergence {
                reason: format!(
                    "Residual norm {} exceeds threshold {}",
                    current_norm, self.divergence_threshold
                ),
            });
        }

        // Check consecutive increases
        if current_norm > previous_norm {
            *divergence_count += 1;
            if *divergence_count >= self.divergence_patience {
                return Some(SolverError::Divergence {
                    reason: format!(
                        "Residual increased for {} consecutive iterations: {:.6e} → {:.6e}",
                        self.divergence_patience, previous_norm, current_norm
                    ),
                });
            }
        } else {
            *divergence_count = 0;
        }

        None
    }

    /// Applies relaxation to the state update.
    ///
    /// Update formula: x_new = x_old - omega * residual
    /// where residual = F(x_k) represents the equation residuals.
    ///
    /// This is the standard Picard iteration: x_{k+1} = x_k - ω·F(x_k)
    fn apply_relaxation(state: &mut [f64], residuals: &[f64], omega: f64) {
        for (x, &r) in state.iter_mut().zip(residuals.iter()) {
            *x -= omega * r;
        }
    }

    fn finalize_failure_diagnostics(
        &self,
        mut diagnostics: Option<ConvergenceDiagnostics>,
        iterations: usize,
        final_residual: f64,
        best_residual: f64,
        elapsed_ms: u64,
        final_state: Option<Vec<f64>>,
    ) -> Option<ConvergenceDiagnostics> {
        if let Some(ref mut diag) = diagnostics {
            diag.iterations = iterations;
            diag.final_residual = final_residual;
            diag.best_residual = best_residual;
            diag.converged = false;
            diag.timing_ms = elapsed_ms;
            diag.final_solver = Some(SolverType::SequentialSubstitution);

            if self.verbose_config.dump_final_state {
                diag.final_state = final_state;
                let json_output = diag.dump_diagnostics(self.verbose_config.output_format);
                tracing::warn!(
                    iterations,
                    final_residual,
                    "Non-convergence diagnostics:\n{}",
                    json_output
                );
            }
        }
        diagnostics
    }
}

impl Solver for PicardConfig {
    fn solve(&mut self, system: &mut System) -> Result<ConvergedState, SolverError> {
        let start_time = Instant::now();

        // Initialize diagnostics collection if verbose mode enabled
        let verbose_enabled = self.verbose_config.enabled && self.verbose_config.is_any_enabled();
        let mut diagnostics = if verbose_enabled {
            Some(ConvergenceDiagnostics::with_capacity(self.max_iterations))
        } else {
            None
        };

        tracing::info!(
            max_iterations = self.max_iterations,
            tolerance = self.tolerance,
            relaxation_factor = self.relaxation_factor,
            divergence_threshold = self.divergence_threshold,
            divergence_patience = self.divergence_patience,
            verbose = verbose_enabled,
            "Sequential Substitution (Picard) solver starting"
        );

        // Get system dimensions
        let n_state = system.full_state_vector_len();
        let n_equations: usize = system
            .traverse_for_jacobian()
            .map(|(_, c, _)| c.n_equations())
            .sum::<usize>()
            + system.constraints().count()
            + system.coupling_residual_count()
            + 2 * system.saturated_controller_count()
            + system.mass_flow_closure_count();

        // Validate system
        if n_state == 0 || n_equations == 0 {
            return Err(SolverError::InvalidSystem {
                message: "Empty system has no state variables or equations".to_string(),
            });
        }

        // Validate state/equation dimensions
        if n_state != n_equations {
            return Err(SolverError::InvalidSystem {
                message: format!(
                    "State dimension ({}) does not match equation count ({})",
                    n_state, n_equations
                ),
            });
        }

        // Pre-allocate all buffers (AC: #6 - no heap allocation in iteration loop)
        // Story 4.6 - AC: #8: Use initial_state if provided, else start from zeros.
        // A mismatched length is a hard error (zero-panic; no silent zeros fallback
        // that would solve a different problem) — consistent with Newton/Homotopy.
        let mut state: Vec<f64> = match self.initial_state.as_ref() {
            Some(s) if s.len() == n_state => s.clone(),
            Some(s) => {
                return Err(SolverError::InvalidSystem {
                    message: format!(
                        "initial_state length {} does not match system state length {}",
                        s.len(),
                        n_state
                    ),
                });
            }
            None => vec![0.0; n_state],
        };
        let mut prev_iteration_state: Vec<f64> = vec![0.0; n_state]; // For convergence delta check
        let mut residuals: Vec<f64> = vec![0.0; n_equations];
        let mut divergence_count: usize = 0;
        let mut previous_norm: f64;

        // Solver bounds mask (pressure floor + bounded control variables):
        // Picard's relaxed update has no line search, so without clipping it
        // can drive a calibration z-factor far outside its bounds (observed:
        // z_ua → −2.8 on an SDT calibration), then crash the components.
        // Saturated-controller actuator slots are EXCLUDED: they carry their
        // own saturation semantics (anti-windup integrator + S(x) inside the
        // controller residual) and hard-clamping them here breaks that dynamic.
        let mut clipping_mask: Vec<Option<(f64, f64)>> = (0..n_state)
            .map(|i| system.get_solver_bounds_for_state_index(i))
            .collect();
        {
            let n_sat = system.saturated_controllers_mut().count();
            for i in 0..n_sat {
                let u_idx = system.saturated_u_index(i);
                if u_idx < clipping_mask.len() {
                    clipping_mask[u_idx] = None;
                }
            }
        }

        // Pre-allocate best-state tracking buffer (Story 4.5 - AC: #5)
        let mut best_state: Vec<f64> = vec![0.0; n_state];
        let mut best_residual: f64;

        // Initial residual computation. A recoverable domain violation is a
        // typed outcome (Picard has no step to shrink); fatal stays InvalidSystem.
        system
            .compute_residuals(&state, &mut residuals)
            .map_err(|e| match e {
                ComponentError::DomainViolation(violation) => {
                    SolverError::DomainViolation(violation)
                }
                fatal => SolverError::InvalidSystem {
                    message: format!("Failed to compute initial residuals: {:?}", fatal),
                },
            })?;

        let mut current_norm = Self::residual_norm(&residuals);

        // Initialize best state tracking with initial state
        best_state.copy_from_slice(&state);
        best_residual = current_norm;

        tracing::debug!(iteration = 0, residual_norm = current_norm, "Initial state");

        // Check if already converged
        if current_norm < self.tolerance {
            tracing::info!(
                iterations = 0,
                final_residual = current_norm,
                "System already converged at initial state"
            );
            return Ok(ConvergedState::new(
                state,
                0,
                current_norm,
                ConvergenceStatus::Converged,
                SimulationMetadata::new(system.input_hash()),
            ));
        }

        // Optional Anderson accelerator (disabled when depth == 0).
        let mut anderson = if self.anderson_depth > 0 {
            Some(AndersonAccelerator::new(
                self.anderson_depth,
                self.anderson_regularization,
            ))
        } else {
            None
        };

        // Main Picard iteration loop
        for iteration in 1..=self.max_iterations {
            // Save state before step for convergence criteria delta checks
            prev_iteration_state.copy_from_slice(&state);

            // Check timeout at iteration start (Story 4.5 - AC: #1)
            if let Some(timeout) = self.timeout {
                if start_time.elapsed() > timeout {
                    tracing::info!(
                        iteration = iteration,
                        elapsed_ms = start_time.elapsed().as_millis(),
                        timeout_ms = timeout.as_millis(),
                        best_residual = best_residual,
                        "Solver timed out"
                    );

                    // Story 4.5 - AC: #2, #6: Return best state or error based on config
                    let failure_diagnostics = self.finalize_failure_diagnostics(
                        diagnostics.take(),
                        iteration - 1,
                        current_norm,
                        best_residual,
                        start_time.elapsed().as_millis() as u64,
                        Some(state.clone()),
                    );
                    return self
                        .handle_timeout(&best_state, best_residual, iteration - 1, timeout, system)
                        .map_err(|err| err.with_optional_diagnostics(failure_diagnostics));
                }
            }

            // Apply update. With Anderson acceleration enabled, extrapolate from the
            // residual/map-value history; otherwise use plain relaxed Picard.
            // Both share the same underlying fixed-point map G(x) = x - ω·F(x).
            if let Some(acc) = anderson.as_mut() {
                acc.next_state_into(&mut state, &residuals, self.relaxation_factor);
            } else {
                Self::apply_relaxation(&mut state, &residuals, self.relaxation_factor);
            }
            // Clamp the relaxed/extrapolated iterate into solver bounds (see
            // the mask comment above): bounded control variables (calibration
            // z-factors, openings) and pressure floors.
            for (i, s) in state.iter_mut().enumerate() {
                if let Some((min, max)) = &clipping_mask[i] {
                    if min.is_finite() && max.is_finite() && min <= max {
                        *s = s.clamp(*min, *max);
                    }
                }
            }

            // Compute new residuals. Recoverable domain violation → typed
            // outcome; fatal → InvalidSystem flattening.
            system
                .compute_residuals(&state, &mut residuals)
                .map_err(|e| match e {
                    ComponentError::DomainViolation(violation) => {
                        SolverError::DomainViolation(violation)
                    }
                    fatal => SolverError::InvalidSystem {
                        message: format!("Failed to compute residuals: {:?}", fatal),
                    },
                })?;

            previous_norm = current_norm;
            current_norm = Self::residual_norm(&residuals);

            // Compute delta norm for diagnostics
            let delta_norm: f64 = state
                .iter()
                .zip(prev_iteration_state.iter())
                .map(|(s, p)| (s - p).powi(2))
                .sum::<f64>()
                .sqrt();

            // Update best state if residual improved (Story 4.5 - AC: #2)
            if current_norm < best_residual {
                best_state.copy_from_slice(&state);
                best_residual = current_norm;
                tracing::debug!(
                    iteration = iteration,
                    best_residual = best_residual,
                    "Best state updated"
                );
            }

            // Verbose mode: Log iteration residuals
            if verbose_enabled && self.verbose_config.log_residuals {
                tracing::info!(
                    iteration,
                    residual_norm = current_norm,
                    delta_norm = delta_norm,
                    relaxation_factor = self.relaxation_factor,
                    "Picard iteration"
                );
            }

            // Collect iteration diagnostics
            if let Some(ref mut diag) = diagnostics {
                let (max_residual_index, max_residual) = dominant_residual(&residuals);
                diag.push_iteration(IterationDiagnostics {
                    iteration,
                    residual_norm: current_norm,
                    delta_norm,
                    alpha: None,              // Picard doesn't use line search
                    jacobian_frozen: false,   // Picard doesn't use Jacobian
                    jacobian_condition: None, // No Jacobian in Picard
                    max_residual_index,
                    max_residual,
                    recoverable_events: 0, // Picard has no step-reduction retry
                });
            }

            tracing::debug!(
                iteration = iteration,
                residual_norm = current_norm,
                relaxation_factor = self.relaxation_factor,
                "Picard iteration complete"
            );

            // Check convergence (AC: #1, Story 4.7 — criteria-aware)
            let converged = if let Some(ref criteria) = self.convergence_criteria {
                let report =
                    criteria.check(&state, Some(&prev_iteration_state), &residuals, system);
                if report.is_globally_converged() {
                    // Finalize diagnostics
                    if let Some(ref mut diag) = diagnostics {
                        diag.iterations = iteration;
                        diag.final_residual = current_norm;
                        diag.best_residual = best_residual;
                        diag.converged = true;
                        diag.timing_ms = start_time.elapsed().as_millis() as u64;
                        diag.final_solver = Some(SolverType::SequentialSubstitution);

                        if self.verbose_config.log_residuals {
                            tracing::info!("{}", diag.summary());
                        }
                    }

                    tracing::info!(
                        iterations = iteration,
                        final_residual = current_norm,
                        relaxation_factor = self.relaxation_factor,
                        "Sequential Substitution converged (criteria)"
                    );
                    let result = ConvergedState::with_report(
                        state,
                        iteration,
                        current_norm,
                        ConvergenceStatus::Converged,
                        report,
                        SimulationMetadata::new(system.input_hash()),
                    );
                    return Ok(if let Some(d) = diagnostics {
                        ConvergedState {
                            diagnostics: Some(d),
                            ..result
                        }
                    } else {
                        result
                    });
                }
                false
            } else {
                current_norm < self.tolerance
            };

            if converged {
                // Finalize diagnostics
                if let Some(ref mut diag) = diagnostics {
                    diag.iterations = iteration;
                    diag.final_residual = current_norm;
                    diag.best_residual = best_residual;
                    diag.converged = true;
                    diag.timing_ms = start_time.elapsed().as_millis() as u64;
                    diag.final_solver = Some(SolverType::SequentialSubstitution);

                    if self.verbose_config.log_residuals {
                        tracing::info!("{}", diag.summary());
                    }
                }

                tracing::info!(
                    iterations = iteration,
                    final_residual = current_norm,
                    relaxation_factor = self.relaxation_factor,
                    "Sequential Substitution converged"
                );
                let result = ConvergedState::new(
                    state,
                    iteration,
                    current_norm,
                    ConvergenceStatus::Converged,
                    SimulationMetadata::new(system.input_hash()),
                );
                return Ok(if let Some(d) = diagnostics {
                    ConvergedState {
                        diagnostics: Some(d),
                        ..result
                    }
                } else {
                    result
                });
            }

            // Check divergence (AC: #5)
            if let Some(err) =
                self.check_divergence(current_norm, previous_norm, &mut divergence_count)
            {
                tracing::warn!(
                    iteration = iteration,
                    residual_norm = current_norm,
                    "Divergence detected"
                );
                let failure_diagnostics = self.finalize_failure_diagnostics(
                    diagnostics.take(),
                    iteration,
                    current_norm,
                    best_residual,
                    start_time.elapsed().as_millis() as u64,
                    Some(state.clone()),
                );
                return Err(err.with_optional_diagnostics(failure_diagnostics));
            }
        }

        // Non-convergence: dump diagnostics if enabled
        let failure_diagnostics = self.finalize_failure_diagnostics(
            diagnostics.take(),
            self.max_iterations,
            current_norm,
            best_residual,
            start_time.elapsed().as_millis() as u64,
            Some(state.clone()),
        );

        // Max iterations exceeded
        tracing::warn!(
            max_iterations = self.max_iterations,
            final_residual = current_norm,
            "Sequential Substitution did not converge"
        );
        Err(SolverError::NonConvergence {
            iterations: self.max_iterations,
            final_residual: current_norm,
        }
        .with_optional_diagnostics(failure_diagnostics))
    }

    fn with_timeout(mut self, timeout: Duration) -> Self {
        self.timeout = Some(timeout);
        self
    }
}

/// Anderson acceleration state for the relaxed Picard fixed-point iteration.
///
/// The underlying fixed-point map is `G(x) = x - ω·F(x)` where `F` is the residual
/// vector and `ω` the relaxation factor. Define the map residual `f(x) = G(x) - x =
/// -ω·F(x)`. Anderson acceleration maintains the last `m` differences of `f` and `G`
/// and, each iteration, solves the small least-squares problem
/// `min_γ ‖f_k - ΔF·γ‖` then sets `x_{k+1} = G_k - ΔG·γ` (Walker & Ni, 2011,
/// following H. Walker's reference `anderson.m`). With `m = 0` (empty history) it
/// reduces exactly to the plain step `x_{k+1} = G_k`.
struct AndersonAccelerator {
    depth: usize,
    regularization: f64,
    /// Previous map-residual f = G(x) - x.
    f_prev: Option<Vec<f64>>,
    /// Previous map value G(x).
    g_prev: Option<Vec<f64>>,
    /// History of Δf columns (most-recent at back), capped at `depth`.
    df: VecDeque<Vec<f64>>,
    /// History of ΔG columns (most-recent at back), capped at `depth`.
    dg: VecDeque<Vec<f64>>,
}

impl AndersonAccelerator {
    fn new(depth: usize, regularization: f64) -> Self {
        Self {
            depth,
            regularization,
            f_prev: None,
            g_prev: None,
            df: VecDeque::with_capacity(depth),
            dg: VecDeque::with_capacity(depth),
        }
    }

    /// Advances `state` in place from `x_k` to the accelerated `x_{k+1}`, given the
    /// current residual vector `F(x_k)` and relaxation factor `ω`.
    fn next_state_into(&mut self, state: &mut [f64], residual: &[f64], omega: f64) {
        let n = state.len();
        // Map residual f = -ω·F and fixed-point map value G = x + f.
        let fval: Vec<f64> = residual.iter().map(|r| -omega * r).collect();
        let gval: Vec<f64> = state.iter().zip(&fval).map(|(x, f)| x + f).collect();

        // Push newest history differences.
        if let (Some(fp), Some(gp)) = (self.f_prev.as_ref(), self.g_prev.as_ref()) {
            let df_col: Vec<f64> = fval.iter().zip(fp).map(|(a, b)| a - b).collect();
            let dg_col: Vec<f64> = gval.iter().zip(gp).map(|(a, b)| a - b).collect();
            self.df.push_back(df_col);
            self.dg.push_back(dg_col);
            while self.df.len() > self.depth {
                self.df.pop_front();
                self.dg.pop_front();
            }
        }
        self.f_prev = Some(fval.clone());
        self.g_prev = Some(gval.clone());

        let m = self.df.len();
        if m == 0 {
            // No history yet — plain relaxed step.
            state.copy_from_slice(&gval);
            return;
        }

        // Solve the small least-squares problem for γ via regularized normal
        // equations: (ΔFᵀΔF + λI)·γ = ΔFᵀ·f_k. `m` is at most `depth` (small).
        let mut ata = DMatrix::<f64>::zeros(m, m);
        let mut atb = DVector::<f64>::zeros(m);
        for i in 0..m {
            for j in i..m {
                let mut s = 0.0;
                for k in 0..n {
                    s += self.df[i][k] * self.df[j][k];
                }
                ata[(i, j)] = s;
                ata[(j, i)] = s;
            }
            ata[(i, i)] += self.regularization;
            let mut s = 0.0;
            for k in 0..n {
                s += self.df[i][k] * fval[k];
            }
            atb[i] = s;
        }

        let gamma = match ata.clone().lu().solve(&atb) {
            Some(g) => g,
            None => {
                // Singular even with regularization — fall back to plain step.
                state.copy_from_slice(&gval);
                return;
            }
        };

        // x_{k+1} = G_k - ΔG·γ.
        for k in 0..n {
            let mut acc = gval[k];
            for (i, g) in gamma.iter().enumerate() {
                acc -= g * self.dg[i][k];
            }
            state[k] = acc;
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::solver::Solver;
    use crate::system::System;
    use std::time::Duration;

    #[test]
    fn test_picard_config_with_timeout() {
        let timeout = Duration::from_millis(250);
        let cfg = PicardConfig::default().with_timeout(timeout);
        assert_eq!(cfg.timeout, Some(timeout));
    }

    #[test]
    fn test_picard_config_default_sensible() {
        let cfg = PicardConfig::default();
        assert_eq!(cfg.max_iterations, 100);
        assert!(cfg.tolerance > 0.0 && cfg.tolerance < 1e-3);
        assert!(cfg.relaxation_factor > 0.0 && cfg.relaxation_factor <= 1.0);
    }

    #[test]
    fn test_picard_apply_relaxation_formula() {
        let mut state = vec![10.0, 20.0];
        let residuals = vec![1.0, 2.0];
        PicardConfig::apply_relaxation(&mut state, &residuals, 0.5);
        assert!((state[0] - 9.5).abs() < 1e-15);
        assert!((state[1] - 19.0).abs() < 1e-15);
    }

    #[test]
    fn test_picard_residual_norm() {
        let residuals = vec![3.0, 4.0];
        let norm = PicardConfig::residual_norm(&residuals);
        assert!((norm - 5.0).abs() < 1e-15);
    }

    #[test]
    fn test_picard_solver_trait_object() {
        let mut boxed: Box<dyn Solver> = Box::new(PicardConfig::default());
        let mut system = System::new();
        system.finalize().unwrap();
        assert!(boxed.solve(&mut system).is_err());
    }

    // ── Anderson acceleration ────────────────────────────────────────────────

    /// Reference linear residual F(x) = A·x - b. Its unique root is x* = A⁻¹·b.
    /// The relaxed Picard map is x_{k+1} = x_k - ω·(A·x_k - b).
    fn linear_residual(a: &[[f64; 2]; 2], b: &[f64; 2], x: &[f64]) -> Vec<f64> {
        vec![
            a[0][0] * x[0] + a[0][1] * x[1] - b[0],
            a[1][0] * x[0] + a[1][1] * x[1] - b[1],
        ]
    }

    fn residual_norm2(r: &[f64]) -> f64 {
        r.iter().map(|v| v * v).sum::<f64>().sqrt()
    }

    #[test]
    fn test_anderson_depth_zero_matches_plain_relaxation() {
        // With no history, next_state_into must equal x - ω·F(x).
        let mut acc = AndersonAccelerator::new(0, 1e-10);
        let mut state = vec![10.0, 20.0];
        let residuals = vec![1.0, 2.0];
        acc.next_state_into(&mut state, &residuals, 0.5);
        assert!((state[0] - 9.5).abs() < 1e-15);
        assert!((state[1] - 19.0).abs() < 1e-15);
    }

    #[test]
    fn test_anderson_converges_faster_than_plain_picard() {
        // Stiff-ish SPD system where plain relaxed Picard converges slowly.
        let a = [[8.0, 1.0], [1.0, 3.0]];
        let b = [9.0, 4.0]; // exact root x* = [1, 1]
        let omega = 0.12; // deliberately small → slow plain Picard
        let tol = 1e-9;
        let max_iter = 2000;

        let count_iters = |depth: usize| -> (usize, Vec<f64>) {
            let mut state = vec![0.0, 0.0];
            let mut acc = AndersonAccelerator::new(depth, 1e-12);
            for it in 1..=max_iter {
                let r = linear_residual(&a, &b, &state);
                if residual_norm2(&r) < tol {
                    return (it - 1, state);
                }
                acc.next_state_into(&mut state, &r, omega);
            }
            (max_iter, state)
        };

        let (plain_iters, _) = count_iters(0);
        let (anderson_iters, sol) = count_iters(3);

        // Anderson must converge, land on the true root, and use far fewer steps.
        assert!(anderson_iters < max_iter, "Anderson did not converge");
        assert!((sol[0] - 1.0).abs() < 1e-6 && (sol[1] - 1.0).abs() < 1e-6);
        assert!(
            anderson_iters * 3 < plain_iters,
            "Anderson ({}) should be much faster than plain Picard ({})",
            anderson_iters,
            plain_iters
        );
    }

    #[test]
    fn test_anderson_solves_where_plain_diverges_marginally() {
        // Anderson should still hit the exact root of a well-posed linear system.
        let a = [[4.0, 1.0], [2.0, 5.0]];
        let b = [6.0, 9.0];
        // exact root: solve → x=[1, 1.4? ] compute: 4x+y=6, 2x+5y=9
        // From first: y = 6-4x; sub: 2x+5(6-4x)=9 → 2x+30-20x=9 → -18x=-21 → x=7/6
        // y = 6-4*7/6 = 6-28/6 = 8/6 = 4/3
        let omega = 0.15;
        let mut state = vec![0.0, 0.0];
        let mut acc = AndersonAccelerator::new(4, 1e-12);
        let mut converged = false;
        for _ in 0..5000 {
            let r = linear_residual(&a, &b, &state);
            if residual_norm2(&r) < 1e-9 {
                converged = true;
                break;
            }
            acc.next_state_into(&mut state, &r, omega);
        }
        assert!(converged);
        assert!((state[0] - 7.0 / 6.0).abs() < 1e-6);
        assert!((state[1] - 4.0 / 3.0).abs() < 1e-6);
    }

    #[test]
    fn test_with_anderson_builder_sets_depth() {
        let cfg = PicardConfig::default().with_anderson(5);
        assert_eq!(cfg.anderson_depth, 5);
        // Default remains disabled.
        assert_eq!(PicardConfig::default().anderson_depth, 0);
    }
}
