//! Entropyk Web API Server
//!
//! REST API that exposes the full CLI physics engine to the Next.js canvas UI.
//!
//! Endpoints:
//!   GET  /api/health         — liveness check
//!   GET  /api/components     — component type catalogue (metadata for the UI)
//!   POST /api/simulate       — run a full simulation (ScenarioConfig JSON → SimulationResult)
//!
//! Run: cargo run -p entropyk-demo --bin ui-server

use axum::{
    extract::{DefaultBodyLimit, Json, Path, Query},
    http::Method,
    routing::{get, post},
    Router,
};
use entropyk_fluids::{CoolPropBackend, FluidBackend, FluidId};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::{LazyLock, Mutex};
use tower_http::cors::{Any, CorsLayer};

// ─── Component catalogue ────────────────────────────────────────────────────

/// Description of one parameter a component accepts.
#[derive(Debug, Serialize)]
struct ParamMeta {
    key: &'static str,
    label: &'static str,
    #[serde(rename = "type")]
    kind: &'static str, // "number" | "string" | "boolean"
    #[serde(skip_serializing_if = "Option::is_none")]
    unit: Option<&'static str>,
    #[serde(skip_serializing_if = "Option::is_none")]
    default: Option<Value>,
    required: bool,
}

/// Metadata for one component type (shown in the palette and property panel).
#[derive(Debug, Serialize)]
struct ComponentMeta {
    #[serde(rename = "type")]
    kind: &'static str,
    label: &'static str,
    category: &'static str,
    description: &'static str,
    /// Port names: ["inlet", "outlet"] for 2-port, ["suction","discharge","eco"] for 3-port.
    ports: Vec<&'static str>,
    params: Vec<ParamMeta>,
    /// Hex colour shown on the canvas node.
    color: &'static str,
}

