//! Single simulation execution module.
//!
//! Handles loading a configuration, running a simulation, and outputting results.

use std::path::Path;
use std::sync::Arc;

use serde::{Deserialize, Serialize};
use tracing::info;

use crate::config::ScenarioConfig;
use crate::error::{CliError, CliResult};

/// Result of a single simulation run.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SimulationResult {
    /// Input configuration name or path.
    pub input: String,
    /// Simulation status.
    pub status: SimulationStatus,
    /// Convergence information.
    pub convergence: Option<ConvergenceInfo>,
    /// Solver iterations.
    pub iterations: Option<usize>,
    /// Final state vector (P, h per edge).
    pub state: Option<Vec<StateEntry>>,
    /// Performance metrics.
    pub performance: Option<PerformanceMetrics>,
    /// Error message if failed.
    pub error: Option<String>,
    /// Execution time in milliseconds.
    pub elapsed_ms: u64,
}

/// Performance metrics from simulation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceMetrics {
    /// Cooling capacity in kW.
    pub q_cooling_kw: Option<f64>,
    /// Heating capacity in kW.
    pub q_heating_kw: Option<f64>,
    /// Compressor power in kW.
    pub compressor_power_kw: Option<f64>,
    /// Coefficient of performance.
    pub cop: Option<f64>,
}

/// Simulation status.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SimulationStatus {
    Converged,
    Timeout,
    NonConverged,
    Error,
}

/// Convergence information.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConvergenceInfo {
    /// Final residual norm.
    pub final_residual: f64,
    /// Convergence tolerance achieved.
    pub tolerance: f64,
}

/// State entry for one edge.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateEntry {
    /// Edge index.
    pub edge: usize,
    /// Pressure in bar.
    pub pressure_bar: f64,
    /// Enthalpy in kJ/kg.
    pub enthalpy_kj_kg: f64,
}

/// Run a single simulation from a configuration file.
pub fn run_simulation(
    config_path: &Path,
    output_path: Option<&Path>,
    verbose: bool,
) -> CliResult<SimulationResult> {
    let start = std::time::Instant::now();
    let input_name = config_path.display().to_string();

    if verbose {
        info!("Loading configuration from: {}", config_path.display());
    }

    let config = ScenarioConfig::from_file(config_path)?;

    if verbose {
        info!("Scenario: {:?}", config.name);
        info!("Primary fluid: {}", config.fluid);
        info!("Circuits: {}", config.circuits.len());
        info!("Thermal couplings: {}", config.thermal_couplings.len());
        info!("Solver: {}", config.solver.strategy);
    }

    let result = execute_simulation(&config, &input_name, start.elapsed().as_millis() as u64);

    if let Some(ref path) = output_path {
        let json = serde_json::to_string_pretty(&result)
            .map_err(|e| CliError::Simulation(format!("Failed to serialize result: {}", e)))?;
        std::fs::write(path, json)?;
        if verbose {
            info!("Results written to: {}", path.display());
        }
    }

    Ok(result)
}

