//! Intelligent fallback solver implementation.
//!
//! This module provides the [`FallbackSolver`] which implements an intelligent
//! fallback strategy between Newton-Raphson and Sequential Substitution (Picard).
//!
//! # Strategy
//!
//! The fallback solver implements the following algorithm:
//!
//! 1. Start with Newton-Raphson (quadratic convergence)
//! 2. If Newton diverges, switch to Picard (more robust)
//! 3. If Picard stabilizes (residual < threshold), try returning to Newton
//! 4. If max switches reached, stay on current solver permanently
//!
//! # Features
//!
//! - Automatic fallback from Newton to Picard on divergence
//! - Return to Newton when Picard stabilizes the solution
//! - Maximum switch limit to prevent infinite oscillation
//! - Time-budgeted solving with graceful degradation (Story 4.5)
//! - Smart initialization support (Story 4.6)
//! - Multi-circuit convergence criteria (Story 4.7)

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

use crate::criteria::ConvergenceCriteria;
use crate::metadata::SimulationMetadata;
use crate::solver::{
    ConvergedState, ConvergenceDiagnostics, ConvergenceStatus, Solver, SolverError,
    SolverSwitchEvent, SolverType, SwitchReason, VerboseConfig,
};
use crate::system::System;

use super::{HomotopyConfig, NewtonConfig, PicardConfig, PtcConfig, TrustRegionConfig};

/// Configuration for the intelligent fallback solver.
///
/// The fallback solver starts with Newton-Raphson (quadratic convergence) and
/// automatically switches to Sequential Substitution (Picard) if Newton diverges.
/// It can return to Newton when Picard stabilizes the solution.
///
/// # Example
///
/// ```rust
/// use entropyk_solver::solver::{FallbackConfig, FallbackSolver, Solver, VerboseConfig};
/// use std::time::Duration;
///
/// let config = FallbackConfig {
///     fallback_enabled: true,
///     return_to_newton_threshold: 1e-3,
///     max_fallback_switches: 2,
///     verbose_config: VerboseConfig::default(),
/// };
///
/// let solver = FallbackSolver::new(config)
///     .with_timeout(Duration::from_secs(1));
/// ```
#[derive(Debug, Clone, PartialEq)]
pub struct FallbackConfig {
    /// Enable automatic fallback from Newton to Picard on divergence.
    ///
    /// When `true` (default), the solver switches to Picard if Newton diverges.
    /// When `false`, the solver runs pure Newton or Picard without fallback.
    pub fallback_enabled: bool,

    /// Residual norm threshold for returning to Newton from Picard.
    ///
    /// When Picard reduces the residual below this threshold, the solver
    /// attempts to return to Newton for faster convergence.
    /// Default: $10^{-3}$.
    pub return_to_newton_threshold: f64,

    /// Maximum number of solver switches before staying on current solver.
    ///
    /// Prevents infinite oscillation between Newton and Picard.
    /// Default: 2.
    pub max_fallback_switches: usize,

    /// Verbose mode configuration for diagnostics.
    pub verbose_config: VerboseConfig,
}

impl Default for FallbackConfig {
    fn default() -> Self {
        Self {
            fallback_enabled: true,
            return_to_newton_threshold: 1e-3,
            max_fallback_switches: 2,
            verbose_config: VerboseConfig::default(),
        }
    }
}

/// Tracks which solver is currently active in the fallback loop.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CurrentSolver {
    Newton,
    Picard,
}

impl From<CurrentSolver> for SolverType {
    fn from(solver: CurrentSolver) -> Self {
        match solver {
            CurrentSolver::Newton => SolverType::NewtonRaphson,
            CurrentSolver::Picard => SolverType::SequentialSubstitution,
        }
    }
}

/// Internal state for the fallback solver.
struct FallbackState {
    current_solver: CurrentSolver,
    switch_count: usize,
    /// Whether we've permanently committed to Picard (after max switches or Newton re-divergence)
    committed_to_picard: bool,
    /// Best state encountered across all solver invocations (Story 4.5 - AC: #4)
    best_state: Option<Vec<f64>>,
    /// Best residual norm across all solver invocations (Story 4.5 - AC: #4)
    best_residual: Option<f64>,
    /// Total iterations across all solver invocations
    total_iterations: usize,
    /// Solver switch events for diagnostics (Story 7.4)
    switch_events: Vec<SolverSwitchEvent>,
}

impl FallbackState {
    fn new() -> Self {
        Self {
            current_solver: CurrentSolver::Newton,
            switch_count: 0,
            committed_to_picard: false,
            best_state: None,
            best_residual: None,
            total_iterations: 0,
            switch_events: Vec::new(),
        }
    }

