//! WASM solver bindings.
//!
//! Provides JavaScript-friendly wrappers for the solver and system.

use crate::backend::global_backend;
use crate::components::WasmComponent;
use crate::types::{WasmConvergedState, WasmThermoState};
use entropyk_fluids::FluidBackend;
use entropyk_solver::{
    ConvergedState, ConvergenceStatus, FallbackConfig, FallbackSolver, NewtonConfig, PicardConfig,
    Solver, SolverStrategy, System,
};
use petgraph::graph::NodeIndex;
use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;

/// WASM wrapper for Newton-Raphson solver configuration.
#[wasm_bindgen]
#[derive(Clone, Debug)]
pub struct WasmNewtonConfig {
    pub(crate) inner: NewtonConfig,
}

#[wasm_bindgen]
impl WasmNewtonConfig {
    /// Create default Newton-Raphson configuration.
    #[wasm_bindgen(constructor)]
    pub fn new() -> Self {
        WasmNewtonConfig {
            inner: NewtonConfig::default(),
        }
    }

    /// Set maximum iterations.
    pub fn set_max_iterations(&mut self, max: usize) {
        self.inner.max_iterations = max;
    }

    /// Set convergence tolerance.
    pub fn set_tolerance(&mut self, tol: f64) {
        self.inner.tolerance = tol;
    }
}

impl Default for WasmNewtonConfig {
    fn default() -> Self {
        Self::new()
    }
}

/// WASM wrapper for Picard (Sequential Substitution) solver configuration.
#[wasm_bindgen]
#[derive(Clone, Debug)]
pub struct WasmPicardConfig {
    pub(crate) inner: PicardConfig,
}

#[wasm_bindgen]
impl WasmPicardConfig {
    /// Create default Picard configuration.
    #[wasm_bindgen(constructor)]
    pub fn new() -> Self {
        WasmPicardConfig {
            inner: PicardConfig::default(),
        }
    }

    /// Set maximum iterations.
    pub fn set_max_iterations(&mut self, max: usize) {
        self.inner.max_iterations = max;
    }

    /// Set relaxation factor. Must be in (0, 1]. Values outside this range are clamped.
    pub fn set_relaxation_factor(&mut self, omega: f64) {
        if omega.is_nan() || omega <= 0.0 {
            tracing::warn!("Invalid relaxation factor {}, clamping to 0.1", omega);
            self.inner.relaxation_factor = 0.1;
        } else if omega > 1.0 {
            tracing::warn!("Relaxation factor {} > 1.0, clamping to 1.0", omega);
            self.inner.relaxation_factor = 1.0;
        } else {
            self.inner.relaxation_factor = omega;
        }
    }
}

impl Default for WasmPicardConfig {
    fn default() -> Self {
        Self::new()
    }
}

/// WASM wrapper for fallback solver configuration.
#[wasm_bindgen]
#[derive(Clone, Debug)]
pub struct WasmFallbackConfig {
    inner: FallbackConfig,
}

#[wasm_bindgen]
impl WasmFallbackConfig {
    /// Create default fallback configuration.
    #[wasm_bindgen(constructor)]
    pub fn new() -> Self {
        WasmFallbackConfig {
            inner: FallbackConfig::default(),
        }
    }

    /// Enable or disable automatic fallback to Picard on divergence.
    pub fn set_fallback_enabled(&mut self, enabled: bool) {
        self.inner.fallback_enabled = enabled;
    }

    /// Set residual threshold for returning to Newton from Picard.
    pub fn set_return_to_newton_threshold(&mut self, threshold: f64) {
        self.inner.return_to_newton_threshold = threshold;
    }

    /// Set maximum number of solver switches before staying on current solver.
    pub fn set_max_fallback_switches(&mut self, max: usize) {
        self.inner.max_fallback_switches = max;
    }
}

impl Default for WasmFallbackConfig {
    fn default() -> Self {
        Self::new()
    }
}

/// WASM wrapper for System (thermodynamic system).
#[wasm_bindgen]
pub struct WasmSystem {
    inner: Rc<RefCell<System>>,
    last_state: RefCell<Option<Vec<f64>>>,
}

#[wasm_bindgen]
impl WasmSystem {
    /// Create a new thermodynamic system.
    #[wasm_bindgen(constructor)]
    pub fn new() -> Result<WasmSystem, JsValue> {
        let system = System::new();
        Ok(WasmSystem {
            inner: Rc::new(RefCell::new(system)),
            last_state: RefCell::new(None),
        })
    }

    /// Add a component to the system. Returns the node index.
    pub fn add_component(&mut self, component: WasmComponent) -> usize {
        self.inner
            .borrow_mut()
            .add_component(component.inner)
            .index()
    }