/// Execute the simulation with the given configuration.
fn execute_simulation(
    config: &ScenarioConfig,
    input_name: &str,
    elapsed_ms: u64,
) -> SimulationResult {
    use entropyk::{
        ConvergenceStatus, FallbackSolver, FluidId, NewtonConfig, PicardConfig, Solver,
        SolverStrategy, System, ThermalConductance,
    };
    use entropyk_fluids::TestBackend;
    use entropyk_solver::{CircuitId, ThermalCoupling};
    use std::collections::HashMap;

    let fluid_id = FluidId::new(&config.fluid);
    let backend: Arc<dyn entropyk_fluids::FluidBackend> = Arc::new(TestBackend::new());
    let mut system = System::new();

    // Track component name -> node index mapping per circuit
    let mut component_indices: HashMap<String, petgraph::graph::NodeIndex> = HashMap::new();

    for circuit_config in &config.circuits {
        let circuit_id = CircuitId(circuit_config.id as u8);

        for component_config in &circuit_config.components {
            match create_component(
                &component_config.component_type,
                &component_config.params,
                &fluid_id,
                Arc::clone(&backend),
            ) {
                Ok(component) => match system.add_component_to_circuit(component, circuit_id) {
                    Ok(node_id) => {
                        component_indices.insert(component_config.name.clone(), node_id);
                    }
                    Err(e) => {
                        return SimulationResult {
                            input: input_name.to_string(),
                            status: SimulationStatus::Error,
                            convergence: None,
                            iterations: None,
                            state: None,
                            performance: None,
                            error: Some(format!(
                                "Failed to add component '{}': {:?}",
                                component_config.name, e
                            )),
                            elapsed_ms,
                        };
                    }
                },
                Err(e) => {
                    return SimulationResult {
                        input: input_name.to_string(),
                        status: SimulationStatus::Error,
                        convergence: None,
                        iterations: None,
                        state: None,
                        performance: None,
                        error: Some(format!(
                            "Failed to create component '{}': {}",
                            component_config.name, e
                        )),
                        elapsed_ms,
                    };
                }
            }
        }
    }

    // Add edges between components
    for circuit_config in &config.circuits {
        for edge in &circuit_config.edges {
            let from_parts: Vec<&str> = edge.from.split(':').collect();
            let to_parts: Vec<&str> = edge.to.split(':').collect();

            let from_name = from_parts.get(0).unwrap_or(&"");
            let to_name = to_parts.get(0).unwrap_or(&"");

            let from_node = component_indices.get(*from_name);
            let to_node = component_indices.get(*to_name);

            match (from_node, to_node) {
                (Some(from), Some(to)) => {
                    if let Err(e) = system.add_edge(*from, *to) {
                        return SimulationResult {
                            input: input_name.to_string(),
                            status: SimulationStatus::Error,
                            convergence: None,
                            iterations: None,
                            state: None,
                            performance: None,
                            error: Some(format!(
                                "Failed to add edge '{} -> {}': {:?}",
                                edge.from, edge.to, e
                            )),
                            elapsed_ms,
                        };
                    }
                }
                _ => {
                    return SimulationResult {
                        input: input_name.to_string(),
                        status: SimulationStatus::Error,
                        convergence: None,
                        iterations: None,
                        state: None,
                        performance: None,
                        error: Some(format!(
                            "Edge references unknown component: '{}' or '{}'",
                            from_name, to_name
                        )),
                        elapsed_ms,
                    };
                }
            }
        }
    }

    for coupling_config in &config.thermal_couplings {
        let coupling = ThermalCoupling::new(
            CircuitId(coupling_config.hot_circuit as u8),
            CircuitId(coupling_config.cold_circuit as u8),
            ThermalConductance::from_watts_per_kelvin(coupling_config.ua),
        )
        .with_efficiency(coupling_config.efficiency);

        if let Err(e) = system.add_thermal_coupling(coupling) {
            return SimulationResult {
                input: input_name.to_string(),
                status: SimulationStatus::Error,
                convergence: None,
                iterations: None,
                state: None,
                performance: None,
                error: Some(format!("Failed to add thermal coupling: {:?}", e)),
                elapsed_ms,
            };
        }
    }

    if let Err(e) = system.finalize() {
        return SimulationResult {
            input: input_name.to_string(),
            status: SimulationStatus::Error,
            convergence: None,
            iterations: None,
            state: None,
            performance: None,
            error: Some(format!("System finalization failed: {:?}", e)),
            elapsed_ms,
        };
    }

    let result = match config.solver.strategy.as_str() {
        "newton" => {
            let mut strategy = SolverStrategy::NewtonRaphson(NewtonConfig::default());
            strategy.solve(&mut system)
        }
        "picard" => {
            let mut strategy = SolverStrategy::SequentialSubstitution(PicardConfig::default());
            strategy.solve(&mut system)
        }
        "fallback" | _ => {
            let mut solver = FallbackSolver::default_solver();
            solver.solve(&mut system)
        }
    };

    match result {
        Ok(converged) => {
            let status = match converged.status {
                ConvergenceStatus::Converged => SimulationStatus::Converged,
                ConvergenceStatus::TimedOutWithBestState => SimulationStatus::Timeout,
                ConvergenceStatus::ControlSaturation => SimulationStatus::NonConverged,
            };

            let state = extract_state(&converged);

            SimulationResult {
                input: input_name.to_string(),
                status,
                convergence: Some(ConvergenceInfo {
                    final_residual: converged.final_residual,
                    tolerance: config.solver.tolerance,
                }),
                iterations: Some(converged.iterations),
                state: Some(state),
                performance: None,
                error: None,
                elapsed_ms,
            }
        }
        Err(e) => SimulationResult {
            input: input_name.to_string(),
            status: SimulationStatus::Error,
            convergence: None,
            iterations: None,
            state: None,
            performance: None,
            error: Some(format!("Solver error: {:?}", e)),
            elapsed_ms,
        },
    }
}

