//! VendorBackend trait and associated data types.
//!
//! This module defines the uniform API for accessing manufacturer equipment data
//! (compressor coefficients, heat exchanger parameters) from vendor catalogs.

use serde::{Deserialize, Serialize};

use crate::error::VendorError;

// ---------------------------------------------------------------------------
// Compressor data types
// ---------------------------------------------------------------------------

/// AHRI 540 compressor coefficient set.
///
/// The 10 coefficients follow the polynomial form:
/// ```text
/// C = a₀ + a₁·Ts + a₂·Td + a₃·Ts² + a₄·Ts·Td + a₅·Td²
///   + a₆·Ts³ + a₇·Td·Ts² + a₈·Ts·Td² + a₉·Td³
/// ```
/// where `Ts` = suction saturation temperature (°C) and
/// `Td` = discharge saturation temperature (°C).
///
/// Both `capacity_coeffs` (W) and `power_coeffs` (W) use this convention.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CompressorCoefficients {
    /// Equipment model identifier (e.g. `"ZP54KCE-TFD"`).
    pub model: String,
    /// Manufacturer name (e.g. `"Copeland"`).
    pub manufacturer: String,
    /// Refrigerant designation (e.g. `"R410A"`).
    pub refrigerant: String,
    /// 10 AHRI 540 capacity polynomial coefficients (W).
    pub capacity_coeffs: [f64; 10],
    /// 10 AHRI 540 power polynomial coefficients (W).
    pub power_coeffs: [f64; 10],
    /// Optional 10 AHRI 540 mass-flow polynomial coefficients (kg/s).
    /// Not all vendors provide mass-flow polynomials.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub mass_flow_coeffs: Option<[f64; 10]>,
    /// Operating envelope for the coefficient set.
    pub validity: CompressorValidityRange,
}

/// Operating envelope for compressor coefficients.
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct CompressorValidityRange {
    /// Minimum suction saturation temperature (°C).
    pub t_suction_min: f64,
    /// Maximum suction saturation temperature (°C).
    pub t_suction_max: f64,
    /// Minimum discharge saturation temperature (°C).
    pub t_discharge_min: f64,
    /// Maximum discharge saturation temperature (°C).
    pub t_discharge_max: f64,
}

impl<'de> Deserialize<'de> for CompressorValidityRange {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        #[derive(Deserialize)]
        struct Inner {
            t_suction_min: f64,
            t_suction_max: f64,
            t_discharge_min: f64,
            t_discharge_max: f64,
        }

        let inner = Inner::deserialize(deserializer)?;

        if inner.t_suction_min > inner.t_suction_max {
            return Err(serde::de::Error::custom(format!(
                "Invalid suction temperature range: min ({} °C) > max ({} °C)",
                inner.t_suction_min, inner.t_suction_max
            )));
        }

        if inner.t_discharge_min > inner.t_discharge_max {
            return Err(serde::de::Error::custom(format!(
                "Invalid discharge temperature range: min ({} °C) > max ({} °C)",
                inner.t_discharge_min, inner.t_discharge_max
            )));
        }

        Ok(Self {
            t_suction_min: inner.t_suction_min,
            t_suction_max: inner.t_suction_max,
            t_discharge_min: inner.t_discharge_min,
            t_discharge_max: inner.t_discharge_max,
        })
    }
}

// ---------------------------------------------------------------------------
// BPHX data types
// ---------------------------------------------------------------------------

/// Brazed-plate heat exchanger (BPHX) parameters from vendor catalog.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct BphxParameters {
    /// Equipment model identifier (e.g. `"B5THx20"`).
    pub model: String,
    /// Manufacturer name (e.g. `"SWEP"`).
    pub manufacturer: String,
    /// Number of plates.
    pub num_plates: usize,
    /// Total heat-transfer area (m²).
    pub area: f64,
    /// Hydraulic diameter (m).
    pub dh: f64,
    /// Chevron angle (degrees).
    pub chevron_angle: f64,
    /// Nominal overall heat-transfer coefficient × area product (W/K)
    /// at reference operating conditions.
    pub ua_nominal: f64,
    /// Optional UA part-load curve for off-design conditions.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub ua_curve: Option<UaCurve>,
}

/// UA part-load correction curve.
///
/// Each point maps a mass-flow ratio (actual / reference) to a UA ratio
/// (actual UA / nominal UA). Linear interpolation is used between points.
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct UaCurve {
    /// Sorted list of `(mass_flow_ratio, ua_ratio)` points.
    pub points: Vec<(f64, f64)>,
}