fn component_catalogue() -> Vec<ComponentMeta> {
    vec![
        // ── Compressors ────────────────────────────────────────────────────
        ComponentMeta {
            kind: "IsentropicCompressor",
            label: "Isentropic Compressor",
            category: "Compressors",
            description: "Compressor with fixed isentropic efficiency. Ideal for quick modelling.",
            ports: vec!["inlet", "outlet"],
            color: "#6366f1",
            params: vec![
                ParamMeta {
                    key: "isentropic_efficiency",
                    label: "Isentropic efficiency (η)",
                    kind: "number",
                    unit: None,
                    default: Some(Value::from(0.75)),
                    required: true,
                },
                ParamMeta {
                    key: "t_cond_k",
                    label: "Condensing temp (K)",
                    kind: "number",
                    unit: Some("K"),
                    default: Some(Value::from(323.15)),
                    required: true,
                },
                ParamMeta {
                    key: "t_evap_k",
                    label: "Evaporating temp (K)",
                    kind: "number",
                    unit: Some("K"),
                    default: Some(Value::from(275.15)),
                    required: true,
                },
                ParamMeta {
                    key: "superheat_k",
                    label: "Superheat (K)",
                    kind: "number",
                    unit: Some("K"),
                    default: Some(Value::from(5.0)),
                    required: false,
                },
            ],
        },
        ComponentMeta {
            kind: "Compressor",
            label: "Polynomial Compressor",
            category: "Compressors",
            description:
                "Compressor modelled with SST/SDT polynomial performance curves (AHRI 540).",
            ports: vec!["inlet", "outlet"],
            color: "#6366f1",
            params: vec![
                ParamMeta {
                    key: "t_cond_k",
                    label: "Condensing temp (K)",
                    kind: "number",
                    unit: Some("K"),
                    default: Some(Value::from(313.15)),
                    required: true,
                },
                ParamMeta {
                    key: "t_evap_k",
                    label: "Evaporating temp (K)",
                    kind: "number",
                    unit: Some("K"),
                    default: Some(Value::from(268.15)),
                    required: true,
                },
                ParamMeta {
                    key: "superheat_k",
                    label: "Superheat (K)",
                    kind: "number",
                    unit: Some("K"),
                    default: Some(Value::from(5.0)),
                    required: false,
                },
            ],
        },
        ComponentMeta {
            kind: "ScrewEconomizerCompressor",
            label: "Screw Compressor (Economizer)",
            category: "Compressors",
            description: "3-port screw compressor with intermediate economizer injection port.",
            ports: vec!["suction", "discharge", "economizer"],
            color: "#4f46e5",
            params: vec![
                ParamMeta {
                    key: "frequency_hz",
                    label: "Frequency (Hz)",
                    kind: "number",
                    unit: Some("Hz"),
                    default: Some(Value::from(50.0)),
                    required: false,
                },
                ParamMeta {
                    key: "nominal_frequency_hz",
                    label: "Nominal frequency (Hz)",
                    kind: "number",
                    unit: Some("Hz"),
                    default: Some(Value::from(50.0)),
                    required: false,
                },
                ParamMeta {
                    key: "mechanical_efficiency",
                    label: "Mechanical efficiency",
                    kind: "number",
                    unit: None,
                    default: Some(Value::from(0.92)),
                    required: false,
                },
                ParamMeta {
                    key: "economizer_fraction",
                    label: "Economizer flow fraction",
                    kind: "number",
                    unit: None,
                    default: Some(Value::from(0.12)),
                    required: false,
                },
                ParamMeta {
                    key: "preset",
                    label: "Curve preset",
                    kind: "text",
                    unit: None,
                    default: Some(Value::from("bitzer_generic_200kw")),
                    required: false,
                },
            ],
        },
        // ── Heat exchangers ────────────────────────────────────────────────
        ComponentMeta {
            kind: "Condenser",
            label: "Condenser",
            category: "Heat Exchangers",
            description: "Refrigerant condenser with live secondary ports (water/air).",
            ports: vec![
                "inlet",
                "outlet",
                "secondary_inlet",
                "secondary_outlet",
            ],
            color: "#ef4444",
            params: vec![
                ParamMeta {
                    key: "ua",
                    label: "UA (W/K)",
                    kind: "number",
                    unit: Some("W/K"),
                    default: Some(Value::from(5000.0)),
                    required: true,
                },
                ParamMeta {
                    key: "emergent_pressure",
                    label: "Emergent pressure",
                    kind: "boolean",
                    unit: None,
                    default: Some(Value::from(false)),
                    required: false,
                },
                ParamMeta {
                    key: "secondary_fluid",
                    label: "Secondary fluid",
                    kind: "string",
                    unit: None,
                    default: Some(Value::from("Water")),
                    required: false,
                },
            ],
        },
        ComponentMeta {
            kind: "Evaporator",
            label: "Evaporator",
            category: "Heat Exchangers",
            description: "DX evaporator with live secondary ports (water/brine/air).",
            ports: vec![
                "inlet",
                "outlet",
                "secondary_inlet",
                "secondary_outlet",
            ],
            color: "#3b82f6",
            params: vec![
                ParamMeta {
                    key: "ua",
                    label: "UA (W/K)",
                    kind: "number",
                    unit: Some("W/K"),
                    default: Some(Value::from(6000.0)),
                    required: true,
                },
                ParamMeta {
                    key: "emergent_pressure",
                    label: "Emergent pressure",
                    kind: "boolean",
                    unit: None,
                    default: Some(Value::from(false)),
                    required: false,
                },
                ParamMeta {
                    key: "superheat_k",
                    label: "Superheat (K)",
                    kind: "number",
                    unit: Some("K"),
                    default: Some(Value::from(5.0)),
                    required: false,
                },
                ParamMeta {
                    key: "secondary_fluid",
                    label: "Secondary fluid",
                    kind: "string",
                    unit: None,
                    default: Some(Value::from("Water")),
                    required: false,
                },
            ],
        },
        ComponentMeta {
            kind: "MchxCondenserCoil",
            label: "MCHX Condenser Coil",
            category: "Heat Exchangers",
            description: "Multi-channel heat exchanger air-cooled condenser coil.",
            ports: vec!["inlet", "outlet"],
            color: "#f97316",
            params: vec![
                ParamMeta {
                    key: "capacity_w",
                    label: "Nominal capacity (W)",
                    kind: "number",
                    unit: Some("W"),
                    default: Some(Value::from(15000.0)),
                    required: true,
                },
                ParamMeta {
                    key: "air_inlet_temp_c",
                    label: "Air inlet temp (°C)",
                    kind: "number",
                    unit: Some("°C"),
                    default: Some(Value::from(35.0)),
                    required: false,
                },
                ParamMeta {
                    key: "fan_speed",
                    label: "Fan speed ratio (0–1)",
                    kind: "number",
                    unit: None,
                    default: Some(Value::from(1.0)),
                    required: false,
                },
            ],
        },
        ComponentMeta {
            kind: "AirCooledCondenser",
            label: "Air-Cooled Condenser",
            category: "Heat Exchangers",
            description: "Condenser with air-side fan model (temperature-dependent UA).",
            ports: vec!["inlet", "outlet"],
            color: "#f59e0b",
            params: vec![
                ParamMeta {
                    key: "ua",
                    label: "UA at full fan (W/K)",
                    kind: "number",
                    unit: Some("W/K"),
                    default: Some(Value::from(4000.0)),
                    required: true,
                },
                ParamMeta {
                    key: "t_air_inlet_c",
                    label: "Air inlet temp (°C)",
                    kind: "number",
                    unit: Some("°C"),
                    default: Some(Value::from(35.0)),
                    required: false,
                },
            ],
        },
        ComponentMeta {
            kind: "FloodedEvaporator",
            label: "Flooded Evaporator",
            category: "Heat Exchangers",
            description:
                "Shell-and-tube flooded evaporator. System mode: wire secondary_inlet/outlet to BrineSource/Sink.",
            ports: vec![
                "inlet",
                "outlet",
                "secondary_inlet",
                "secondary_outlet",
            ],
            color: "#06b6d4",
            params: vec![
                ParamMeta {
                    key: "ua",
                    label: "UA (W/K)",
                    kind: "number",
                    unit: Some("W/K"),
                    default: Some(Value::from(8000.0)),
                    required: true,
                },
                ParamMeta {
                    key: "secondary_fluid",
                    label: "Secondary fluid",
                    kind: "string",
                    unit: None,
                    default: Some(Value::from("Water")),
                    required: false,
                },
                ParamMeta {
                    key: "quality_control",
                    label: "Quality control residual",
                    kind: "boolean",
                    unit: None,
                    default: Some(Value::from(false)),
                    required: false,
                },
            ],
        },
        ComponentMeta {
            kind: "HeatExchanger",
            label: "Generic Heat Exchanger",
            category: "Heat Exchangers",
            description: "Two-fluid heat exchanger with configurable UA and effectiveness.",
            ports: vec!["hot_inlet", "hot_outlet", "cold_inlet", "cold_outlet"],
            color: "#8b5cf6",
            params: vec![
                ParamMeta {
                    key: "ua",
                    label: "UA (W/K)",
                    kind: "number",
                    unit: Some("W/K"),
                    default: Some(Value::from(3000.0)),
                    required: true,
                },
                ParamMeta {
                    key: "efficiency",
                    label: "Effectiveness",
                    kind: "number",
                    unit: None,
                    default: Some(Value::from(0.85)),
                    required: false,
                },
            ],
        },
        // ── Expansion devices ──────────────────────────────────────────────
        ComponentMeta {
            kind: "IsenthalpicExpansionValve",
            label: "Expansion Valve (EXV)",
            category: "Expansion",
            description: "Isenthalpic electronic expansion valve. h_out = h_in, P_out = P_evap.",
            ports: vec!["inlet", "outlet"],
            color: "#10b981",
            params: vec![
                ParamMeta {
                    key: "t_evap_k",
                    label: "Evaporating temp (K)",
                    kind: "number",
                    unit: Some("K"),
                    default: Some(Value::from(275.15)),
                    required: false,
                },
                ParamMeta {
                    key: "opening",
                    label: "Valve opening (0–1)",
                    kind: "number",
                    unit: None,
                    default: Some(Value::from(1.0)),
                    required: false,
                },
            ],
        },
        ComponentMeta {
            kind: "ExpansionValve",
            label: "Expansion Valve (basic)",
            category: "Expansion",
            description: "Basic expansion valve with fixed capacity coefficient.",
            ports: vec!["inlet", "outlet"],
            color: "#10b981",
            params: vec![ParamMeta {
                key: "capacity_w",
                label: "Nominal capacity (W)",
                kind: "number",
                unit: Some("W"),
                default: Some(Value::from(10000.0)),
                required: false,
            }],
        },
        ComponentMeta {
            kind: "BypassValve",
            label: "Bypass Valve",
            category: "Expansion",
            description: "Proportional bypass valve with configurable opening.",
            ports: vec!["inlet", "outlet"],
            color: "#14b8a6",
            params: vec![ParamMeta {
                key: "opening",
                label: "Opening (0–1)",
                kind: "number",
                unit: None,
                default: Some(Value::from(0.5)),
                required: false,
            }],
        },
        // ── Piping ────────────────────────────────────────────────────────
        ComponentMeta {
            kind: "Pipe",
            label: "Pipe",
            category: "Piping",
            description: "Straight pipe with friction pressure drop (Haaland correlation).",
            ports: vec!["inlet", "outlet"],
            color: "#64748b",
            params: vec![
                ParamMeta {
                    key: "length_m",
                    label: "Length (m)",
                    kind: "number",
                    unit: Some("m"),
                    default: Some(Value::from(10.0)),
                    required: true,
                },
                ParamMeta {
                    key: "diameter_m",
                    label: "Diameter (m)",
                    kind: "number",
                    unit: Some("m"),
                    default: Some(Value::from(0.022)),
                    required: true,
                },
                ParamMeta {
                    key: "roughness_m",
                    label: "Roughness (m)",
                    kind: "number",
                    unit: Some("m"),
                    default: Some(Value::from(0.0000015)),
                    required: false,
                },
            ],
        },
        ComponentMeta {
            kind: "Drum",
            label: "Liquid-Vapor Separator",
            category: "Piping",
            description: "Phase separator / accumulator (flash vessel).",
            ports: vec!["inlet", "liquid_outlet", "vapor_outlet"],
            color: "#94a3b8",
            params: vec![],
        },
        ComponentMeta {
            kind: "FlowSplitter",
            label: "Flow Splitter",
            category: "Piping",
            description: "Splits a flow into two branches by fixed ratio.",
            ports: vec!["inlet", "outlet_a", "outlet_b"],
            color: "#94a3b8",
            params: vec![ParamMeta {
                key: "split_ratio",
                label: "Split ratio (outlet_a)",
                kind: "number",
                unit: None,
                default: Some(Value::from(0.5)),
                required: false,
            }],
        },
        ComponentMeta {
            kind: "FlowMerger",
            label: "Flow Merger",
            category: "Piping",
            description: "Merges two flows into one.",
            ports: vec!["inlet_a", "inlet_b", "outlet"],
            color: "#94a3b8",
            params: vec![],
        },
        // ── Hydraulics ────────────────────────────────────────────────────
        ComponentMeta {
            kind: "Pump",
            label: "Pump",
            category: "Hydraulics",
            description: "Centrifugal pump with polynomial head/efficiency curves.",
            ports: vec!["inlet", "outlet"],
            color: "#a855f7",
            params: vec![
                ParamMeta {
                    key: "head_coeffs",
                    label: "Head curve (a0,a1,a2)",
                    kind: "string",
                    unit: Some("m"),
                    default: Some(Value::from("30,-10,-50")),
                    required: true,
                },
                ParamMeta {
                    key: "eff_coeffs",
                    label: "Efficiency curve (a0,a1,a2)",
                    kind: "string",
                    unit: None,
                    default: Some(Value::from("0.5,0.3,-0.5")),
                    required: true,
                },
                ParamMeta {
                    key: "density_kg_m3",
                    label: "Fluid density (kg/m³)",
                    kind: "number",
                    unit: Some("kg/m³"),
                    default: Some(Value::from(1000.0)),
                    required: false,
                },
            ],
        },
        ComponentMeta {
            kind: "Fan",
            label: "Fan",
            category: "Hydraulics",
            description: "Centrifugal fan for air-side loops.",
            ports: vec!["inlet", "outlet"],
            color: "#d946ef",
            params: vec![
                ParamMeta {
                    key: "pressure_rise_pa",
                    label: "Pressure rise (Pa)",
                    kind: "number",
                    unit: Some("Pa"),
                    default: Some(Value::from(200.0)),
                    required: false,
                },
                ParamMeta {
                    key: "flow_rate_m3_s",
                    label: "Flow rate (m³/s)",
                    kind: "number",
                    unit: Some("m³/s"),
                    default: Some(Value::from(1.5)),
                    required: false,
                },
            ],
        },
        // ── Sources / Sinks ───────────────────────────────────────────────
        ComponentMeta {
            kind: "RefrigerantSource",
            label: "Refrigerant Source",
            category: "Boundaries",
            description: "Fixed-state refrigerant boundary condition (inlet node).",
            ports: vec!["outlet"],
            color: "#f1f5f9",
            params: vec![
                ParamMeta {
                    key: "pressure_bar",
                    label: "Pressure (bar)",
                    kind: "number",
                    unit: Some("bar"),
                    default: Some(Value::from(10.0)),
                    required: true,
                },
                ParamMeta {
                    key: "temperature_c",
                    label: "Temperature (°C)",
                    kind: "number",
                    unit: Some("°C"),
                    default: Some(Value::from(40.0)),
                    required: false,
                },
            ],
        },
        ComponentMeta {
            kind: "RefrigerantSink",
            label: "Refrigerant Sink",
            category: "Boundaries",
            description: "Fixed-pressure refrigerant boundary condition (outlet node).",
            ports: vec!["inlet"],
            color: "#f1f5f9",
            params: vec![ParamMeta {
                key: "pressure_bar",
                label: "Pressure (bar)",
                kind: "number",
                unit: Some("bar"),
                default: Some(Value::from(4.0)),
                required: true,
            }],
        },
        ComponentMeta {
            kind: "BrineSource",
            label: "Brine / Glycol Source",
            category: "Boundaries",
            description: "Fixed-state secondary fluid (brine/glycol) boundary.",
            ports: vec!["outlet"],
            color: "#e0f2fe",
            params: vec![
                ParamMeta {
                    key: "temperature_c",
                    label: "Temperature (°C)",
                    kind: "number",
                    unit: Some("°C"),
                    default: Some(Value::from(12.0)),
                    required: true,
                },
                ParamMeta {
                    key: "mass_flow_kg_s",
                    label: "Mass flow (kg/s)",
                    kind: "number",
                    unit: Some("kg/s"),
                    default: Some(Value::from(0.5)),
                    required: false,
                },
            ],
        },
        ComponentMeta {
            kind: "BrineSink",
            label: "Brine / Glycol Sink",
            category: "Boundaries",
            description: "Fixed-pressure secondary fluid boundary.",
            ports: vec!["inlet"],
            color: "#e0f2fe",
            params: vec![],
        },
        ComponentMeta {
            kind: "AirSource",
            label: "Air Source",
            category: "Boundaries",
            description: "Fixed-state air boundary (ambient conditions).",
            ports: vec!["outlet"],
            color: "#f0fdf4",
            params: vec![
                ParamMeta {
                    key: "temperature_c",
                    label: "Temperature (°C)",
                    kind: "number",
                    unit: Some("°C"),
                    default: Some(Value::from(35.0)),
                    required: true,
                },
                ParamMeta {
                    key: "relative_humidity",
                    label: "Relative humidity (0–1)",
                    kind: "number",
                    unit: None,
                    default: Some(Value::from(0.5)),
                    required: false,
                },
            ],
        },
        ComponentMeta {
            kind: "AirSink",
            label: "Air Sink",
            category: "Boundaries",
            description: "Air boundary outlet (ambient discharge).",
            ports: vec!["inlet"],
            color: "#f0fdf4",
            params: vec![],
        },
        ComponentMeta {
            kind: "CapillaryTube",
            label: "Capillary Tube",
            category: "Expansion",
            description: "Adiabatic capillary tube (segmented ΔP, isenthalpic).",
            ports: vec!["inlet", "outlet"],
            color: "#059669",
            params: vec![
                ParamMeta {
                    key: "diameter_m",
                    label: "Inner diameter (m)",
                    kind: "number",
                    unit: Some("m"),
                    default: Some(Value::from(0.001)),
                    required: true,
                },
                ParamMeta {
                    key: "length_m",
                    label: "Length (m)",
                    kind: "number",
                    unit: Some("m"),
                    default: Some(Value::from(2.0)),
                    required: true,
                },
            ],
        },
        ComponentMeta {
            kind: "CentrifugalCompressor",
            label: "Centrifugal Compressor",
            category: "Compressors",
            description: "Centrifugal compressor with φ–Mach polytropic map.",
            ports: vec!["inlet", "outlet"],
            color: "#4338ca",
            params: vec![
                ParamMeta {
                    key: "diameter_m",
                    label: "Impeller diameter (m)",
                    kind: "number",
                    unit: Some("m"),
                    default: Some(Value::from(0.25)),
                    required: true,
                },
                ParamMeta {
                    key: "speed_rpm",
                    label: "Speed (rpm)",
                    kind: "number",
                    unit: Some("rpm"),
                    default: Some(Value::from(9000.0)),
                    required: true,
                },
            ],
        },
        // ── Placeholder ──────────────────────────────────────────────────
        ComponentMeta {
            kind: "Placeholder",
            label: "Placeholder",
            category: "Generic",
            description:
                "Generic placeholder with configurable equation count. Use for rapid prototyping.",
            ports: vec!["inlet", "outlet"],
            color: "#e2e8f0",
            params: vec![ParamMeta {
                key: "n_equations",
                label: "Number of equations",
                kind: "number",
                unit: None,
                default: Some(Value::from(2)),
                required: true,
            }],
        },
    ]
}