    /// Update best state if the given residual is better (Story 4.5 - AC: #4).
    fn update_best_state(&mut self, state: &[f64], residual: f64) {
        if self.best_residual.is_none() || residual < self.best_residual.unwrap() {
            self.best_state = Some(state.to_vec());
            self.best_residual = Some(residual);
        }
    }

    /// Record a solver switch event (Story 7.4)
    fn record_switch(
        &mut self,
        from: CurrentSolver,
        to: CurrentSolver,
        reason: SwitchReason,
        residual_at_switch: f64,
    ) {
        self.switch_events.push(SolverSwitchEvent {
            from_solver: from.into(),
            to_solver: to.into(),
            reason,
            iteration: self.total_iterations,
            residual_at_switch,
        });
    }
}

/// Intelligent fallback solver that switches between Newton-Raphson and Picard.
///
/// The fallback solver implements the following algorithm:
///
/// 1. Start with Newton-Raphson (quadratic convergence)
/// 2. If Newton diverges, switch to Picard (more robust)
/// 3. If Picard stabilizes (residual < threshold), try returning to Newton
/// 4. If max switches reached, stay on current solver permanently
///
/// # Timeout Handling
///
/// The timeout applies to the total solving time across all solver switches.
/// Each solver inherits the remaining time budget.
///
/// # Pre-Allocated Buffers
///
/// All buffers are pre-allocated once before the fallback loop to avoid
/// heap allocation during solver switches (NFR4).
#[derive(Debug, Clone)]
pub struct FallbackSolver {
    /// Fallback behavior configuration.
    pub config: FallbackConfig,
    /// Newton-Raphson configuration.
    pub newton_config: NewtonConfig,
    /// Sequential Substitution (Picard) configuration.
    pub picard_config: PicardConfig,
    /// Optional Newton-homotopy continuation used as a last-resort recovery.
    ///
    /// When set, the homotopy solver is invoked if both Newton and Picard fail
    /// (divergence or non-convergence) from the cold start. This mirrors IPM
    /// BOLT's escalating initialization cascade (`GLBL` → `iGLBL` → `PRVS` →
    /// `iPRVS`): cheap solvers first, robust continuation only when needed.
    /// `None` (default) preserves the original Newton↔Picard-only behaviour.
    pub homotopy_config: Option<HomotopyConfig>,
    /// Optional pseudo-transient continuation (PTC) globalization stage.
    ///
    /// Invoked when the primary Newton/Picard stages fail, before the homotopy
    /// stage: PTC regularizes the Newton step with a diagonal δ⁻¹ shift and
    /// grows the timestep as the residual shrinks (Kelley-Keyes 1998). It
    /// targets exactly the line-search stagnation observed on fully-coupled
    /// systems with two-phase pressure drops at high EXV opening. `Some` by
    /// default (it only runs on primary failure); `None` disables the stage.
    pub ptc_config: Option<PtcConfig>,
    /// Optional Powell dogleg trust-region globalization stage.
    ///
    /// Invoked when the primary Newton/Picard stages *and* PTC have failed,
    /// before the homotopy stage. Trust-region escapes the local-minimum-of-‖F‖
    /// trap by blending Newton and Cauchy steps inside a trust ball of radius
    /// δ, accepting steps based on the gain ratio ρ of actual vs predicted
    /// ‖r‖ reduction. It targets the exact failure mode where PTC's timestep
    /// collapses (Newton step almost orthogonal to ∇‖F‖, but still close to a
    /// root). `Some` by default; `None` disables the stage.
    pub trust_region_config: Option<TrustRegionConfig>,
}

impl FallbackSolver {
    /// Creates a new fallback solver with the given configuration.
    ///
    /// The Picard fallback stage is configured with Anderson acceleration
    /// (depth 3) by default, which converges the fixed-point iteration
    /// super-linearly and typically halves the fallback iteration count without
    /// affecting robustness. Override via [`Self::with_picard_config`].
    pub fn new(config: FallbackConfig) -> Self {
        Self {
            config,
            newton_config: NewtonConfig::default(),
            picard_config: PicardConfig::default().with_anderson(3),
            homotopy_config: None,
            ptc_config: Some(PtcConfig::default()),
            trust_region_config: Some(TrustRegionConfig::default()),
        }
    }

    /// Creates a fallback solver with default configuration.
    pub fn default_solver() -> Self {
        Self::new(FallbackConfig::default())
    }