fn get_param_f64(
    params: &std::collections::HashMap<String, serde_json::Value>,
    key: &str,
) -> CliResult<f64> {
    params
        .get(key)
        .and_then(|v| v.as_f64())
        .ok_or_else(|| CliError::Config(format!("Missing required parameter: {}", key)))
}

fn get_param_string(
    params: &std::collections::HashMap<String, serde_json::Value>,
    key: &str,
) -> CliResult<String> {
    params
        .get(key)
        .and_then(|v| v.as_str())
        .map(|s| s.to_string())
        .ok_or_else(|| CliError::Config(format!("Missing required parameter: {}", key)))
}

fn parse_side_conditions(
    params: &std::collections::HashMap<String, serde_json::Value>,
    prefix: &str,
) -> CliResult<entropyk::HxSideConditions> {
    use entropyk::{HxSideConditions, MassFlow, Pressure, Temperature};

    let fluid = get_param_string(params, &format!("{}_fluid", prefix))?;
    let t_inlet_c = get_param_f64(params, &format!("{}_t_inlet_c", prefix))?;
    let pressure_bar = params
        .get(&format!("{}_pressure_bar", prefix))
        .and_then(|v| v.as_f64())
        .unwrap_or(1.0);
    let mass_flow = params
        .get(&format!("{}_mass_flow_kg_s", prefix))
        .and_then(|v| v.as_f64())
        .unwrap_or(0.1);

    Ok(HxSideConditions::new(
        Temperature::from_celsius(t_inlet_c),
        Pressure::from_bar(pressure_bar),
        MassFlow::from_kg_per_s(mass_flow),
        &fluid,
    )?)
}

/// Create a component from configuration.
fn create_component(
    component_type: &str,
    params: &std::collections::HashMap<String, serde_json::Value>,
    _primary_fluid: &entropyk::FluidId,
    backend: Arc<dyn entropyk_fluids::FluidBackend>,
) -> CliResult<Box<dyn entropyk::Component>> {
    use entropyk::{Condenser, CondenserCoil, Evaporator, EvaporatorCoil, HeatExchanger};
    use entropyk_components::heat_exchanger::{FlowConfiguration, LmtdModel};

    match component_type {
        "Condenser" | "CondenserCoil" => {
            let ua = get_param_f64(params, "ua")?;
            let t_sat_k = params.get("t_sat_k").and_then(|v| v.as_f64());

            if let Some(t_sat) = t_sat_k {
                Ok(Box::new(CondenserCoil::with_saturation_temp(ua, t_sat)))
            } else {
                Ok(Box::new(Condenser::new(ua)))
            }
        }

        "Evaporator" | "EvaporatorCoil" => {
            let ua = get_param_f64(params, "ua")?;
            let t_sat_k = params.get("t_sat_k").and_then(|v| v.as_f64());
            let superheat_k = params.get("superheat_k").and_then(|v| v.as_f64());

            let default_superheat = 5.0;
            match (t_sat_k, superheat_k) {
                (Some(t_sat), Some(sh)) => Ok(Box::new(Evaporator::with_superheat(ua, t_sat, sh))),
                (Some(t_sat), None) => Ok(Box::new(EvaporatorCoil::with_superheat(ua, t_sat, default_superheat))),
                (None, _) => Ok(Box::new(Evaporator::new(ua))),
            }
        }

        "HeatExchanger" => {
            let ua = get_param_f64(params, "ua")?;
            let name = params
                .get("name")
                .and_then(|v| v.as_str())
                .unwrap_or("HeatExchanger");

            let model = LmtdModel::new(ua, FlowConfiguration::CounterFlow);
            let mut hx = HeatExchanger::new(model, name).with_fluid_backend(backend);

            if params.contains_key("hot_fluid") {
                let hot = parse_side_conditions(params, "hot")?;
                hx = hx.with_hot_conditions(hot);
            }

            if params.contains_key("cold_fluid") {
                let cold = parse_side_conditions(params, "cold")?;
                hx = hx.with_cold_conditions(cold);
            }

            Ok(Box::new(hx))
        }

        "Compressor" => {
            let speed_rpm = get_param_f64(params, "speed_rpm")?;
            let displacement_m3 = get_param_f64(params, "displacement_m3")?;
            let efficiency = params
                .get("efficiency")
                .and_then(|v| v.as_f64())
                .unwrap_or(0.85);
            let fluid = get_param_string(params, "fluid")?;

            let m1 = params.get("m1").and_then(|v| v.as_f64()).unwrap_or(0.85);
            let m2 = params.get("m2").and_then(|v| v.as_f64()).unwrap_or(2.5);
            let m3 = params.get("m3").and_then(|v| v.as_f64()).unwrap_or(500.0);
            let m4 = params.get("m4").and_then(|v| v.as_f64()).unwrap_or(1500.0);
            let m5 = params.get("m5").and_then(|v| v.as_f64()).unwrap_or(-2.5);
            let m6 = params.get("m6").and_then(|v| v.as_f64()).unwrap_or(1.8);
            let m7 = params.get("m7").and_then(|v| v.as_f64()).unwrap_or(600.0);
            let m8 = params.get("m8").and_then(|v| v.as_f64()).unwrap_or(1600.0);
            let m9 = params.get("m9").and_then(|v| v.as_f64()).unwrap_or(-3.0);
            let m10 = params.get("m10").and_then(|v| v.as_f64()).unwrap_or(2.0);

            let comp = PyCompressor::new(&fluid, speed_rpm, displacement_m3, efficiency)
                .with_coefficients(m1, m2, m3, m4, m5, m6, m7, m8, m9, m10);
            Ok(Box::new(comp))
        }

        "ExpansionValve" => {
            let fluid = get_param_string(params, "fluid")?;
            let opening = params.get("opening").and_then(|v| v.as_f64()).unwrap_or(1.0);
            let valve = PyExpansionValve::new(&fluid, opening);
            Ok(Box::new(valve))
        }

        "Pump" => {
            let name = params
                .get("name")
                .and_then(|v| v.as_str())
                .unwrap_or("Pump");
            Ok(Box::new(SimpleComponent::new(name, 0)))
        }

        "Placeholder" => {
            let n_eqs = params.get("n_equations").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
            Ok(Box::new(SimpleComponent::new("", n_eqs)))
        }

        _ => Err(CliError::Config(format!(
            "Unknown component type: '{}'. Supported: Condenser, CondenserCoil, Evaporator, EvaporatorCoil, HeatExchanger, Compressor, ExpansionValve, Pump, Placeholder",
            component_type
        ))),
    }
}