// ─── Request / Response types ────────────────────────────────────────────────

#[derive(Debug, Serialize)]
struct SimulateResponse {
    ok: bool,
    result: Option<Value>,
    error: Option<String>,
}

#[derive(Debug, Serialize)]
struct HealthResponse {
    status: &'static str,
    version: &'static str,
}

#[derive(Debug, Deserialize)]
struct MollierQuery {
    fluid: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
struct MollierPoint {
    t_k: f64,
    p_bar: f64,
    h_liq_kj_kg: f64,
    h_vap_kj_kg: f64,
}

#[derive(Debug, Serialize)]
struct MollierResponse {
    ok: bool,
    fluid: String,
    saturation: Vec<MollierPoint>,
    error: Option<String>,
}

static MOLLIER_CACHE: LazyLock<Mutex<HashMap<String, Vec<MollierPoint>>>> =
    LazyLock::new(|| Mutex::new(HashMap::new()));

/// CoolProp's C++ core is not thread-safe. Concurrent `/api/mollier` +
/// `/api/simulate` (common from the Next UI) can abort the process and surface
/// as a bare Next.js `500 Internal Server Error` with a stuck spinner.
static COOLPROP_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));

// ─── Handlers ────────────────────────────────────────────────────────────────

async fn health() -> Json<HealthResponse> {
    Json(HealthResponse {
        status: "ok",
        version: env!("CARGO_PKG_VERSION"),
    })
}