    /// Sets custom Newton-Raphson configuration.
    pub fn with_newton_config(mut self, config: NewtonConfig) -> Self {
        self.newton_config = config;
        self
    }

    /// Sets custom Picard configuration.
    pub fn with_picard_config(mut self, config: PicardConfig) -> Self {
        self.picard_config = config;
        self
    }

    /// Enables Newton-homotopy continuation as a last-resort recovery stage.
    ///
    /// If both Newton and Picard fail from the cold start, the homotopy solver
    /// is invoked to walk in from `λ = 0` to `λ = 1` (see [`HomotopyConfig`]).
    /// When the supplied config has no `initial_state`, the fallback solver's
    /// Newton initial state is reused so all stages share the same cold start.
    pub fn with_homotopy(mut self, config: HomotopyConfig) -> Self {
        self.homotopy_config = Some(config);
        self
    }

    /// Sets a custom pseudo-transient continuation configuration, or disables
    /// the stage with `None`. See [`PtcConfig`] for the stage semantics.
    pub fn with_ptc(mut self, config: Option<PtcConfig>) -> Self {
        self.ptc_config = config;
        self
    }

    /// Sets a custom Powell dogleg trust-region configuration, or disables the
    /// stage with `None`. See [`TrustRegionConfig`] for the stage semantics.
    pub fn with_trust_region(mut self, config: Option<TrustRegionConfig>) -> Self {
        self.trust_region_config = config;
        self
    }

    /// Sets the initial state for cold-start solving (Story 4.6 — builder pattern).
    ///
    /// Delegates to `newton_config`, `picard_config`, and the optional homotopy
    /// stage so the initial state is used regardless of which solver runs.
    pub fn with_initial_state(mut self, state: Vec<f64>) -> Self {
        self.newton_config.initial_state = Some(state.clone());
        self.picard_config.initial_state = Some(state.clone());
        if let Some(ref mut p) = self.ptc_config {
            p.initial_state = Some(state.clone());
        }
        if let Some(ref mut tr) = self.trust_region_config {
            tr.initial_state = Some(state.clone());
        }
        if let Some(ref mut h) = self.homotopy_config {
            h.initial_state = Some(state);
        }
        self
    }

    /// Sets multi-circuit convergence criteria (Story 4.7 — builder pattern).
    ///
    /// Delegates to both `newton_config` and `picard_config` so criteria are
    /// applied regardless of which solver is active in the fallback loop.
    pub fn with_convergence_criteria(mut self, criteria: ConvergenceCriteria) -> Self {
        self.newton_config.convergence_criteria = Some(criteria.clone());
        self.picard_config.convergence_criteria = Some(criteria);
        self
    }