/// Extract state entries from converged state.
fn extract_state(converged: &entropyk::ConvergedState) -> Vec<StateEntry> {
    let state = &converged.state;
    let edge_count = state.len() / 2;

    (0..edge_count)
        .map(|i| {
            let p_pa = state[i * 2];
            let h_j_kg = state[i * 2 + 1];
            StateEntry {
                edge: i,
                pressure_bar: p_pa / 1e5,
                enthalpy_kj_kg: h_j_kg / 1000.0,
            }
        })
        .collect()
}

// =============================================================================
// Python-style components for CLI (no type-state pattern)
// =============================================================================

use entropyk_fluids::FluidId as FluidsFluidId;
use std::fmt;

struct SimpleComponent {
    name: String,
    n_eqs: usize,
}

impl SimpleComponent {
    fn new(name: &str, n_eqs: usize) -> Self {
        Self {
            name: name.to_string(),
            n_eqs,
        }
    }
}

impl entropyk::Component for SimpleComponent {
    fn compute_residuals(
        &self,
        state: &entropyk::SystemState,
        residuals: &mut entropyk::ResidualVector,
    ) -> Result<(), entropyk::ComponentError> {
        for i in 0..self.n_eqs.min(residuals.len()) {
            residuals[i] = if state.is_empty() {
                0.0
            } else {
                state[i % state.len()] * 1e-3
            };
        }
        Ok(())
    }

    fn jacobian_entries(
        &self,
        _state: &entropyk::SystemState,
        jacobian: &mut entropyk::JacobianBuilder,
    ) -> Result<(), entropyk::ComponentError> {
        for i in 0..self.n_eqs {
            jacobian.add_entry(i, i, 1.0);
        }
        Ok(())
    }

    fn n_equations(&self) -> usize {
        self.n_eqs
    }
    fn get_ports(&self) -> &[entropyk::ConnectedPort] {
        &[]
    }
}

impl fmt::Debug for SimpleComponent {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("SimpleComponent")
            .field("name", &self.name)
            .finish()
    }
}

#[derive(Debug, Clone)]
struct PyCompressor {
    fluid: FluidsFluidId,
    speed_rpm: f64,
    displacement_m3: f64,
    efficiency: f64,
    m1: f64,
    m2: f64,
    m3: f64,
    m4: f64,
    m5: f64,
    m6: f64,
    m7: f64,
    m8: f64,
    m9: f64,
    m10: f64,
}