impl<'de> Deserialize<'de> for UaCurve {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        #[derive(Deserialize)]
        struct Inner {
            points: Vec<(f64, f64)>,
        }

        let mut inner = Inner::deserialize(deserializer)?;

        // Sort points by mass_flow_ratio (x-axis) to ensure interpolation works correctly
        // even if the JSON is out of order. f64 sorting requires partial_cmp.
        inner
            .points
            .sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));

        Ok(Self {
            points: inner.points,
        })
    }
}

impl UaCurve {
    /// Linearly interpolate the UA ratio for a given mass-flow ratio.
    ///
    /// Values outside the curve range are clamped to the nearest endpoint.
    /// Returns `None` if the curve has no points.
    pub fn interpolate(&self, mass_flow_ratio: f64) -> Option<f64> {
        let pts = &self.points;
        if pts.is_empty() {
            return None;
        }
        if pts.len() == 1 {
            return Some(pts[0].1);
        }

        // Clamp to endpoints
        if mass_flow_ratio <= pts[0].0 {
            return Some(pts[0].1);
        }
        if mass_flow_ratio >= pts[pts.len() - 1].0 {
            return Some(pts[pts.len() - 1].1);
        }

        // Find enclosing interval and interpolate
        for window in pts.windows(2) {
            let (x0, y0) = window[0];
            let (x1, y1) = window[1];
            if mass_flow_ratio >= x0 && mass_flow_ratio <= x1 {
                let t = (mass_flow_ratio - x0) / (x1 - x0);
                return Some(y0 + t * (y1 - y0));
            }
        }

        // Fallback (should not be reached with sorted points)
        Some(pts[pts.len() - 1].1)
    }
}

/// Input parameters for a vendor-specific UA calculation.
#[derive(Debug, Clone)]
pub struct UaCalcParams {
    /// Current mass flow rate (kg/s).
    pub mass_flow: f64,
    /// Reference mass flow rate (kg/s) at which `ua_nominal` was measured.
    pub mass_flow_ref: f64,
    /// Hot-side inlet temperature (K).
    pub temperature_hot_in: f64,
    /// Cold-side inlet temperature (K).
    pub temperature_cold_in: f64,
    /// Refrigerant designation.
    pub refrigerant: String,
}

// ---------------------------------------------------------------------------
// VendorBackend trait
// ---------------------------------------------------------------------------

/// Trait for accessing manufacturer equipment data.
///
/// Implementations load catalog data from vendor-specific file formats
/// (JSON, CSV, etc.) and expose a uniform query API.
///
/// The trait is object-safe (`Send + Sync`) so it can be stored as
/// `Box<dyn VendorBackend>` or `Arc<dyn VendorBackend>`.
pub trait VendorBackend: Send + Sync {
    /// Human-readable name of the vendor (e.g. `"Copeland (Emerson)"`).
    fn vendor_name(&self) -> &str;

    /// List all compressor models available in this vendor's catalog.
    fn list_compressor_models(&self) -> Result<Vec<String>, VendorError>;

    /// Retrieve AHRI 540 coefficients for a specific compressor model.
    fn get_compressor_coefficients(
        &self,
        model: &str,
    ) -> Result<CompressorCoefficients, VendorError>;

    /// List all BPHX models available in this vendor's catalog.
    fn list_bphx_models(&self) -> Result<Vec<String>, VendorError>;

    /// Retrieve catalog parameters for a specific BPHX model.
    fn get_bphx_parameters(&self, model: &str) -> Result<BphxParameters, VendorError>;