    /// Main fallback solving loop.
    ///
    /// Implements the intelligent fallback algorithm:
    /// - Start with Newton-Raphson
    /// - Switch to Picard on Newton divergence
    /// - Return to Newton when Picard stabilizes (if under switch limit and residual below threshold)
    /// - Stay on Picard permanently after max switches or if Newton re-diverges
    fn solve_with_fallback(
        &mut self,
        system: &mut System,
        start_time: Instant,
        timeout: Option<Duration>,
    ) -> Result<ConvergedState, SolverError> {
        let mut state = FallbackState::new();

        // Verbose mode setup
        let verbose_enabled =
            self.config.verbose_config.enabled && self.config.verbose_config.is_any_enabled();
        let mut diagnostics = if verbose_enabled {
            Some(ConvergenceDiagnostics::with_capacity(100))
        } else {
            None
        };

        // Pre-configure solver configs once
        let mut newton_cfg = self.newton_config.clone();
        let mut picard_cfg = self.picard_config.clone();

        // Propagate verbose config to child solvers
        newton_cfg.verbose_config = self.config.verbose_config.clone();
        picard_cfg.verbose_config = self.config.verbose_config.clone();

        loop {
            // Check remaining time budget
            let remaining = timeout.map(|t| t.saturating_sub(start_time.elapsed()));

            // Check for timeout before running solver
            if let Some(remaining_time) = remaining {
                if remaining_time.is_zero() {
                    return Err(SolverError::Timeout {
                        timeout_ms: timeout.unwrap().as_millis() as u64,
                    });
                }
            }

            // Run current solver with remaining time
            newton_cfg.timeout = remaining;
            picard_cfg.timeout = remaining;

            let result = match state.current_solver {
                CurrentSolver::Newton => newton_cfg.solve(system),
                CurrentSolver::Picard => picard_cfg.solve(system),
            };

            match result {
                Ok(converged) => {
                    // Update best state tracking (Story 4.5 - AC: #4)
                    state.update_best_state(&converged.state, converged.final_residual);
                    state.total_iterations += converged.iterations;

                    // Finalize diagnostics
                    if let Some(ref mut diag) = diagnostics {
                        diag.iterations = state.total_iterations;
                        diag.final_residual = converged.final_residual;
                        diag.best_residual =
                            state.best_residual.unwrap_or(converged.final_residual);
                        diag.converged = true;
                        diag.timing_ms = start_time.elapsed().as_millis() as u64;
                        diag.final_solver = Some(state.current_solver.into());
                        diag.solver_switches = state.switch_events.clone();

                        // Merge iteration history from child solver if available
                        if let Some(ref child_diag) = converged.diagnostics {
                            diag.iteration_history = child_diag.iteration_history.clone();
                        }

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

                    tracing::info!(
                        solver = match state.current_solver {
                            CurrentSolver::Newton => "NewtonRaphson",
                            CurrentSolver::Picard => "Picard",
                        },
                        iterations = converged.iterations,
                        final_residual = converged.final_residual,
                        switch_count = state.switch_count,
                        "Fallback solver converged"
                    );

                    // Return with diagnostics if verbose mode enabled
                    return Ok(if let Some(d) = diagnostics {
                        ConvergedState {
                            diagnostics: Some(d),
                            ..converged
                        }
                    } else {
                        converged
                    });
                }
                Err(err) => {
                    let child_diagnostics = err.diagnostics().cloned();
                    match err.without_diagnostics() {
                        SolverError::Timeout { timeout_ms } => {
                            // Story 4.5 - AC: #4: Return best state on timeout if available
                            if let (Some(best_state), Some(best_residual)) =
                                (state.best_state.clone(), state.best_residual)
                            {
                                tracing::info!(
                                    best_residual = best_residual,
                                    "Returning best state across all solver invocations on timeout"
                                );
                                return Ok(ConvergedState::new(
                                    best_state,
                                    state.total_iterations,
                                    best_residual,
                                    ConvergenceStatus::TimedOutWithBestState,
                                    SimulationMetadata::new(system.input_hash()),
                                ));
                            }
                            return Err(SolverError::Timeout { timeout_ms }
                                .with_optional_diagnostics(child_diagnostics));
                        }
                        divergence_like @ (SolverError::Divergence { .. }
                        | SolverError::DomainViolation(_)) => {
                            // Recoverable-exhaustion failures (DomainViolation)
                            // are recovery-eligible, like divergence: switch
                            // strategy or attempt the PTC/trust-region/homotopy
                            // cascade instead of propagating immediately.
                            let reason =
                                if let SolverError::Divergence { reason } = &divergence_like {
                                    reason.clone()
                                } else {
                                    divergence_like.to_string()
                                };
                            // Handle divergence based on current solver and state
                            if !self.config.fallback_enabled {
                                tracing::info!(
                                    solver = match state.current_solver {
                                        CurrentSolver::Newton => "NewtonRaphson",
                                        CurrentSolver::Picard => "Picard",
                                    },
                                    reason = reason,
                                    "Divergence detected, fallback disabled"
                                );
                                return Err(
                                    divergence_like.with_optional_diagnostics(child_diagnostics)
                                );
                            }

                            match state.current_solver {
                                CurrentSolver::Newton => {
                                    // Get residual from error context (use best known)
                                    let residual_at_switch =
                                        state.best_residual.unwrap_or(f64::MAX);

                                    // Newton diverged - switch to Picard (stay there permanently after max switches)
                                    if state.switch_count >= self.config.max_fallback_switches {
                                        // Max switches reached - commit to Picard permanently
                                        state.committed_to_picard = true;
                                        let prev_solver = state.current_solver;
                                        state.current_solver = CurrentSolver::Picard;

                                        // Record switch event
                                        state.record_switch(
                                            prev_solver,
                                            state.current_solver,
                                            SwitchReason::Divergence,
                                            residual_at_switch,
                                        );

                                        // Verbose logging
                                        if verbose_enabled
                                            && self.config.verbose_config.log_solver_switches
                                        {
                                            tracing::info!(
                                                from = "NewtonRaphson",
                                                to = "Picard",
                                                reason = "divergence",
                                                switch_count = state.switch_count,
                                                residual = residual_at_switch,
                                                "Solver switch (max switches reached)"
                                            );
                                        }

                                        tracing::info!(
                                    switch_count = state.switch_count,
                                    max_switches = self.config.max_fallback_switches,
                                    "Max switches reached, committing to Picard permanently"
                                );
                                    } else {
                                        // Switch to Picard
                                        state.switch_count += 1;
                                        let prev_solver = state.current_solver;
                                        state.current_solver = CurrentSolver::Picard;

                                        // Record switch event
                                        state.record_switch(
                                            prev_solver,
                                            state.current_solver,
                                            SwitchReason::Divergence,
                                            residual_at_switch,
                                        );

                                        // Verbose logging
                                        if verbose_enabled
                                            && self.config.verbose_config.log_solver_switches
                                        {
                                            tracing::info!(
                                                from = "NewtonRaphson",
                                                to = "Picard",
                                                reason = "divergence",
                                                switch_count = state.switch_count,
                                                residual = residual_at_switch,
                                                "Solver switch"
                                            );
                                        }

                                        tracing::warn!(
                                            switch_count = state.switch_count,
                                            reason = reason,
                                            "Newton diverged, switching to Picard"
                                        );
                                    }
                                    // Continue loop with Picard
                                }
                                CurrentSolver::Picard => {
                                    // Picard diverged - if we were trying Newton again, commit to Picard permanently
                                    if state.switch_count > 0 && !state.committed_to_picard {
                                        state.committed_to_picard = true;
                                        tracing::info!(
                                    switch_count = state.switch_count,
                                    reason = reason,
                                    "Newton re-diverged after return from Picard, staying on Picard permanently"
                                );
                                        // Stay on Picard and try again
                                    } else {
                                        // Picard diverged with no return attempt - no more fallbacks available
                                        tracing::warn!(
                                            reason = reason,
                                            "Picard diverged, no more fallbacks available"
                                        );
                                        return Err(divergence_like
                                            .with_optional_diagnostics(child_diagnostics));
                                    }
                                }
                            }
                        }
                        SolverError::NonConvergence {
                            iterations,
                            final_residual,
                        } => {
                            state.total_iterations += iterations;

                            // Non-convergence: check if we should try the other solver
                            if !self.config.fallback_enabled {
                                return Err(SolverError::NonConvergence {
                                    iterations,
                                    final_residual,
                                }
                                .with_optional_diagnostics(child_diagnostics));
                            }

                            match state.current_solver {
                                CurrentSolver::Newton => {
                                    // Newton didn't converge - try Picard
                                    if state.switch_count >= self.config.max_fallback_switches {
                                        // Max switches reached - commit to Picard permanently
                                        state.committed_to_picard = true;
                                        let prev_solver = state.current_solver;
                                        state.current_solver = CurrentSolver::Picard;

                                        // Record switch event
                                        state.record_switch(
                                            prev_solver,
                                            state.current_solver,
                                            SwitchReason::SlowConvergence,
                                            final_residual,
                                        );

                                        // Verbose logging
                                        if verbose_enabled
                                            && self.config.verbose_config.log_solver_switches
                                        {
                                            tracing::info!(
                                                from = "NewtonRaphson",
                                                to = "Picard",
                                                reason = "slow_convergence",
                                                switch_count = state.switch_count,
                                                residual = final_residual,
                                                "Solver switch (max switches reached)"
                                            );
                                        }

                                        tracing::info!(
                                    switch_count = state.switch_count,
                                    "Max switches reached, committing to Picard permanently"
                                );
                                    } else {
                                        state.switch_count += 1;
                                        let prev_solver = state.current_solver;
                                        state.current_solver = CurrentSolver::Picard;

                                        // Record switch event
                                        state.record_switch(
                                            prev_solver,
                                            state.current_solver,
                                            SwitchReason::SlowConvergence,
                                            final_residual,
                                        );

                                        // Verbose logging
                                        if verbose_enabled
                                            && self.config.verbose_config.log_solver_switches
                                        {
                                            tracing::info!(
                                                from = "NewtonRaphson",
                                                to = "Picard",
                                                reason = "slow_convergence",
                                                switch_count = state.switch_count,
                                                residual = final_residual,
                                                "Solver switch"
                                            );
                                        }

                                        tracing::info!(
                                            switch_count = state.switch_count,
                                            iterations = iterations,
                                            final_residual = final_residual,
                                            "Newton did not converge, switching to Picard"
                                        );
                                    }
                                    // Continue loop with Picard
                                }
                                CurrentSolver::Picard => {
                                    // Picard didn't converge - check if we should try Newton
                                    if state.committed_to_picard
                                        || state.switch_count >= self.config.max_fallback_switches
                                    {
                                        tracing::info!(
                                            iterations = iterations,
                                            final_residual = final_residual,
                                            "Picard did not converge, no more fallbacks"
                                        );
                                        return Err(SolverError::NonConvergence {
                                            iterations,
                                            final_residual,
                                        }
                                        .with_optional_diagnostics(child_diagnostics));
                                    }

                                    // Check if residual is low enough to try Newton
                                    if final_residual < self.config.return_to_newton_threshold {
                                        state.switch_count += 1;
                                        let prev_solver = state.current_solver;
                                        state.current_solver = CurrentSolver::Newton;

                                        // Record switch event
                                        state.record_switch(
                                            prev_solver,
                                            state.current_solver,
                                            SwitchReason::ReturnToNewton,
                                            final_residual,
                                        );

                                        // Verbose logging
                                        if verbose_enabled
                                            && self.config.verbose_config.log_solver_switches
                                        {
                                            tracing::info!(
                                                from = "Picard",
                                                to = "NewtonRaphson",
                                                reason = "return_to_newton",
                                                switch_count = state.switch_count,
                                                residual = final_residual,
                                                threshold = self.config.return_to_newton_threshold,
                                                "Solver switch (Picard stabilized)"
                                            );
                                        }

                                        tracing::info!(
                                            switch_count = state.switch_count,
                                            final_residual = final_residual,
                                            threshold = self.config.return_to_newton_threshold,
                                            "Picard stabilized, attempting Newton return"
                                        );
                                        // Continue loop with Newton
                                    } else {
                                        // Stay on Picard and keep trying
                                        tracing::debug!(
                                            final_residual = final_residual,
                                            threshold = self.config.return_to_newton_threshold,
                                            "Picard not yet stabilized, aborting"
                                        );
                                        return Err(SolverError::NonConvergence {
                                            iterations,
                                            final_residual,
                                        }
                                        .with_optional_diagnostics(child_diagnostics));
                                    }
                                }
                            }
                        }
                        other => {
                            // InvalidSystem or other errors - propagate immediately
                            return Err(other.with_optional_diagnostics(child_diagnostics));
                        }
                    }
                }
            }
        }
    }

    /// Attempts pseudo-transient continuation after the primary Newton/Picard
    /// stages have failed, before falling back to homotopy continuation.
    ///
    /// Returns the PTC result on success; otherwise returns the *primary* error
    /// (the PTC failure is logged but not surfaced, like the homotopy stage).
    /// Structural `InvalidSystem` errors are never retried.
    fn try_ptc_recovery(
        &self,
        system: &mut System,
        primary_err: SolverError,
        remaining: Option<Duration>,
    ) -> Result<ConvergedState, SolverError> {
        if matches!(primary_err.base_error(), SolverError::InvalidSystem { .. }) {
            return Err(primary_err);
        }

        let Some(mut ptc) = self.ptc_config.clone() else {
            return Err(primary_err);
        };

        // Share the cold start with the primary solvers unless explicitly set.
        if ptc.initial_state.is_none() {
            ptc.initial_state = self.newton_config.initial_state.clone();
        }
        // Clamp the stage budget to the remaining global time budget: the
        // cascade shares ONE time budget across all stages — the tighter of
        // the stage's own timeout and what is left of the global budget wins.
        ptc.timeout = match (ptc.timeout, remaining) {
            (Some(t), Some(rem)) => Some(t.min(rem)),
            (None, rem) => rem,
            (t, None) => t,
        };

        tracing::info!(
            error = %primary_err,
            "Primary solvers failed; attempting pseudo-transient continuation"
        );

        match ptc.solve(system) {
            Ok(converged) => {
                tracing::info!(
                    iterations = converged.iterations,
                    final_residual = converged.final_residual,
                    "PTC recovered convergence"
                );
                Ok(converged)
            }
            Err(ptc_err) => {
                tracing::warn!(
                    error = %ptc_err,
                    "PTC also failed; returning primary error"
                );
                Err(primary_err)
            }
        }
    }

    /// Attempts the Powell dogleg trust-region stage after PTC has failed,
    /// before the homotopy stage. Trust-region is more effective than PTC when
    /// Newton stagnates *near* a root (PTC's timestep collapses there because
    /// the regularized step is too short to make progress).
    ///
    /// Returns the trust-region result on success; otherwise returns the
    /// *primary* error (the trust-region failure is logged but not surfaced,
    /// like the homotopy stage). Structural `InvalidSystem` errors are never
    /// retried.
    fn try_trust_region_recovery(
        &self,
        system: &mut System,
        primary_err: SolverError,
        remaining: Option<Duration>,
    ) -> Result<ConvergedState, SolverError> {
        if matches!(primary_err.base_error(), SolverError::InvalidSystem { .. }) {
            return Err(primary_err);
        }

        let Some(mut tr) = self.trust_region_config.clone() else {
            return Err(primary_err);
        };

        // Share the cold start with the primary solvers unless explicitly set.
        if tr.initial_state.is_none() {
            tr.initial_state = self.newton_config.initial_state.clone();
        }
        // Clamp the stage budget to the remaining global time budget: the
        // cascade shares ONE time budget across all stages — the tighter of
        // the stage's own timeout and what is left of the global budget wins.
        tr.timeout = match (tr.timeout, remaining) {
            (Some(t), Some(rem)) => Some(t.min(rem)),
            (None, rem) => rem,
            (t, None) => t,
        };

        tracing::info!(
            error = %primary_err,
            "PTC failed; attempting Powell dogleg trust-region continuation"
        );

        match tr.solve(system) {
            Ok(converged) => {
                tracing::info!(
                    iterations = converged.iterations,
                    final_residual = converged.final_residual,
                    "Trust-region continuation recovered convergence"
                );
                Ok(converged)
            }
            Err(tr_err) => {
                tracing::warn!(
                    error = %tr_err,
                    "Trust-region continuation also failed; returning primary error"
                );
                Err(primary_err)
            }
        }
    }

    /// Attempts Newton-homotopy continuation as a last-resort recovery after the
    /// primary Newton/Picard stages have failed.
    ///
    /// Returns the homotopy result on success; otherwise returns the *primary*
    /// error (the homotopy failure is logged but not surfaced, since the primary
    /// error is the more actionable diagnostic). Structural `InvalidSystem`
    /// errors are never retried — they indicate a malformed model, not a hard
    /// cold start.
    fn try_homotopy_recovery(
        &self,
        system: &mut System,
        primary_err: SolverError,
        remaining: Option<Duration>,
    ) -> Result<ConvergedState, SolverError> {
        if matches!(primary_err.base_error(), SolverError::InvalidSystem { .. }) {
            return Err(primary_err);
        }

        let Some(mut homotopy) = self.homotopy_config.clone() else {
            return Err(primary_err);
        };

        // Share the cold start with the primary solvers unless explicitly set.
        if homotopy.initial_state.is_none() {
            homotopy.initial_state = self.newton_config.initial_state.clone();
        }
        // Clamp the stage budget to the remaining global time budget: the
        // cascade shares ONE time budget across all stages — the tighter of
        // the stage's own timeout and what is left of the global budget wins.
        homotopy.timeout = match (homotopy.timeout, remaining) {
            (Some(t), Some(rem)) => Some(t.min(rem)),
            (None, rem) => rem,
            (t, None) => t,
        };

        tracing::info!(
            error = %primary_err,
            "Primary solvers failed; attempting Newton-homotopy continuation as last resort"
        );

        match homotopy.solve(system) {
            Ok(converged) => {
                tracing::info!(
                    iterations = converged.iterations,
                    final_residual = converged.final_residual,
                    "Homotopy continuation recovered convergence"
                );
                Ok(converged)
            }
            Err(homotopy_err) => {
                let primary_diagnostics = primary_err.diagnostics().cloned();
                let homotopy_diagnostics = homotopy_err.diagnostics().cloned();
                let diagnostics = match (primary_diagnostics, homotopy_diagnostics) {
                    (Some(primary), Some(homotopy)) => {
                        if homotopy.iterations >= primary.iterations {
                            Some(homotopy)
                        } else {
                            Some(primary)
                        }
                    }
                    (Some(primary), None) => Some(primary),
                    (None, Some(homotopy)) => Some(homotopy),
                    (None, None) => None,
                };
                tracing::warn!(
                    error = %homotopy_err,
                    "Homotopy continuation also failed; returning primary error"
                );
                Err(primary_err
                    .without_diagnostics()
                    .with_optional_diagnostics(diagnostics))
            }
        }
    }
}

impl Solver for FallbackSolver {
    fn solve(&mut self, system: &mut System) -> Result<ConvergedState, SolverError> {
        let start_time = Instant::now();
        let timeout = self.newton_config.timeout.or(self.picard_config.timeout);

        tracing::info!(
            fallback_enabled = self.config.fallback_enabled,
            return_to_newton_threshold = self.config.return_to_newton_threshold,
            max_fallback_switches = self.config.max_fallback_switches,
            homotopy_recovery = self.homotopy_config.is_some(),
            "Fallback solver starting"
        );

        let primary = if self.config.fallback_enabled {
            self.solve_with_fallback(system, start_time, timeout)
        } else {
            // Fallback disabled - run pure Newton
            self.newton_config.solve(system)
        };

        match primary {
            Ok(converged) => Ok(converged),
            Err(primary_err) => {
                let remaining = timeout.map(|t| t.saturating_sub(start_time.elapsed()));
                match self.try_ptc_recovery(system, primary_err.clone(), remaining) {
                    Ok(converged) => Ok(converged),
                    Err(ptc_err) => {
                        let remaining = timeout.map(|t| t.saturating_sub(start_time.elapsed()));
                        match self.try_trust_region_recovery(system, ptc_err.clone(), remaining) {
                            Ok(converged) => Ok(converged),
                            Err(tr_err) => {
                                let remaining =
                                    timeout.map(|t| t.saturating_sub(start_time.elapsed()));
                                self.try_homotopy_recovery(system, tr_err, remaining)
                            }
                        }
                    }
                }
            }
        }
    }

