//! Pluggable linear-algebra backend contract: the `LinearSolver` trait.
//!
//! ## Lifecycle (diffsol-style, FR3)
//!
//! 1. [`LinearSolver::set_problem`] — structure/setup: allocate for a problem
//!    of `n_equations × n_unknowns`. Called once per system shape.
//! 2. [`LinearSolver::set_linearisation`] — assemble the Jacobian at the new
//!    iterate (triplet format, same as `JacobianBuilder` emits: `(row, col,
//!    value)`, values accumulated with `+=` over duplicates) and compute the
//!    numeric factorization. Called per Jacobian refresh.
//! 3. [`LinearSolver::solve_in_place`] — solve `J·x = b` with the stored
//!    factorization, overwriting the caller-owned buffer `rhs` with `x`.
//!    Zero allocation on the hot path.
//!
//! The split between (2) and (3) is the typed hook the Jacobian-freezing
//! policy needs (Story 1.6 / FR4): while frozen, the solver skips
//! `set_linearisation` and reuses the factorization (and cached scaling
//! factors) across iterations.
//!
//! ## Object safety (deliberate deviation from diffsol)
//!
//! diffsol's `LinearSolver<M: Matrix>` is generic and explicitly NOT
//! dyn-compatible; runtime selection lives in the separate `diffsol-c` crate.
//! Entropyk instead requires an **object-safe** trait (`Box<dyn LinearSolver>`)
//! so the Epic-3 string-keyed strategy registry can hold heterogeneous
//! backends without a wrapper crate. Monomorphization is preserved *inside*
//! backends; only the selection boundary is dynamic.
//!
//! ## Example
//!
//! ```ignore
//! let mut lu = NalgebraLuSolver::new();
//! lu.set_problem(2, 2)?;
//! lu.set_linearisation(&[(0, 0, 2.0), (1, 1, 1.0)])?;
//! let mut rhs = vec![4.0, 3.0];
//! lu.solve_in_place(&mut rhs)?;
//! assert!((rhs[0] - 2.0).abs() < 1e-12); // x = J⁻¹·b
//! ```

use crate::SolverCoreError;

/// A pluggable linear-system backend with an explicit lifecycle (FR3).
///
/// Implementations must be `Send` so strategies can move them across rayon
/// jobs later (Epic 4). All methods are object-safe by design.
pub trait LinearSolver: Send {
    /// Allocates/reset for a problem of `n_equations × n_unknowns`.
    ///
    /// Errors: `Usage` if a dimension is zero.
    fn set_problem(&mut self, n_equations: usize, n_unknowns: usize)
        -> Result<(), SolverCoreError>;

    /// Assembles the Jacobian from `(row, col, value)` triplets (accumulating
    /// duplicates) and computes the numeric factorization at this point.
    ///
    /// Errors: `Usage` if called before `set_problem` or an index is out of
    /// bounds.
    fn set_linearisation(&mut self, entries: &[(usize, usize, f64)])
        -> Result<(), SolverCoreError>;

    /// Solves `J·x = b` with the stored factorization, overwriting `rhs` with
    /// the solution. `rhs.len()` must equal the problem size (square systems).
    ///
    /// Errors: `Usage` on shape mismatch or missing factorization;
    /// `InvalidSystem` when the factorization is singular or yields a
    /// non-finite step (the strategy maps this to its singular-Jacobian
    /// outcome).
    fn solve_in_place(&mut self, rhs: &mut [f64]) -> Result<(), SolverCoreError>;

    /// Problem size (number of rows) for wiring checks.
    fn n(&self) -> usize;
}

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

    /// Minimal hand-written 2×2 backend proving object-safety and the
    /// lifecycle error paths (no nalgebra in this crate).
    struct TinyLu {
        a: Option<[[f64; 2]; 2]>,
    }

    impl LinearSolver for TinyLu {
        fn set_problem(
            &mut self,
            n_equations: usize,
            n_unknowns: usize,
        ) -> Result<(), SolverCoreError> {
            if n_equations != 2 || n_unknowns != 2 {
                return Err(SolverCoreError::Usage {
                    message: alloc::format!("TinyLu is 2x2 only, got {n_equations}x{n_unknowns}"),
                });
            }
            self.a = None;
            Ok(())
        }

        fn set_linearisation(
            &mut self,
            entries: &[(usize, usize, f64)],
        ) -> Result<(), SolverCoreError> {
            let mut a = [[0.0; 2]; 2];
            for &(r, c, v) in entries {
                if r > 1 || c > 1 {
                    return Err(SolverCoreError::Usage {
                        message: alloc::format!("index out of bounds ({r},{c})"),
                    });
                }
                a[r][c] += v;
            }
            self.a = Some(a);
            Ok(())
        }

        fn solve_in_place(&mut self, rhs: &mut [f64]) -> Result<(), SolverCoreError> {
            let a = self.a.ok_or_else(|| SolverCoreError::Usage {
                message: alloc::string::String::from("set_linearisation not called"),
            })?;
            if rhs.len() != 2 {
                return Err(SolverCoreError::Usage {
                    message: alloc::format!("rhs len {} != 2", rhs.len()),
                });
            }
            let det = a[0][0] * a[1][1] - a[0][1] * a[1][0];
            if det == 0.0 {
                return Err(SolverCoreError::InvalidSystem {
                    message: alloc::string::String::from("singular"),
                });
            }
            let (b0, b1) = (rhs[0], rhs[1]);
            rhs[0] = (b0 * a[1][1] - b1 * a[0][1]) / det;
            rhs[1] = (a[0][0] * b1 - a[1][0] * b0) / det;
            Ok(())
        }

        fn n(&self) -> usize {
            2
        }
    }

    #[test]
    fn lifecycle_through_dyn_trait_object() {
        // Object-safety proof: the trait must be usable behind Box<dyn>.
        let mut solver: alloc::boxed::Box<dyn LinearSolver> =
            alloc::boxed::Box::new(TinyLu { a: None });
        solver.set_problem(2, 2).unwrap();
        // Solve before factorization data exists → Usage error (TinyLu has
        // none until set_linearisation; here it returns InvalidSystem-free
        // Usage via missing `a`).
        assert!(solver.solve_in_place(&mut [1.0, 2.0]).is_err());
        solver
            .set_linearisation(&[(0, 0, 2.0), (1, 1, 1.0), (0, 0, 2.0)]) // duplicate accumulates
            .unwrap();
        let mut rhs = alloc::vec![8.0, 3.0];
        solver.solve_in_place(&mut rhs).unwrap();
        assert!((rhs[0] - 2.0).abs() < 1e-12); // 4x = 8
        assert!((rhs[1] - 3.0).abs() < 1e-12);
        assert_eq!(solver.n(), 2);
    }

    #[test]
    fn lifecycle_error_paths() {
        let mut solver = TinyLu { a: None };
        // solve before set_problem → error (TinyLu has no factorization state).
        assert!(solver.solve_in_place(&mut [1.0, 2.0]).is_err());
        assert!(solver.set_problem(0, 2).is_err());
        solver.set_problem(2, 2).unwrap();
        assert!(solver.set_linearisation(&[(2, 0, 1.0)]).is_err()); // out of bounds
        solver.set_linearisation(&[(0, 0, 1.0)]).unwrap(); // rank-1 → singular
        assert!(matches!(
            solver.solve_in_place(&mut [1.0, 2.0]),
            Err(SolverCoreError::InvalidSystem { .. })
        ));
        let mut too_long: Vec<f64> = alloc::vec![1.0, 2.0, 3.0];
        assert!(solver.solve_in_place(&mut too_long).is_err());
    }
}