async fn components() -> Json<Vec<ComponentMeta>> {
    Json(component_catalogue())
}

// ─── Workspace modules (.ekmod) ─────────────────────────────────────────────

#[derive(Debug, Serialize)]
struct ModuleInfo {
    name: String,
    /// Exposed port names from the module's `ports` map.
    ports: Vec<String>,
    /// Component count inside the module.
    n_components: usize,
    /// Subsystem instance count inside the module.
    n_instances: usize,
}

/// `GET /api/modules` — list all `.ekmod` files in `.entropyk/modules/`.
async fn modules() -> Json<Vec<ModuleInfo>> {
    let dir = std::path::Path::new(".entropyk/modules");
    let mut list = Vec::new();

    if let Ok(entries) = std::fs::read_dir(dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.extension().and_then(|e| e.to_str()) != Some("ekmod") {
                continue;
            }
            let name = path
                .file_stem()
                .and_then(|s| s.to_str())
                .unwrap_or("unknown")
                .to_string();

            let (ports, n_components, n_instances) = std::fs::read_to_string(&path)
                .ok()
                .and_then(|json| serde_json::from_str::<serde_json::Value>(&json).ok())
                .map(|v| {
                    let ports = v
                        .get("ports")
                        .and_then(|p| p.as_object())
                        .map(|m| m.keys().cloned().collect())
                        .unwrap_or_default();
                    let n_components = v
                        .get("components")
                        .and_then(|c| c.as_array())
                        .map(|a| a.len())
                        .unwrap_or(0);
                    let n_instances = v
                        .get("instances")
                        .and_then(|c| c.as_array())
                        .map(|a| a.len())
                        .unwrap_or(0);
                    (ports, n_components, n_instances)
                })
                .unwrap_or_default();

            list.push(ModuleInfo {
                name,
                ports,
                n_components,
                n_instances,
            });
        }
    }

    Json(list)
}

