//! Bitzer compressor data backend.
//!
//! Loads AHRI 540 compressor coefficients from CSV files in the
//! `data/bitzer/compressors/` directory. The CSV columns `c0`..`c9` (capacity)
//! and `p0`..`p9` (power) are in AHRI 540 standard polynomial order:
//! Ts, Td, Ts², Ts·Td, Td², Ts³, Td·Ts², Ts·Td², Td³ (see [`CompressorCoefficients`]).

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

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

/// Backend for Bitzer compressor data.
///
/// Discovers compressor models by scanning `*.csv` files in `data/bitzer/compressors/`
/// and uses the file stem (e.g. `4NFC-20Y`) as the model id. Each CSV row is mapped
/// to AHRI 540 `CompressorCoefficients`.
///
/// # CSV format
///
/// Header row with columns: `model`, `manufacturer`, `refrigerant`, `c0`..`c9` (capacity),
/// `p0`..`p9` (power), `t_suction_min`, `t_suction_max`, `t_discharge_min`, `t_discharge_max`.
/// One data row per file (one model per CSV file).
///
/// # Example
///
/// ```no_run
/// use entropyk_vendors::compressors::bitzer::BitzerBackend;
/// use entropyk_vendors::VendorBackend;
///
/// let backend = BitzerBackend::new().expect("load bitzer data");
/// let models = backend.list_compressor_models().unwrap();
/// println!("Available: {:?}", models);
/// ```
#[derive(Debug)]
pub struct BitzerBackend {
    /// Root path to the Bitzer data directory.
    data_path: PathBuf,
    /// Pre-loaded compressor coefficients keyed by model name.
    compressor_cache: HashMap<String, CompressorCoefficients>,
    /// Sorted list of available models.
    sorted_models: Vec<String>,
}

impl BitzerBackend {
    /// Create a new Bitzer backend, loading all compressor 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("bitzer");

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

        backend.load_compressors()?;

        Ok(backend)
    }

    /// Create a new Bitzer 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,
            compressor_cache: HashMap::new(),
            sorted_models: Vec::new(),
        };

        backend.load_compressors()?;

        Ok(backend)
    }

    /// Discover CSV files in `data/bitzer/compressors/` and pre-cache all models.
    fn load_compressors(&mut self) -> Result<(), VendorError> {
        let compressors_dir = self.data_path.join("compressors");

        let entries = match std::fs::read_dir(&compressors_dir) {
            Ok(e) => e,
            Err(e) => {
                return Err(VendorError::IoError {
                    path: compressors_dir.display().to_string(),
                    source: e,
                });
            }
        };

        for entry in entries.flatten() {
            let path = entry.path();
            if path.extension().is_some_and(|e| e == "csv") {
                if let Some(stem) = path.file_stem() {
                    let model = stem.to_string_lossy().into_owned();
                    match self.load_model(&model) {
                        Ok(coeffs) => {
                            self.compressor_cache.insert(model.clone(), coeffs);
                            self.sorted_models.push(model);
                        }
                        Err(e) => {
                            log::warn!("[entropyk-vendors] Skipping Bitzer model {}: {}", model, e);
                        }
                    }
                }
            }
        }
        self.sorted_models.sort();

        Ok(())
    }

    /// Load a single compressor model from its CSV file.
    fn load_model(&self, model: &str) -> Result<CompressorCoefficients, VendorError> {
        if model.contains('/') || model.contains('\\') || model.contains("..") {
            return Err(VendorError::ModelNotFound(model.to_string()));
        }

        let model_path = self
            .data_path
            .join("compressors")
            .join(format!("{}.csv", model));

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

        parse_bitzer_csv(&content, model).map_err(|e| {
            VendorError::InvalidFormat(format!("Parse error in {}: {}", model_path.display(), e))
        })
    }
}

