//! SWEP brazed-plate heat exchanger (BPHX) data backend.
//!
//! Loads BPHX parameters and UA curves from JSON files in the
//! `data/swep/bphx/` directory.

use std::collections::HashMap;
use std::path::PathBuf;

use crate::error::VendorError;
use crate::vendor_api::{BphxParameters, CompressorCoefficients, UaCalcParams, VendorBackend};

/// Backend for SWEP brazed-plate heat exchanger data.
///
/// Loads an index file (`index.json`) listing available BPHX models,
/// then eagerly pre-caches each model's JSON file into memory.
///
/// # Example
///
/// ```no_run
/// use entropyk_vendors::heat_exchangers::swep::SwepBackend;
/// use entropyk_vendors::VendorBackend;
///
/// let backend = SwepBackend::new().expect("load SWEP data");
/// let models = backend.list_bphx_models().unwrap();
/// println!("Available: {:?}", models);
/// ```
#[derive(Debug)]
pub struct SwepBackend {
    /// Root path to the SWEP data directory.
    data_path: PathBuf,
    /// Pre-loaded BPHX parameters keyed by model name.
    bphx_cache: HashMap<String, BphxParameters>,
    /// Sorted list of available models.
    sorted_models: Vec<String>,
}

impl SwepBackend {
    /// Create a new SWEP backend, loading all BPHX models from disk.
    ///
    /// The data directory is resolved via the `ENTROPYK_DATA` environment variable.
    /// If unset, it falls back to the compile-time `CARGO_MANIFEST_DIR/data` in debug mode,
    /// or `./data` in release mode.
    pub fn new() -> Result<Self, VendorError> {
        let base_path = std::env::var("ENTROPYK_DATA")
            .map(PathBuf::from)
            .unwrap_or_else(|_| {
                #[cfg(debug_assertions)]
                {
                    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("data")
                }
                #[cfg(not(debug_assertions))]
                {
                    PathBuf::from("data")
                }
            });

        let data_path = base_path.join("swep");

        let mut backend = Self {
            data_path,
            bphx_cache: HashMap::new(),
            sorted_models: Vec::new(),
        };

        backend.load_index()?;

        Ok(backend)
    }

    /// Create a new SWEP backend from a custom data path.
    ///
    /// Useful for testing with alternative data directories.
    pub fn from_path(data_path: PathBuf) -> Result<Self, VendorError> {
        let mut backend = Self {
            data_path,
            bphx_cache: HashMap::new(),
            sorted_models: Vec::new(),
        };

        backend.load_index()?;

        Ok(backend)
    }

    /// Load the BPHX index and pre-cache all referenced models.
    fn load_index(&mut self) -> Result<(), VendorError> {
        let index_path = self.data_path.join("bphx").join("index.json");
        let index_content =
            std::fs::read_to_string(&index_path).map_err(|e| VendorError::IoError {
                path: index_path.display().to_string(),
                source: e,
            })?;

        let models: Vec<String> = serde_json::from_str(&index_content)?;

        for model in models {
            match self.load_model(&model) {
                Ok(params) => {
                    self.bphx_cache.insert(model.clone(), params);
                    self.sorted_models.push(model);
                }
                Err(e) => {
                    log::warn!("[entropyk-vendors] Skipping SWEP model {}: {}", model, e);
                }
            }
        }
        self.sorted_models.sort();

        Ok(())
    }

    /// Load a single BPHX model from its JSON file.
    fn load_model(&self, model: &str) -> Result<BphxParameters, VendorError> {
        let model_path = self.data_path.join("bphx").join(format!("{}.json", model));

        let content = std::fs::read_to_string(&model_path).map_err(|e| VendorError::IoError {
            path: model_path.display().to_string(),
            source: e,
        })?;

        let params: BphxParameters = serde_json::from_str(&content)?;

        Ok(params)
    }
}

impl VendorBackend for SwepBackend {
    fn vendor_name(&self) -> &str {
        "SWEP"
    }

    fn list_compressor_models(&self) -> Result<Vec<String>, VendorError> {
        // SWEP does not provide compressor data
        Ok(vec![])
    }

    fn get_compressor_coefficients(
        &self,
        model: &str,
    ) -> Result<CompressorCoefficients, VendorError> {
        Err(VendorError::InvalidFormat(format!(
            "SWEP does not provide compressor data (requested: {})",
            model
        )))
    }

    fn list_bphx_models(&self) -> Result<Vec<String>, VendorError> {
        Ok(self.sorted_models.clone())
    }

    fn get_bphx_parameters(&self, model: &str) -> Result<BphxParameters, VendorError> {
        self.bphx_cache
            .get(model)
            .cloned()
            .ok_or_else(|| VendorError::ModelNotFound(model.to_string()))
    }

