//! BPHX (Brazed Plate Heat Exchanger) Geometry Definition
//!
//! Defines geometry parameters for brazed plate heat exchangers used in
//! evaporation, condensation, and generic heat transfer applications.
//!
//! ## Key Parameters
//!
//! - **Hydraulic diameter (dh)**: Characteristic length for flow calculations
//! - **Heat transfer area**: Total plate surface area for heat exchange
//! - **Chevron angle**: Plate corrugation angle (typically 30-65°)
//! - **Number of plates**: Total count of heat transfer plates

use std::fmt;

/// BPHX exchanger type classification
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BphxType {
    /// DX or flooded evaporation
    Evaporator,
    /// Condensation with subcooled liquid outlet
    Condenser,
    /// Generic heat transfer (default)
    #[default]
    Generic,
}

/// Geometry parameters for a Brazed Plate Heat Exchanger (BPHX)
///
/// The hydraulic diameter and heat transfer area are calculated from
/// plate dimensions and corrugation parameters.
///
/// # Example
///
/// ```
/// use entropyk_components::heat_exchanger::BphxGeometry;
///
/// let geo = BphxGeometry::new(30)
///     .with_plate_dimensions(0.5, 0.1)
///     .with_chevron_angle(60.0)
///     .build()
///     .expect("Valid geometry");
///
/// assert!(geo.dh > 0.0);
/// assert!(geo.area > 0.0);
/// ```
#[derive(Debug, Clone)]
pub struct BphxGeometry {
    /// Number of heat transfer plates
    pub n_plates: u32,
    /// Plate length (flow direction) in meters
    pub plate_length: f64,
    /// Plate width (perpendicular to flow) in meters
    pub plate_width: f64,
    /// Plate thickness in meters
    pub plate_thickness: f64,
    /// Chevron (corrugation) angle in degrees (typically 30-65°)
    pub chevron_angle: f64,
    /// Hydraulic diameter in meters (calculated)
    pub dh: f64,
    /// Total heat transfer area in m² (calculated)
    pub area: f64,
    /// Channel spacing (plate gap) in meters
    pub channel_spacing: f64,
    /// Corrugation pitch in meters
    pub corrugation_pitch: f64,
    /// Exchanger type classification
    pub exchanger_type: BphxType,
}

impl Default for BphxGeometry {
    fn default() -> Self {
        Self {
            n_plates: 20,
            plate_length: 0.3,
            plate_width: 0.1,
            plate_thickness: 0.0006,
            chevron_angle: 60.0,
            dh: 0.003,
            area: 0.5,
            channel_spacing: 0.002,
            corrugation_pitch: 0.006,
            exchanger_type: BphxType::Generic,
        }
    }
}

impl BphxGeometry {
    /// Minimum valid values for geometry parameters
    pub const MIN_PLATES: u32 = 1;
    /// Documentation pending
    pub const MIN_DIMENSION: f64 = 1e-6;
    /// Documentation pending
    pub const MIN_CHEVRON_ANGLE: f64 = 10.0;
    /// Documentation pending
    pub const MAX_CHEVRON_ANGLE: f64 = 80.0;

    /// Creates a new geometry builder with the specified number of plates.
    ///
    /// # Arguments
    ///
    /// * `n_plates` - Number of heat transfer plates (must be >= 1)
    ///
    /// # Example
    ///
    /// ```
    /// use entropyk_components::heat_exchanger::BphxGeometry;
    ///
    /// let geo = BphxGeometry::new(30);
    /// ```
    pub fn new(n_plates: u32) -> BphxGeometryBuilder {
        BphxGeometryBuilder {
            n_plates: n_plates.max(Self::MIN_PLATES),
            plate_length: None,
            plate_width: None,
            plate_thickness: 0.0006,
            chevron_angle: 60.0,
            channel_spacing: 0.002,
            corrugation_pitch: None,
            exchanger_type: BphxType::Generic,
        }
    }