/// Parse a Bitzer CSV string into CompressorCoefficients.
///
/// CSV must have a header row and at least one data row (one model per file).
/// Only the first data row is used; additional rows are ignored.
/// Columns `c0`..`c9` and `p0`..`p9` are in AHRI 540 order: Ts, Td, Ts², Ts·Td, Td², Ts³, Td·Ts², Ts·Td², Td³.
/// The `model_id` argument is used as fallback when the CSV `model` column is missing.
fn parse_bitzer_csv(content: &str, model_id: &str) -> Result<CompressorCoefficients, String> {
    let mut rdr = csv::Reader::from_reader(content.as_bytes());
    let headers: Vec<String> = rdr
        .headers()
        .map_err(|e| e.to_string())?
        .iter()
        .map(String::from)
        .collect();

    let mut record = csv::StringRecord::new();
    if !rdr.read_record(&mut record).map_err(|e| e.to_string())? {
        return Err("CSV has no data row".to_string());
    }

    let get = |name: &str| -> Result<f64, String> {
        let i = headers
            .iter()
            .position(|h| h == name)
            .ok_or_else(|| format!("missing column {}", name))?;
        record
            .get(i)
            .ok_or_else(|| format!("missing value for {}", name))?
            .trim()
            .parse()
            .map_err(|e: std::num::ParseFloatError| e.to_string())
    };

    let get_str = |name: &str| -> Result<String, String> {
        let i = headers
            .iter()
            .position(|h| h == name)
            .ok_or_else(|| format!("missing column {}", name))?;
        record
            .get(i)
            .map(|s| s.trim().to_string())
            .ok_or_else(|| format!("missing value for {}", name))
    };

    let model = get_str("model").unwrap_or_else(|_| model_id.to_string());
    let manufacturer = get_str("manufacturer").unwrap_or_else(|_| "Bitzer".to_string());
    let refrigerant = get_str("refrigerant").unwrap_or_else(|_| "R134a".to_string());

    let mut capacity_coeffs = [0.0_f64; 10];
    for (i, c) in capacity_coeffs.iter_mut().enumerate() {
        *c = get(&format!("c{}", i))?;
    }
    let mut power_coeffs = [0.0_f64; 10];
    for (i, p) in power_coeffs.iter_mut().enumerate() {
        *p = get(&format!("p{}", i))?;
    }

    let validity = CompressorValidityRange {
        t_suction_min: get("t_suction_min")?,
        t_suction_max: get("t_suction_max")?,
        t_discharge_min: get("t_discharge_min")?,
        t_discharge_max: get("t_discharge_max")?,
    };

    if validity.t_suction_min > validity.t_suction_max {
        return Err(format!(
            "Invalid suction temperature range: min ({}) > max ({})",
            validity.t_suction_min, validity.t_suction_max
        ));
    }
    if validity.t_discharge_min > validity.t_discharge_max {
        return Err(format!(
            "Invalid discharge temperature range: min ({}) > max ({})",
            validity.t_discharge_min, validity.t_discharge_max
        ));
    }

    Ok(CompressorCoefficients {
        model,
        manufacturer,
        refrigerant,
        capacity_coeffs,
        power_coeffs,
        mass_flow_coeffs: None,
        validity,
    })
}

impl VendorBackend for BitzerBackend {
    fn vendor_name(&self) -> &str {
        "Bitzer"
    }

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

    fn get_compressor_coefficients(
        &self,
        model: &str,
    ) -> Result<CompressorCoefficients, VendorError> {
        self.compressor_cache
            .get(model)
            .cloned()
            .ok_or_else(|| VendorError::ModelNotFound(model.to_string()))
    }

    fn list_bphx_models(&self) -> Result<Vec<String>, VendorError> {
        Ok(vec![])
    }

    fn get_bphx_parameters(&self, model: &str) -> Result<BphxParameters, VendorError> {
        Err(VendorError::InvalidFormat(format!(
            "Bitzer does not provide BPHX data (requested: {})",
            model
        )))
    }