/// `GET /api/modules/:name` — fetch the full content of a `.ekmod` file.
async fn module(Path(name): Path<String>) -> Json<Value> {
    let path = std::path::Path::new(".entropyk/modules").join(format!("{name}.ekmod"));
    match std::fs::read_to_string(&path) {
        Ok(json) => {
            let value: Value = serde_json::from_str(&json).unwrap_or(Value::Null);
            Json(value)
        }
        Err(_) => Json(Value::Null),
    }
}

#[derive(Debug, Deserialize)]
struct CreateModulePayload {
    name: String,
    content: Value,
}

#[derive(Debug, Serialize)]
struct CreateModuleResponse {
    ok: bool,
    error: Option<String>,
}

/// `POST /api/modules` — create/overwrite a `.ekmod` module file in `.entropyk/modules/`.
async fn create_module(Json(payload): Json<CreateModulePayload>) -> Json<CreateModuleResponse> {
    save_module_file(&payload.name, &payload.content)
}

/// `POST /api/modules/:name` — save a module by name.
async fn save_module_by_name(
    Path(name): Path<String>,
    Json(content): Json<Value>,
) -> Json<CreateModuleResponse> {
    save_module_file(&name, &content)
}

fn save_module_file(name: &str, content: &Value) -> Json<CreateModuleResponse> {
    let clean_name = name
        .chars()
        .filter(|c| c.is_alphanumeric() || *c == '_' || *c == '-')
        .collect::<String>();

    if clean_name.is_empty() {
        return Json(CreateModuleResponse {
            ok: false,
            error: Some("Invalid module name".to_string()),
        });
    }

    let dir = std::path::Path::new(".entropyk/modules");
    if let Err(e) = std::fs::create_dir_all(dir) {
        return Json(CreateModuleResponse {
            ok: false,
            error: Some(format!("Failed to create modules directory: {e}")),
        });
    }

    let path = dir.join(format!("{clean_name}.ekmod"));
    let json_str = match serde_json::to_string_pretty(content) {
        Ok(s) => s,
        Err(e) => {
            return Json(CreateModuleResponse {
                ok: false,
                error: Some(format!("Failed to serialize module content: {e}")),
            });
        }
    };

    match std::fs::write(&path, json_str) {
        Ok(_) => Json(CreateModuleResponse {
            ok: true,
            error: None,
        }),
        Err(e) => Json(CreateModuleResponse {
            ok: false,
            error: Some(format!("Failed to write module file: {e}")),
        }),
    }
}