    /// Creates a geometry from known hydraulic diameter and area.
    ///
    /// Use this when manufacturer data provides dh and area directly.
    ///
    /// # Arguments
    ///
    /// * `dh` - Hydraulic diameter in meters
    /// * `area` - Heat transfer area in m²
    /// * `n_plates` - Number of plates
    ///
    /// # Example
    ///
    /// ```
    /// use entropyk_components::heat_exchanger::BphxGeometry;
    ///
    /// let geo = BphxGeometry::from_dh_area(0.003, 0.5, 20);
    /// assert!((geo.dh - 0.003).abs() < 1e-10);
    /// ```
    pub fn from_dh_area(dh: f64, area: f64, n_plates: u32) -> Self {
        Self {
            n_plates: n_plates.max(Self::MIN_PLATES),
            dh: dh.max(Self::MIN_DIMENSION),
            area: area.max(Self::MIN_DIMENSION),
            plate_length: 0.3,
            plate_width: 0.1,
            plate_thickness: 0.0006,
            chevron_angle: 60.0,
            channel_spacing: 0.002,
            corrugation_pitch: 0.006,
            exchanger_type: BphxType::Generic,
        }
    }

    /// Sets the exchanger type.
    ///
    /// # Arguments
    ///
    /// * `exchanger_type` - The type of heat exchanger (Evaporator, Condenser, Generic)
    pub fn with_exchanger_type(mut self, exchanger_type: BphxType) -> Self {
        self.exchanger_type = exchanger_type;
        self
    }

    /// Returns the effective flow cross-sectional area per channel (m²).
    ///
    /// A_channel = channel_spacing × plate_width
    pub fn channel_flow_area(&self) -> f64 {
        self.channel_spacing * self.plate_width
    }

    /// Returns the total number of inter-plate gaps (n_plates − 1).
    pub fn n_channels(&self) -> u32 {
        self.n_plates.saturating_sub(1)
    }

    /// Channels available to **one** fluid in a single-pass counterflow pack.
    ///
    /// Alternating hot/cold channels split the gaps roughly in half:
    /// `⌊n_plates / 2⌋` (at least 1). Using the full `n_channels` for mass flux
    /// underestimates G by ~2× and ΔP by ~4×.
    pub fn n_channels_per_side(&self) -> u32 {
        (self.n_plates / 2).max(1)
    }

    /// Returns the mass flux for a given mass flow rate on one fluid side.
    ///
    /// G = m_dot / (A_channel × n_channels_per_side)
    ///
    /// # Arguments
    ///
    /// * `mass_flow` - Total mass flow rate on that side (kg/s)
    pub fn mass_flux(&self, mass_flow: f64) -> f64 {
        let n_channels = self.n_channels_per_side() as f64;
        if n_channels < 1e-10 {
            return 0.0;
        }
        let channel_area = self.channel_flow_area();
        mass_flow / (channel_area * n_channels)
    }

    /// Validates the geometry parameters.
    ///
    /// # Errors
    ///
    /// Returns an error if any parameter is invalid.
    pub fn validate(&self) -> Result<(), BphxGeometryError> {
        if self.n_plates < Self::MIN_PLATES {
            return Err(BphxGeometryError::InvalidPlates {
                n_plates: self.n_plates,
                min: Self::MIN_PLATES,
            });
        }
        if self.plate_length < Self::MIN_DIMENSION {
            return Err(BphxGeometryError::InvalidDimension {
                name: "plate_length",
                value: self.plate_length,
                min: Self::MIN_DIMENSION,
            });
        }
        if self.plate_width < Self::MIN_DIMENSION {
            return Err(BphxGeometryError::InvalidDimension {
                name: "plate_width",
                value: self.plate_width,
                min: Self::MIN_DIMENSION,
            });
        }
        if self.dh < Self::MIN_DIMENSION {
            return Err(BphxGeometryError::InvalidDimension {
                name: "dh",
                value: self.dh,
                min: Self::MIN_DIMENSION,
            });
        }
        if self.area < Self::MIN_DIMENSION {
            return Err(BphxGeometryError::InvalidDimension {
                name: "area",
                value: self.area,
                min: Self::MIN_DIMENSION,
            });
        }
        if self.chevron_angle < Self::MIN_CHEVRON_ANGLE
            || self.chevron_angle > Self::MAX_CHEVRON_ANGLE
        {
            return Err(BphxGeometryError::InvalidChevronAngle {
                angle: self.chevron_angle,
                min: Self::MIN_CHEVRON_ANGLE,
                max: Self::MAX_CHEVRON_ANGLE,
            });
        }
        Ok(())
    }
}