    /// Add an edge between components.
    ///
    /// Returns an error if node indices are out of bounds.
    pub fn add_edge(&mut self, from_idx: usize, to_idx: usize) -> Result<(), JsValue> {
        let system = self.inner.borrow();
        let node_count = system.node_count();
        drop(system);

        if from_idx >= node_count {
            return Err(js_sys::Error::new(&format!(
                "from_idx {} is out of bounds (system has {} nodes)",
                from_idx, node_count
            ))
            .into());
        }
        if to_idx >= node_count {
            return Err(js_sys::Error::new(&format!(
                "to_idx {} is out of bounds (system has {} nodes)",
                to_idx, node_count
            ))
            .into());
        }

        self.inner
            .borrow_mut()
            .add_edge(
                NodeIndex::from(from_idx as u32),
                NodeIndex::from(to_idx as u32),
            )
            .map(|_| ())
            .map_err(|e| js_sys::Error::new(&e.to_string()).into())
    }

    /// Finalize the system topology before solving.
    pub fn finalize(&mut self) -> Result<(), JsValue> {
        self.inner
            .borrow_mut()
            .finalize()
            .map_err(|e| js_sys::Error::new(&e.to_string()).into())
    }

    /// Solve the system with fallback strategy.
    pub fn solve(&mut self, config: WasmFallbackConfig) -> Result<WasmConvergedState, JsValue> {
        let mut solver = FallbackSolver::new(config.inner);
        let state = solver
            .solve(&mut self.inner.borrow_mut())
            .map_err(|e| js_sys::Error::new(&e.to_string()))?;

        *self.last_state.borrow_mut() = Some(state.state.clone());
        Ok((&state).into())
    }

    /// Solve with Newton-Raphson method.
    pub fn solve_newton(
        &mut self,
        config: WasmNewtonConfig,
    ) -> Result<WasmConvergedState, JsValue> {
        let mut solver = SolverStrategy::NewtonRaphson(config.inner);
        let state = solver
            .solve(&mut self.inner.borrow_mut())
            .map_err(|e| js_sys::Error::new(&e.to_string()))?;

        *self.last_state.borrow_mut() = Some(state.state.clone());
        Ok((&state).into())
    }

    /// Get node count.
    pub fn node_count(&self) -> usize {
        self.inner.borrow().node_count()
    }

    /// Get edge count.
    pub fn edge_count(&self) -> usize {
        self.inner.borrow().edge_count()
    }

    /// Get thermodynamic state for a specific node (after solve).
    ///
    /// Uses the global TabularBackend to compute full thermodynamic properties
    /// (temperature, entropy, density, phase, quality, etc.) from the solver's
    /// pressure and enthalpy state.
    pub fn get_node_result(&self, node_idx: usize) -> Result<WasmThermoState, JsValue> {
        let system = self.inner.borrow();
        let state_ref = self.last_state.borrow();
        let state = state_ref.as_ref().ok_or_else(|| {
            js_sys::Error::new("System must be solved before calling get_node_result")
        })?;

        for (idx, component, edges) in system.traverse_for_jacobian() {
            if idx.index() == node_idx {
                if let Some((_edge_idx, p_idx, h_idx)) = edges.first() {
                    if *p_idx >= state.len() || *h_idx >= state.len() {
                        return Err(js_sys::Error::new(&format!(
                            "State index out of bounds: p_idx={}, h_idx={}, state_len={}",
                            p_idx,
                            h_idx,
                            state.len()
                        ))
                        .into());
                    }

                    let p = state[*p_idx];
                    let h = state[*h_idx];

                    let ports = component.get_ports();
                    let fluid_id = if !ports.is_empty() {
                        entropyk_fluids::FluidId::new(ports[0].fluid_id().as_str())
                    } else {
                        return Err(js_sys::Error::new(
                            "Component has no ports — cannot determine fluid",
                        )
                        .into());
                    };

                    let backend = global_backend();
                    let thermo_state = backend
                        .full_state(
                            fluid_id,
                            entropyk_core::Pressure::from_pascals(p),
                            entropyk_core::Enthalpy::from_joules_per_kg(h),
                        )
                        .map_err(|e| {
                            js_sys::Error::new(&format!(
                                "Failed to compute thermodynamic state: {}",
                                e
                            ))
                        })?;

                    return Ok(thermo_state.into());
                }
            }
        }

        Err(js_sys::Error::new("Node not found or has no connections").into())
    }

    /// Convert system structural info to JSON.
    pub fn toJson(&self) -> Result<String, JsValue> {
        let data = serde_json::json!({
            "node_count": self.node_count(),
            "edge_count": self.edge_count(),
        });
        Ok(data.to_string())
    }
}

impl From<&ConvergedState> for WasmConvergedState {
    fn from(state: &ConvergedState) -> Self {
        let status_str = match state.status {
            ConvergenceStatus::Converged => "Converged",
            ConvergenceStatus::TimedOutWithBestState => "TimedOut",
            ConvergenceStatus::ControlSaturation => "ControlSaturation",
        };
        WasmConvergedState {
            converged: state.is_converged(),
            iterations: state.iterations,
            final_residual: state.final_residual,
            status: status_str.to_string(),
        }
    }
}