fn build_mollier_saturation(fluid_name: &str) -> Result<Vec<MollierPoint>, String> {
    let _guard = COOLPROP_LOCK
        .lock()
        .map_err(|_| "CoolProp lock poisoned".to_string())?;

    let fluid = FluidId::new(fluid_name);
    let backend = CoolPropBackend::new();

    let critical = backend
        .critical_point(fluid.clone())
        .map_err(|e| e.to_string())?;

    let t_crit = critical.temperature.to_kelvin();
    let t_min = match fluid_name.to_ascii_lowercase().as_str() {
        "r744" | "co2" => 218.0,
        "water" => 274.0,
        _ => 220.0,
    };
    let t_max = (t_crit - 1.0).max(t_min + 5.0);
    let n = 44usize;
    let mut saturation = Vec::with_capacity(n);

    for i in 0..n {
        let frac = i as f64 / (n.saturating_sub(1) as f64);
        let t_k = t_min + frac * (t_max - t_min);
        let p = backend.saturation_pressure_t(fluid.clone(), t_k);
        let h_liq = backend.saturation_enthalpy_t(fluid.clone(), t_k, 0.0);
        let h_vap = backend.saturation_enthalpy_t(fluid.clone(), t_k, 1.0);
        if let (Ok(p), Ok(h_liq), Ok(h_vap)) = (p, h_liq, h_vap) {
            if p.is_finite() && h_liq.is_finite() && h_vap.is_finite() && p > 0.0 {
                saturation.push(MollierPoint {
                    t_k,
                    p_bar: p / 1.0e5,
                    h_liq_kj_kg: h_liq / 1000.0,
                    h_vap_kj_kg: h_vap / 1000.0,
                });
            }
        }
    }

    if saturation.len() >= 8 {
        Ok(saturation)
    } else {
        Err("not enough saturation points for Mollier diagram".to_string())
    }
}