impl fmt::Display for BphxGeometry {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "BphxGeometry({} plates, dh={:.3}mm, A={:.3}m², β={:.0}°)",
            self.n_plates,
            self.dh * 1000.0,
            self.area,
            self.chevron_angle
        )
    }
}

/// Builder for BphxGeometry
#[derive(Debug, Clone)]
pub struct BphxGeometryBuilder {
    n_plates: u32,
    plate_length: Option<f64>,
    plate_width: Option<f64>,
    plate_thickness: f64,
    chevron_angle: f64,
    channel_spacing: f64,
    corrugation_pitch: Option<f64>,
    exchanger_type: BphxType,
}

impl BphxGeometryBuilder {
    /// Sets the plate dimensions (length and width in meters).
    pub fn with_plate_dimensions(mut self, length: f64, width: f64) -> Self {
        self.plate_length = Some(length.max(BphxGeometry::MIN_DIMENSION));
        self.plate_width = Some(width.max(BphxGeometry::MIN_DIMENSION));
        self
    }

    /// Sets the number of plates.
    pub fn with_plates(mut self, n: u32) -> Self {
        self.n_plates = n.max(BphxGeometry::MIN_PLATES);
        self
    }

    /// Sets the chevron (corrugation) angle in degrees.
    pub fn with_chevron_angle(mut self, degrees: f64) -> Self {
        self.chevron_angle = degrees.clamp(
            BphxGeometry::MIN_CHEVRON_ANGLE,
            BphxGeometry::MAX_CHEVRON_ANGLE,
        );
        self
    }

    /// Sets the plate thickness in meters.
    pub fn with_plate_thickness(mut self, thickness: f64) -> Self {
        self.plate_thickness = thickness.max(BphxGeometry::MIN_DIMENSION);
        self
    }

    /// Sets the channel spacing (plate gap) in meters.
    pub fn with_channel_spacing(mut self, spacing: f64) -> Self {
        self.channel_spacing = spacing.max(BphxGeometry::MIN_DIMENSION);
        self
    }

    /// Sets the corrugation pitch in meters.
    pub fn with_corrugation_pitch(mut self, pitch: f64) -> Self {
        self.corrugation_pitch = Some(pitch.max(BphxGeometry::MIN_DIMENSION));
        self
    }

    /// Sets the exchanger type.
    pub fn with_exchanger_type(mut self, exchanger_type: BphxType) -> Self {
        self.exchanger_type = exchanger_type;
        self
    }

    /// Builds the geometry, calculating dh and area from parameters.
    ///
    /// # Errors
    ///
    /// Returns an error if required parameters are missing or invalid.
    pub fn build(self) -> Result<BphxGeometry, BphxGeometryError> {
        let plate_length = self
            .plate_length
            .ok_or(BphxGeometryError::MissingParameter {
                name: "plate_length",
            })?;
        let plate_width = self
            .plate_width
            .ok_or(BphxGeometryError::MissingParameter {
                name: "plate_width",
            })?;

        let corrugation_pitch = self.corrugation_pitch.unwrap_or(2.0 * self.channel_spacing);

        let dh = 2.0 * self.channel_spacing;
        let n_channels = (self.n_plates.saturating_sub(1)) as f64;
        let area = 2.0 * n_channels * plate_length * plate_width;

        let geo = BphxGeometry {
            n_plates: self.n_plates,
            plate_length,
            plate_width,
            plate_thickness: self.plate_thickness,
            chevron_angle: self.chevron_angle,
            dh,
            area,
            channel_spacing: self.channel_spacing,
            corrugation_pitch,
            exchanger_type: self.exchanger_type,
        };

        geo.validate()?;
        Ok(geo)
    }
}

/// Errors for BPHX geometry validation
#[derive(Debug, Clone, thiserror::Error)]
pub enum BphxGeometryError {
    #[error("Invalid number of plates: {n_plates}, minimum is {min}")]
    /// Documentation pending
    InvalidPlates {
        /// Number of plates provided
        n_plates: u32,
        /// Minimum allowed plates (2)
        min: u32,
    },