    fn compute_ua(&self, model: &str, params: &UaCalcParams) -> Result<f64, VendorError> {
        let bphx = self.get_bphx_parameters(model)?;
        match bphx.ua_curve {
            Some(ref curve) => {
                let ratio = if params.mass_flow_ref > 0.0 {
                    params.mass_flow / params.mass_flow_ref
                } else {
                    1.0
                };
                let ua_ratio = curve.interpolate(ratio).unwrap_or(1.0);
                Ok(bphx.ua_nominal * ua_ratio)
            }
            None => Ok(bphx.ua_nominal),
        }
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    #[test]
    fn test_swep_backend_new() {
        let backend = SwepBackend::new();
        assert!(backend.is_ok(), "SwepBackend::new() should succeed");
    }

    #[test]
    fn test_swep_vendor_name() {
        let backend = SwepBackend::new().unwrap();
        assert_eq!(backend.vendor_name(), "SWEP");
    }

    #[test]
    fn test_swep_list_bphx_models() {
        let backend = SwepBackend::new().unwrap();
        let models = backend.list_bphx_models().unwrap();
        assert_eq!(models.len(), 2);
        // Sorted order
        assert_eq!(models, vec!["B5THx20".to_string(), "B8THx30".to_string()]);
    }

    #[test]
    fn test_swep_get_bphx_b5thx20() {
        let backend = SwepBackend::new().unwrap();
        let params = backend.get_bphx_parameters("B5THx20").unwrap();

        assert_eq!(params.model, "B5THx20");
        assert_eq!(params.manufacturer, "SWEP");
        assert_eq!(params.num_plates, 20);
        assert!((params.area - 0.45).abs() < 1e-10);
        assert!((params.dh - 0.003).abs() < 1e-10);
        assert!((params.chevron_angle - 65.0).abs() < 1e-10);
        assert!((params.ua_nominal - 1500.0).abs() < 1e-10);
        assert!(params.ua_curve.is_some());
    }

    #[test]
    fn test_swep_get_bphx_b8thx30() {
        let backend = SwepBackend::new().unwrap();
        let params = backend.get_bphx_parameters("B8THx30").unwrap();

        assert_eq!(params.model, "B8THx30");
        assert_eq!(params.manufacturer, "SWEP");
        assert_eq!(params.num_plates, 30);
        assert!((params.area - 0.72).abs() < 1e-10);
        assert!((params.dh - 0.0025).abs() < 1e-10);
        assert!((params.chevron_angle - 60.0).abs() < 1e-10);
        assert!((params.ua_nominal - 2500.0).abs() < 1e-10);
        assert!(params.ua_curve.is_none());
    }

    #[test]
    fn test_swep_ua_curve_present() {
        let backend = SwepBackend::new().unwrap();
        let params = backend.get_bphx_parameters("B5THx20").unwrap();
        let curve = params.ua_curve.as_ref().unwrap();

        // Verify curve has correct number of points
        assert_eq!(curve.points.len(), 7);

        // Verify interpolation at known points
        assert!((curve.interpolate(1.0).unwrap() - 1.0).abs() < 1e-10);
        assert!((curve.interpolate(0.2).unwrap() - 0.30).abs() < 1e-10);
        assert!((curve.interpolate(1.5).unwrap() - 1.15).abs() < 1e-10);
    }

    #[test]
    fn test_swep_model_not_found() {
        let backend = SwepBackend::new().unwrap();
        let result = backend.get_bphx_parameters("NONEXISTENT");
        assert!(result.is_err());
        match result.unwrap_err() {
            VendorError::ModelNotFound(m) => assert_eq!(m, "NONEXISTENT"),
            other => panic!("Expected ModelNotFound, got: {:?}", other),
        }
    }

    #[test]
    fn test_swep_compute_ua_with_curve() {
        let backend = SwepBackend::new().unwrap();
        let params = UaCalcParams {
            mass_flow: 0.5,
            mass_flow_ref: 1.0,
            temperature_hot_in: 340.0,
            temperature_cold_in: 280.0,
            refrigerant: "R410A".into(),
        };
        let ua = backend.compute_ua("B5THx20", &params).unwrap();
        // mass_flow_ratio = 0.5 → interpolate between (0.4, 0.55) and (0.6, 0.72)
        // t = (0.5 - 0.4) / (0.6 - 0.4) = 0.5
        // ua_ratio = 0.55 + 0.5 * (0.72 - 0.55) = 0.55 + 0.085 = 0.635
        // ua = 1500.0 * 0.635 = 952.5
        assert!((ua - 952.5).abs() < 1e-10);
    }

    #[test]
    fn test_swep_compute_ua_without_curve() {
        let backend = SwepBackend::new().unwrap();
        let params = UaCalcParams {
            mass_flow: 0.3,
            mass_flow_ref: 0.5,
            temperature_hot_in: 340.0,
            temperature_cold_in: 280.0,
            refrigerant: "R410A".into(),
        };
        let ua = backend.compute_ua("B8THx30", &params).unwrap();
        // No curve → returns ua_nominal
        assert!((ua - 2500.0).abs() < 1e-10);
    }

    #[test]
    fn test_swep_compute_ua_model_not_found() {
        let backend = SwepBackend::new().unwrap();
        let params = UaCalcParams {
            mass_flow: 0.3,
            mass_flow_ref: 0.5,
            temperature_hot_in: 340.0,
            temperature_cold_in: 280.0,
            refrigerant: "R410A".into(),
        };
        let result = backend.compute_ua("NONEXISTENT", &params);
        assert!(result.is_err());
    }

    #[test]
    fn test_swep_list_compressor_models_empty() {
        let backend = SwepBackend::new().unwrap();
        let models = backend.list_compressor_models().unwrap();
        assert!(models.is_empty());
    }

    #[test]
    fn test_swep_get_compressor_returns_error() {
        let backend = SwepBackend::new().unwrap();
        let result = backend.get_compressor_coefficients("anything");
        assert!(result.is_err());
        match result.unwrap_err() {
            VendorError::InvalidFormat(msg) => {
                assert!(msg.contains("SWEP does not provide compressor"));
            }
            other => panic!("Expected InvalidFormat, got: {:?}", other),
        }
    }

    #[test]
    fn test_swep_object_safety() {
        let backend: Box<dyn VendorBackend> = Box::new(SwepBackend::new().unwrap());
        assert_eq!(backend.vendor_name(), "SWEP");
        let models = backend.list_bphx_models().unwrap();
        assert!(!models.is_empty());
    }
}
