//! 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, ConvergenceStatus, Solver, SolverError};
use crate::system::System;

use super::{NewtonConfig, PicardConfig};

/// 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};
/// use std::time::Duration;
///
/// let config = FallbackConfig {
///     fallback_enabled: true,
///     return_to_newton_threshold: 1e-3,
///     max_fallback_switches: 2,
/// };
///
/// 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,
}

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

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

/// 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>,
}

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

    /// 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);
        }
    }
}

/// 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,
}

impl FallbackSolver {
    /// Creates a new fallback solver with the given configuration.
    pub fn new(config: FallbackConfig) -> Self {
        Self {
            config,
            newton_config: NewtonConfig::default(),
            picard_config: PicardConfig::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
    }

    /// Sets the initial state for cold-start solving (Story 4.6 — builder pattern).
    ///
    /// Delegates to both `newton_config` and `picard_config` so the initial state
    /// is used regardless of which solver is active in the fallback loop.
    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);
        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();

        // Pre-configure solver configs once
        let mut newton_cfg = self.newton_config.clone();
        let mut picard_cfg = self.picard_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);

                    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 Ok(converged);
                }
                Err(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,
                            0, // iterations not tracked across switches
                            best_residual,
                            ConvergenceStatus::TimedOutWithBestState,
                            SimulationMetadata::new(system.input_hash()),
                        ));
                    }
                    return Err(SolverError::Timeout { timeout_ms });
                }
                Err(SolverError::Divergence { ref reason }) => {
                    // 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 result;
                    }

                    match state.current_solver {
                        CurrentSolver::Newton => {
                            // 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;
                                state.current_solver = CurrentSolver::Picard;
                                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;
                                state.current_solver = CurrentSolver::Picard;
                                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 result;
                            }
                        }
                    }
                }
                Err(SolverError::NonConvergence {
                    iterations,
                    final_residual,
                }) => {
                    // Non-convergence: check if we should try the other solver
                    if !self.config.fallback_enabled {
                        return Err(SolverError::NonConvergence {
                            iterations,
                            final_residual,
                        });
                    }

                    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;
                                state.current_solver = CurrentSolver::Picard;
                                tracing::info!(
                                    switch_count = state.switch_count,
                                    "Max switches reached, committing to Picard permanently"
                                );
                            } else {
                                state.switch_count += 1;
                                state.current_solver = CurrentSolver::Picard;
                                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,
                                });
                            }

                            // Check if residual is low enough to try Newton
                            if final_residual < self.config.return_to_newton_threshold {
                                state.switch_count += 1;
                                state.current_solver = CurrentSolver::Newton;
                                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,
                                });
                            }
                        }
                    }
                }
                Err(other) => {
                    // InvalidSystem or other errors - propagate immediately
                    return Err(other);
                }
            }
        }
    }
}

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,
            "Fallback solver starting"
        );

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

    fn with_timeout(mut self, timeout: Duration) -> Self {
        self.newton_config.timeout = Some(timeout);
        self.picard_config.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,
        };
        let solver = FallbackSolver::new(config.clone());
        assert_eq!(solver.config, config);
    }

    #[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());
    }
}