    fn with_timeout(mut self, timeout: Duration) -> Self {
        self.newton_config.timeout = Some(timeout);
        self.picard_config.timeout = Some(timeout);
        if let Some(ref mut p) = self.ptc_config {
            p.timeout = Some(timeout);
        }
        if let Some(ref mut tr) = self.trust_region_config {
            tr.timeout = Some(timeout);
        }
        if let Some(ref mut h) = self.homotopy_config {
            h.timeout = Some(timeout);
        }
        self
    }
}

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

    #[test]
    fn test_fallback_config_defaults() {
        let cfg = FallbackConfig::default();
        assert!(cfg.fallback_enabled);
        assert!((cfg.return_to_newton_threshold - 1e-3).abs() < 1e-15);
        assert_eq!(cfg.max_fallback_switches, 2);
    }

    #[test]
    fn test_fallback_solver_new() {
        let config = FallbackConfig {
            fallback_enabled: false,
            return_to_newton_threshold: 5e-4,
            max_fallback_switches: 3,
            ..Default::default()
        };
        let solver = FallbackSolver::new(config.clone());
        assert_eq!(solver.config.fallback_enabled, config.fallback_enabled);
        assert_eq!(solver.config.return_to_newton_threshold, 5e-4);
        assert_eq!(solver.config.max_fallback_switches, 3);
    }