    fn compute_ua(&self, model: &str, _params: &UaCalcParams) -> Result<f64, VendorError> {
        Err(VendorError::InvalidFormat(format!(
            "Bitzer does not provide BPHX/UA data (requested: {})",
            model
        )))
    }
}

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

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

    #[test]
    fn test_bitzer_backend_from_path() {
        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 backend = BitzerBackend::from_path(base_path.join("bitzer"));
        assert!(backend.is_ok(), "BitzerBackend::from_path() should succeed");
    }

    #[test]
    fn test_bitzer_vendor_name() {
        let backend = BitzerBackend::new().unwrap();
        assert_eq!(backend.vendor_name(), "Bitzer");
    }

    #[test]
    fn test_bitzer_list_compressor_models() {
        let backend = BitzerBackend::new().unwrap();
        let models = backend.list_compressor_models().unwrap();
        assert_eq!(models.len(), 2);
        assert!(models.contains(&"4NFC-20Y".to_string()));
        assert!(models.contains(&"4HES-5Y".to_string()));
        assert_eq!(models, vec!["4HES-5Y".to_string(), "4NFC-20Y".to_string()]);
    }

    #[test]
    fn test_bitzer_get_compressor_4nfc_20y() {
        let backend = BitzerBackend::new().unwrap();
        let coeffs = backend.get_compressor_coefficients("4NFC-20Y").unwrap();

        assert_eq!(coeffs.model, "4NFC-20Y");
        assert_eq!(coeffs.manufacturer, "Bitzer");
        assert_eq!(coeffs.refrigerant, "R134a");
        assert_eq!(coeffs.capacity_coeffs.len(), 10);
        assert_eq!(coeffs.power_coeffs.len(), 10);
        assert!((coeffs.capacity_coeffs[0] - 32000.0).abs() < 1e-10);
        assert!((coeffs.power_coeffs[0] - 8200.0).abs() < 1e-10);
        assert!((coeffs.capacity_coeffs[9] - 0.01).abs() < 1e-10);
        assert!((coeffs.power_coeffs[9] - 0.006).abs() < 1e-10);
        assert!(coeffs.mass_flow_coeffs.is_none());
    }

    #[test]
    fn test_bitzer_get_compressor_4hes_5y() {
        let backend = BitzerBackend::new().unwrap();
        let coeffs = backend.get_compressor_coefficients("4HES-5Y").unwrap();

        assert_eq!(coeffs.model, "4HES-5Y");
        assert_eq!(coeffs.manufacturer, "Bitzer");
        assert_eq!(coeffs.refrigerant, "R410A");
        assert!((coeffs.capacity_coeffs[0] - 12000.0).abs() < 1e-10);
        assert!((coeffs.power_coeffs[0] - 3200.0).abs() < 1e-10);
    }

    #[test]
    fn test_bitzer_validity_range() {
        let backend = BitzerBackend::new().unwrap();
        let coeffs = backend.get_compressor_coefficients("4NFC-20Y").unwrap();

        assert!((coeffs.validity.t_suction_min - (-10.0)).abs() < 1e-10);
        assert!((coeffs.validity.t_suction_max - 15.0).abs() < 1e-10);
        assert!((coeffs.validity.t_discharge_min - 25.0).abs() < 1e-10);
        assert!((coeffs.validity.t_discharge_max - 60.0).abs() < 1e-10);
    }

    #[test]
    fn test_bitzer_model_not_found() {
        let backend = BitzerBackend::new().unwrap();
        let result = backend.get_compressor_coefficients("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_bitzer_list_bphx_empty() {
        let backend = BitzerBackend::new().unwrap();
        let models = backend.list_bphx_models().unwrap();
        assert!(models.is_empty());
    }

    #[test]
    fn test_bitzer_get_bphx_returns_error() {
        let backend = BitzerBackend::new().unwrap();
        let result = backend.get_bphx_parameters("anything");
        assert!(result.is_err());
        match result.unwrap_err() {
            VendorError::InvalidFormat(msg) => {
                assert!(msg.contains("Bitzer does not provide BPHX"));
            }
            other => panic!("Expected InvalidFormat, got: {:?}", other),
        }
    }

    #[test]
    fn test_bitzer_object_safety() {
        let backend: Box<dyn VendorBackend> = Box::new(BitzerBackend::new().unwrap());
        assert_eq!(backend.vendor_name(), "Bitzer");
        let models = backend.list_compressor_models().unwrap();
        assert!(!models.is_empty());
    }

    #[test]
    fn test_bitzer_csv_multiple_rows_first_used() {
        // When CSV has multiple data rows, only the first is used (one model per file).
        let csv = "model,manufacturer,refrigerant,c0,c1,c2,c3,c4,c5,c6,c7,c8,c9,p0,p1,p2,p3,p4,p5,p6,p7,p8,p9,t_suction_min,t_suction_max,t_discharge_min,t_discharge_max\n\
                   4NFC-20Y,Bitzer,R134a,32000.0,580.0,-150.0,4.2,2.5,-5.0,0.06,0.03,-0.02,0.01,8200.0,180.0,70.0,1.8,1.2,1.8,0.025,0.015,0.01,0.006,-10.0,15.0,25.0,60.0\n\
                   Other,Bitzer,R410A,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,-15.0,15.0,30.0,60.0";
        let result = parse_bitzer_csv(csv, "test");
        assert!(result.is_ok());
        let coeffs = result.unwrap();
        assert_eq!(coeffs.model, "4NFC-20Y");
        assert!((coeffs.capacity_coeffs[0] - 32000.0).abs() < 1e-10);
    }
}