    /// Compute UA for a given operating point using vendor-specific methods.
    ///
    /// The default implementation simply returns `ua_nominal`.
    fn compute_ua(&self, model: &str, _params: &UaCalcParams) -> Result<f64, VendorError> {
        let bphx = self.get_bphx_parameters(model)?;
        Ok(bphx.ua_nominal)
    }
}

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

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

    // ---- Serialization round-trip: CompressorCoefficients ----

    #[test]
    fn test_compressor_coefficients_json_round_trip() {
        let coeffs = CompressorCoefficients {
            model: "ZP54KCE-TFD".into(),
            manufacturer: "Copeland".into(),
            refrigerant: "R410A".into(),
            capacity_coeffs: [
                18000.0, 350.0, -120.0, 2.5, 1.8, -4.2, 0.05, 0.03, -0.02, 0.01,
            ],
            power_coeffs: [4500.0, 95.0, 45.0, 0.8, 0.5, 1.2, 0.02, 0.01, 0.01, 0.005],
            mass_flow_coeffs: None,
            validity: CompressorValidityRange {
                t_suction_min: -10.0,
                t_suction_max: 20.0,
                t_discharge_min: 25.0,
                t_discharge_max: 65.0,
            },
        };

        let json = serde_json::to_string_pretty(&coeffs).expect("serialize");
        let deserialized: CompressorCoefficients =
            serde_json::from_str(&json).expect("deserialize");
        assert_eq!(coeffs, deserialized);
    }

    #[test]
    fn test_compressor_coefficients_with_mass_flow() {
        let coeffs = CompressorCoefficients {
            model: "TEST-001".into(),
            manufacturer: "TestVendor".into(),
            refrigerant: "R134a".into(),
            capacity_coeffs: [1.0; 10],
            power_coeffs: [2.0; 10],
            mass_flow_coeffs: Some([0.1; 10]),
            validity: CompressorValidityRange {
                t_suction_min: -15.0,
                t_suction_max: 15.0,
                t_discharge_min: 30.0,
                t_discharge_max: 60.0,
            },
        };

        let json = serde_json::to_string(&coeffs).expect("serialize");
        let deserialized: CompressorCoefficients =
            serde_json::from_str(&json).expect("deserialize");
        assert_eq!(coeffs, deserialized);
        assert!(deserialized.mass_flow_coeffs.is_some());
    }

    // ---- Serialization round-trip: BphxParameters ----

    #[test]
    fn test_bphx_parameters_json_round_trip_without_curve() {
        let params = BphxParameters {
            model: "B5THx20".into(),
            manufacturer: "SWEP".into(),
            num_plates: 20,
            area: 0.45,
            dh: 0.003,
            chevron_angle: 65.0,
            ua_nominal: 1500.0,
            ua_curve: None,
        };

        let json = serde_json::to_string_pretty(&params).expect("serialize");
        let deserialized: BphxParameters = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(params, deserialized);
        assert!(deserialized.ua_curve.is_none());
    }

    #[test]
    fn test_bphx_parameters_json_round_trip_with_curve() {
        let params = BphxParameters {
            model: "B8THx30".into(),
            manufacturer: "SWEP".into(),
            num_plates: 30,
            area: 0.72,
            dh: 0.0025,
            chevron_angle: 60.0,
            ua_nominal: 2500.0,
            ua_curve: Some(UaCurve {
                points: vec![(0.2, 0.3), (0.5, 0.65), (1.0, 1.0), (1.5, 1.2)],
            }),
        };

        let json = serde_json::to_string_pretty(&params).expect("serialize");
        let deserialized: BphxParameters = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(params, deserialized);
        assert!(deserialized.ua_curve.is_some());
    }

    // ---- UaCurve interpolation ----

    #[test]
    fn test_ua_curve_interpolation_midpoint() {
        let curve = UaCurve {
            points: vec![(0.0, 0.0), (1.0, 1.0), (2.0, 1.5)],
        };

        // Exact point
        assert_eq!(curve.interpolate(0.0), Some(0.0));
        assert_eq!(curve.interpolate(1.0), Some(1.0));
        assert_eq!(curve.interpolate(2.0), Some(1.5));

        // Midpoint
        let mid = curve.interpolate(0.5).unwrap();
        assert!((mid - 0.5).abs() < 1e-10);

        let mid2 = curve.interpolate(1.5).unwrap();
        assert!((mid2 - 1.25).abs() < 1e-10);
    }

    #[test]
    fn test_ua_curve_interpolation_clamp() {
        let curve = UaCurve {
            points: vec![(0.5, 0.4), (1.0, 1.0), (1.5, 1.3)],
        };

        // Below range → clamp to first
        assert_eq!(curve.interpolate(0.1), Some(0.4));

        // Above range → clamp to last
        assert_eq!(curve.interpolate(3.0), Some(1.3));
    }

    #[test]
    fn test_ua_curve_interpolation_empty() {
        let curve = UaCurve { points: vec![] };
        assert_eq!(curve.interpolate(1.0), None);
    }

    #[test]
    fn test_ua_curve_interpolation_single_point() {
        let curve = UaCurve {
            points: vec![(1.0, 0.8)],
        };
        assert_eq!(curve.interpolate(0.5), Some(0.8));
        assert_eq!(curve.interpolate(2.0), Some(0.8));
    }

    // ---- VendorError display ----

    #[test]
    fn test_vendor_error_display_model_not_found() {
        let err = VendorError::ModelNotFound("XYZ-999".into());
        assert_eq!(err.to_string(), "Model not found: XYZ-999");
    }

    #[test]
    fn test_vendor_error_display_invalid_format() {
        let err = VendorError::InvalidFormat("missing capacity_coeffs".into());
        assert_eq!(
            err.to_string(),
            "Invalid data format: missing capacity_coeffs"
        );
    }

    #[test]
    fn test_vendor_error_display_file_not_found() {
        let err = VendorError::FileNotFound("/data/missing.json".into());
        assert_eq!(err.to_string(), "Data file not found: /data/missing.json");
    }

    // ---- Mock VendorBackend ----

    struct MockVendor {
        compressors: std::collections::HashMap<String, CompressorCoefficients>,
        bphx: std::collections::HashMap<String, BphxParameters>,
    }

    impl MockVendor {
        fn new() -> Self {
            let mut compressors = std::collections::HashMap::new();
            compressors.insert(
                "TEST-COMP".into(),
                CompressorCoefficients {
                    model: "TEST-COMP".into(),
                    manufacturer: "MockVendor".into(),
                    refrigerant: "R410A".into(),
                    capacity_coeffs: [1.0; 10],
                    power_coeffs: [2.0; 10],
                    mass_flow_coeffs: None,
                    validity: CompressorValidityRange {
                        t_suction_min: -10.0,
                        t_suction_max: 20.0,
                        t_discharge_min: 25.0,
                        t_discharge_max: 65.0,
                    },
                },
            );

            let mut bphx = std::collections::HashMap::new();
            bphx.insert(
                "TEST-HX".into(),
                BphxParameters {
                    model: "TEST-HX".into(),
                    manufacturer: "MockVendor".into(),
                    num_plates: 20,
                    area: 0.5,
                    dh: 0.003,
                    chevron_angle: 65.0,
                    ua_nominal: 2000.0,
                    ua_curve: None,
                },
            );

            Self { compressors, bphx }
        }
    }

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

        fn list_compressor_models(&self) -> Result<Vec<String>, VendorError> {
            Ok(self.compressors.keys().cloned().collect())
        }

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

        fn list_bphx_models(&self) -> Result<Vec<String>, VendorError> {
            Ok(self.bphx.keys().cloned().collect())
        }

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

    #[test]
    fn test_mock_vendor_list_compressors() {
        let vendor = MockVendor::new();
        let models = vendor.list_compressor_models().unwrap();
        assert_eq!(models.len(), 1);
        assert!(models.contains(&"TEST-COMP".to_string()));
    }

    #[test]
    fn test_mock_vendor_get_compressor() {
        let vendor = MockVendor::new();
        let coeffs = vendor.get_compressor_coefficients("TEST-COMP").unwrap();
        assert_eq!(coeffs.model, "TEST-COMP");
        assert_eq!(coeffs.manufacturer, "MockVendor");
        assert_eq!(coeffs.refrigerant, "R410A");
    }

    #[test]
    fn test_mock_vendor_compressor_not_found() {
        let vendor = MockVendor::new();
        let result = vendor.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_mock_vendor_list_bphx() {
        let vendor = MockVendor::new();
        let models = vendor.list_bphx_models().unwrap();
        assert_eq!(models.len(), 1);
        assert!(models.contains(&"TEST-HX".to_string()));
    }

    #[test]
    fn test_mock_vendor_get_bphx() {
        let vendor = MockVendor::new();
        let params = vendor.get_bphx_parameters("TEST-HX").unwrap();
        assert_eq!(params.model, "TEST-HX");
        assert_eq!(params.num_plates, 20);
    }

    #[test]
    fn test_mock_vendor_bphx_not_found() {
        let vendor = MockVendor::new();
        let result = vendor.get_bphx_parameters("NONEXISTENT");
        assert!(result.is_err());
    }

    #[test]
    fn test_default_compute_ua_returns_nominal() {
        let vendor = MockVendor::new();
        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 = vendor.compute_ua("TEST-HX", &params).unwrap();
        assert!((ua - 2000.0).abs() < 1e-10);
    }

    #[test]
    fn test_default_compute_ua_model_not_found() {
        let vendor = MockVendor::new();
        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 = vendor.compute_ua("NONEXISTENT", &params);
        assert!(result.is_err());
    }

    // ---- Object safety ----

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