async fn mollier(Query(query): Query<MollierQuery>) -> Json<MollierResponse> {
    let fluid_name = query.fluid.unwrap_or_else(|| "R134a".to_string());
    if let Some(cached) = MOLLIER_CACHE
        .lock()
        .ok()
        .and_then(|cache| cache.get(&fluid_name).cloned())
    {
        return Json(MollierResponse {
            ok: true,
            fluid: fluid_name,
            saturation: cached,
            error: None,
        });
    }

    let fluid_for_task = fluid_name.clone();
    let built =
        tokio::task::spawn_blocking(move || build_mollier_saturation(&fluid_for_task)).await;

    match built {
        Ok(Ok(saturation)) => {
            if let Ok(mut cache) = MOLLIER_CACHE.lock() {
                cache.insert(fluid_name.clone(), saturation.clone());
            }
            Json(MollierResponse {
                ok: true,
                fluid: fluid_name,
                saturation,
                error: None,
            })
        }
        Ok(Err(e)) => Json(MollierResponse {
            ok: false,
            fluid: fluid_name,
            saturation: Vec::new(),
            error: Some(e),
        }),
        Err(e) => Json(MollierResponse {
            ok: false,
            fluid: fluid_name,
            saturation: Vec::new(),
            error: Some(format!("Mollier worker failed: {e}")),
        }),
    }
}

/// Server-side default time budget: when the caller does not set
/// `solver.timeout_ms`, inject one (env ENTROPYK_UI_TIMEOUT_MS, default
/// 120 s) so a pathological case returns an honest Timeout error instead of
/// hanging the request until the proxy gives up with a bare 500.
///
/// Also defaults `solver.verbose` on so the UI diagnostics tab always gets
/// per-iteration history (small per-iteration cost, big post-mortem value).
///
/// Shared by `simulate` (single case) and `batch` (parallel cases) so both
/// paths apply the identical default.
fn ensure_solver_timeout(value: &mut Value) {
    let default_budget_ms: u64 = std::env::var("ENTROPYK_UI_TIMEOUT_MS")
        .ok()
        .and_then(|v| v.parse().ok())
        .unwrap_or(120_000);
    if let Some(solver) = value.get_mut("solver").and_then(|s| s.as_object_mut()) {
        let current = solver
            .get("timeout_ms")
            .and_then(|v| v.as_u64())
            .unwrap_or(0);
        if current == 0 {
            solver.insert("timeout_ms".to_string(), Value::from(default_budget_ms));
        }
        if solver.get("verbose").is_none() {
            solver.insert("verbose".to_string(), Value::from(true));
        }
    }
}