    #[error("Invalid {name}: {value}, minimum is {min}")]
    /// Documentation pending
    InvalidDimension {
        /// Documentation pending
        name: &'static str,
        /// Documentation pending
        value: f64,
        /// Documentation pending
        min: f64,
    },

    #[error("Invalid chevron angle: {angle}°, valid range is {min}° to {max}°")]
    /// Documentation pending
    InvalidChevronAngle {
        /// Angle provided
        angle: f64,
        /// Minimum allowed angle
        min: f64,
        /// Maximum allowed angle
        max: f64,
    },

    #[error("Missing required parameter: {name}")]
    /// Documentation pending
    MissingParameter {
        /// Parameter name
        name: &'static str,
    },
}

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

    #[test]
    fn test_bphx_geometry_default() {
        let geo = BphxGeometry::default();
        assert!(geo.validate().is_ok());
        assert_eq!(geo.n_plates, 20);
        assert!((geo.chevron_angle - 60.0).abs() < 1e-10);
    }

    #[test]
    fn test_bphx_geometry_from_dh_area() {
        let geo = BphxGeometry::from_dh_area(0.003, 0.5, 20);
        assert!((geo.dh - 0.003).abs() < 1e-10);
        assert!((geo.area - 0.5).abs() < 1e-10);
        assert_eq!(geo.n_plates, 20);
    }

    #[test]
    fn test_bphx_geometry_builder() {
        let geo = BphxGeometry::new(30)
            .with_plate_dimensions(0.5, 0.1)
            .with_chevron_angle(60.0)
            .build()
            .expect("Valid geometry");

        assert_eq!(geo.n_plates, 30);
        assert!((geo.plate_length - 0.5).abs() < 1e-10);
        assert!((geo.plate_width - 0.1).abs() < 1e-10);
        assert!((geo.chevron_angle - 60.0).abs() < 1e-10);
        assert!(geo.dh > 0.0);
        assert!(geo.area > 0.0);
    }

    #[test]
    fn test_bphx_geometry_builder_missing_dimensions() {
        let result = BphxGeometry::new(20).build();
        assert!(result.is_err());
    }

    #[test]
    fn test_bphx_geometry_n_channels() {
        let geo = BphxGeometry::new(30)
            .with_plate_dimensions(0.5, 0.1)
            .build()
            .unwrap();
        assert_eq!(geo.n_channels(), 29);
    }

    #[test]
    fn test_bphx_geometry_mass_flux() {
        let geo = BphxGeometry::new(10)
            .with_plate_dimensions(0.3, 0.1)
            .with_channel_spacing(0.002)
            .build()
            .unwrap();

        let mass_flow = 0.1;
        // 10 plates → 5 channels per side; A_ch = 0.002 × 0.1 = 2e-4 m²
        // G = 0.1 / (2e-4 × 5) = 100 kg/(m²·s)
        let g = geo.mass_flux(mass_flow);
        assert!((g - 100.0).abs() < 1e-9, "got G={g}");
        assert_eq!(geo.n_channels_per_side(), 5);
        assert_eq!(geo.n_channels(), 9);
    }

    #[test]
    fn test_bphx_geometry_validate_invalid_plates() {
        let mut geo = BphxGeometry::default();
        geo.n_plates = 0;
        assert!(geo.validate().is_err());
    }

    #[test]
    fn test_bphx_geometry_validate_invalid_chevron() {
        let mut geo = BphxGeometry::default();
        geo.chevron_angle = 5.0;
        assert!(geo.validate().is_err());
    }

    #[test]
    fn test_bphx_geometry_with_exchanger_type() {
        let geo = BphxGeometry::new(20)
            .with_plate_dimensions(0.3, 0.1)
            .with_exchanger_type(BphxType::Evaporator)
            .build()
            .unwrap();
        assert_eq!(geo.exchanger_type, BphxType::Evaporator);
    }

    #[test]
    fn test_bphx_geometry_display() {
        let geo = BphxGeometry::default();
        let s = format!("{}", geo);
        assert!(s.contains("20 plates"));
        assert!(s.contains("60°"));
    }

    #[test]
    fn test_bphx_geometry_clamps_min_plates() {
        let geo = BphxGeometry::from_dh_area(0.003, 0.5, 1);
        assert_eq!(geo.n_plates, 1);
    }
}