    #[test]
    fn test_fallback_solver_with_timeout() {
        let timeout = Duration::from_millis(500);
        let solver = FallbackSolver::default_solver().with_timeout(timeout);
        assert_eq!(solver.newton_config.timeout, Some(timeout));
        assert_eq!(solver.picard_config.timeout, Some(timeout));
    }

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

    // ── Homotopy last-resort recovery wiring ──────────────────────────────────

    #[test]
    fn test_fallback_homotopy_disabled_by_default() {
        let solver = FallbackSolver::default_solver();
        assert!(solver.homotopy_config.is_none());
    }

    #[test]
    fn test_fallback_with_homotopy_sets_config() {
        let solver = FallbackSolver::default_solver()
            .with_homotopy(HomotopyConfig::default().with_initial_steps(20));
        let h = solver
            .homotopy_config
            .expect("homotopy should be configured");
        assert_eq!(h.initial_steps, 20);
    }

    #[test]
    fn test_with_initial_state_propagates_to_homotopy() {
        let solver = FallbackSolver::default_solver()
            .with_homotopy(HomotopyConfig::default())
            .with_initial_state(vec![1.0, 2.0, 3.0]);
        assert_eq!(
            solver.newton_config.initial_state,
            Some(vec![1.0, 2.0, 3.0])
        );
        assert_eq!(
            solver.picard_config.initial_state,
            Some(vec![1.0, 2.0, 3.0])
        );
        assert_eq!(
            solver.homotopy_config.unwrap().initial_state,
            Some(vec![1.0, 2.0, 3.0])
        );
    }

    #[test]
    fn test_with_timeout_propagates_to_homotopy() {
        let timeout = Duration::from_millis(750);
        let solver = FallbackSolver::default_solver()
            .with_homotopy(HomotopyConfig::default())
            .with_timeout(timeout);
        assert_eq!(solver.homotopy_config.unwrap().timeout, Some(timeout));
    }

    #[test]
    fn test_invalid_system_not_retried_by_homotopy() {
        // An empty (degenerate) system yields InvalidSystem; the homotopy stage
        // must NOT retry it — a malformed model is not a hard cold start.
        let mut solver = FallbackSolver::default_solver().with_homotopy(HomotopyConfig::default());
        let mut system = System::new();
        system.finalize().unwrap();
        assert!(solver.solve(&mut system).is_err());
    }
}