async fn simulate(Json(mut body): Json<Value>) -> Json<SimulateResponse> {
    ensure_solver_timeout(&mut body);
    let json_str = serde_json::to_string(&body).unwrap_or_default();
    let body_size = json_str.len();
    eprintln!(
        "[ui-server] POST /api/simulate — body {body_size} bytes — preview: {}",
        if json_str.len() > 240 {
            format!("{}…", &json_str[..240])
        } else {
            json_str.clone()
        }
    );
    // Persist the exact payload for post-mortem reproduction: when a config
    // hard-crashes the process (no unwind), this file is the only trace left.
    let dump_dir = std::path::Path::new("target/tmp");
    let _ = std::fs::create_dir_all(dump_dir);
    let _ = std::fs::write(dump_dir.join("last_simulate_request.json"), &json_str);

    // Run off the async runtime and serialize CoolProp access (see COOLPROP_LOCK).
    let joined = tokio::task::spawn_blocking(move || {
        let _guard = match COOLPROP_LOCK.lock() {
            Ok(g) => g,
            Err(_) => {
                eprintln!("[ui-server] CoolProp lock poisoned — previous panic left it dirty");
                return Err("CoolProp lock poisoned — restart ui-server".to_string());
            }
        };
        let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            entropyk_cli::run::simulate_from_json(&json_str)
        }));
        match outcome {
            Ok(Ok(result)) => {
                eprintln!("[ui-server] simulate OK — status: {:?}", result.status);
                Ok(Ok(result))
            }
            Ok(Err(e)) => {
                eprintln!("[ui-server] simulate solver error: {e}");
                Ok(Err(e.to_string()))
            }
            Err(panic) => {
                let msg = if let Some(s) = panic.downcast_ref::<&str>() {
                    (*s).to_string()
                } else if let Some(s) = panic.downcast_ref::<String>() {
                    s.clone()
                } else {
                    "unknown panic in simulate_from_json".to_string()
                };
                eprintln!("[ui-server] simulate PANIC: {msg}");
                Err(format!("Internal solver panic: {msg}"))
            }
        }
    })
    .await;

    match &joined {
        Ok(Ok(Ok(_))) => eprintln!("[ui-server] responding 200 ok=true"),
        Ok(Ok(Err(e))) => eprintln!("[ui-server] responding 200 ok=false (solver): {e}"),
        Ok(Err(e)) => eprintln!("[ui-server] responding 200 ok=false (panic captured): {e}"),
        Err(e) => eprintln!("[ui-server] responding 200 ok=false (JoinError): {e}"),
    }

    match joined {
        Ok(Ok(Ok(result))) => {
            let value = serde_json::to_value(result).unwrap_or(Value::Null);
            Json(SimulateResponse {
                ok: true,
                result: Some(value),
                error: None,
            })
        }
        Ok(Ok(Err(e))) => Json(SimulateResponse {
            ok: false,
            result: None,
            error: Some(e),
        }),
        Ok(Err(e)) => Json(SimulateResponse {
            ok: false,
            result: None,
            error: Some(e),
        }),
        Err(e) => Json(SimulateResponse {
            ok: false,
            result: None,
            error: Some(format!("Simulate worker failed: {e}")),
        }),
    }
}

// ─── Main ────────────────────────────────────────────────────────────────────

#[tokio::main]
async fn main() {
    // Sub-second solves for the UI (Story 0.7): enable the finite-only,
    // overflow-safe property cache for this process unless the operator
    // explicitly opted out (ENTROPYK_CACHE=0).
    if std::env::var("ENTROPYK_CACHE").is_err() {
        std::env::set_var("ENTROPYK_CACHE", "1");
    }

    let port = std::env::var("PORT").unwrap_or_else(|_| "3030".to_string());
    let addr: SocketAddr = format!("0.0.0.0:{}", port)
        .parse()
        .expect("invalid address");

    let cors = CorsLayer::new()
        .allow_origin(Any)
        .allow_methods([Method::GET, Method::POST, Method::OPTIONS])
        .allow_headers(Any);

    // Raise Axum's request body limit (default 2 MB). Complex scenarios with
    // many components + module instances can exceed 2 MB and silently surface
    // as HTTP 413/500 from the Json extractor.
    let app = Router::new()
        .route("/api/health", get(health))
        .route("/api/components", get(components))
        .route("/api/modules", get(modules).post(create_module))
        .route("/api/modules/:name", get(module).post(save_module_by_name))
        .route("/api/mollier", get(mollier))
        .route("/api/simulate", post(simulate))
        .layer(cors)
        .layer(DefaultBodyLimit::max(32 * 1024 * 1024));

    println!("Entropyk API server — http://localhost:{}", port);
    println!("  GET  /api/health");
    println!("  GET  /api/components");
    println!("  GET  /api/modules");
    println!("  GET  /api/modules/:name");
    println!("  GET  /api/mollier?fluid=R134a");
    println!("  POST /api/simulate");

    // Prewarm the CoolProp fluid database in the background: the first property
    // query in a fresh process pays a one-time fluid-library load (~30 s with
    // the prebuilt static CoolProp.lib). Handlers serialize on COOLPROP_LOCK,
    // so a request arriving during warmup simply waits for it instead of
    // failing or timing out — and by the time the user clicks "Simulate",
    // warmup is usually long done.
    tokio::task::spawn_blocking(|| {
        eprintln!("[ui-server] warming up CoolProp fluid database (one-time cost)…");
        let t0 = std::time::Instant::now();
        let _guard = COOLPROP_LOCK.lock();
        let available = CoolPropBackend::new().is_fluid_available(&FluidId::new("R134a"));
        eprintln!(
            "[ui-server] CoolProp fluid database ready in {:.1?} (R134a available: {available})",
            t0.elapsed()
        );
    });

    let listener = tokio::net::TcpListener::bind(&addr)
        .await
        .unwrap_or_else(|e| {
            eprintln!("Cannot bind port {} : {}", port, e);
            std::process::exit(1);
        });

    axum::serve(listener, app).await.unwrap();
}