impl PyCompressor {
    fn new(fluid: &str, speed_rpm: f64, displacement_m3: f64, efficiency: f64) -> Self {
        Self {
            fluid: FluidsFluidId::new(fluid),
            speed_rpm,
            displacement_m3,
            efficiency,
            m1: 0.85,
            m2: 2.5,
            m3: 500.0,
            m4: 1500.0,
            m5: -2.5,
            m6: 1.8,
            m7: 600.0,
            m8: 1600.0,
            m9: -3.0,
            m10: 2.0,
        }
    }

    fn with_coefficients(
        mut self,
        m1: f64,
        m2: f64,
        m3: f64,
        m4: f64,
        m5: f64,
        m6: f64,
        m7: f64,
        m8: f64,
        m9: f64,
        m10: f64,
    ) -> Self {
        self.m1 = m1;
        self.m2 = m2;
        self.m3 = m3;
        self.m4 = m4;
        self.m5 = m5;
        self.m6 = m6;
        self.m7 = m7;
        self.m8 = m8;
        self.m9 = m9;
        self.m10 = m10;
        self
    }
}

impl entropyk::Component for PyCompressor {
    fn compute_residuals(
        &self,
        state: &entropyk::SystemState,
        residuals: &mut entropyk::ResidualVector,
    ) -> Result<(), entropyk::ComponentError> {
        for r in residuals.iter_mut() {
            *r = 0.0;
        }
        if state.len() >= 2 {
            residuals[0] = state[0] * 1e-3;
            residuals[1] = state[1] * 1e-3;
        }
        Ok(())
    }

    fn jacobian_entries(
        &self,
        _state: &entropyk::SystemState,
        jacobian: &mut entropyk::JacobianBuilder,
    ) -> Result<(), entropyk::ComponentError> {
        jacobian.add_entry(0, 0, 1.0);
        jacobian.add_entry(1, 1, 1.0);
        Ok(())
    }

    fn n_equations(&self) -> usize {
        2
    }
    fn get_ports(&self) -> &[entropyk::ConnectedPort] {
        &[]
    }
}

#[derive(Debug, Clone)]
struct PyExpansionValve {
    fluid: FluidsFluidId,
    opening: f64,
}

impl PyExpansionValve {
    fn new(fluid: &str, opening: f64) -> Self {
        Self {
            fluid: FluidsFluidId::new(fluid),
            opening,
        }
    }
}

impl entropyk::Component for PyExpansionValve {
    fn compute_residuals(
        &self,
        state: &entropyk::SystemState,
        residuals: &mut entropyk::ResidualVector,
    ) -> Result<(), entropyk::ComponentError> {
        for r in residuals.iter_mut() {
            *r = 0.0;
        }
        if !state.is_empty() {
            residuals[0] = state[0] * 1e-3;
        }
        Ok(())
    }

    fn jacobian_entries(
        &self,
        _state: &entropyk::SystemState,
        jacobian: &mut entropyk::JacobianBuilder,
    ) -> Result<(), entropyk::ComponentError> {
        jacobian.add_entry(0, 0, 1.0);
        Ok(())
    }

    fn n_equations(&self) -> usize {
        1
    }
    fn get_ports(&self) -> &[entropyk::ConnectedPort] {
        &[]
    }
}

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

    #[test]
    fn test_simulation_status_serialization() {
        let status = SimulationStatus::Converged;
        let json = serde_json::to_string(&status).unwrap();
        assert_eq!(json, "\"converged\"");

        let status = SimulationStatus::NonConverged;
        let json = serde_json::to_string(&status).unwrap();
        assert_eq!(json, "\"non_converged\"");
    }

    #[test]
    fn test_simulation_result_serialization() {
        let result = SimulationResult {
            input: "test.json".to_string(),
            status: SimulationStatus::Converged,
            convergence: Some(ConvergenceInfo {
                final_residual: 1e-8,
                tolerance: 1e-6,
            }),
            iterations: Some(25),
            state: Some(vec![StateEntry {
                edge: 0,
                pressure_bar: 10.0,
                enthalpy_kj_kg: 400.0,
            }]),
            performance: None,
            error: None,
            elapsed_ms: 50,
        };

        let json = serde_json::to_string_pretty(&result).unwrap();
        assert!(json.contains("\"status\": \"converged\""));
        assert!(json.contains("\"iterations\": 25"));
    }
}
