//! BPHX channel pressure-drop correlations (friction), separate from HTC.
//!
//! Pressure-drop form (Fanning-like `f`, matching existing SimplifiedChannel):
//! `ΔP = z_dp · 2 · f · L · G² / (ρ · d_h)`.
//!
//! Martin (1996/1999) returns a Fanning friction factor from the reciprocal-
//! square-root chevron model (same intermediate `f` as `fluids` /
//! `ht.friction_plate_Martin_1999` before the ×4 Darcy conversion).

use super::bphx_geometry::BphxGeometry;
use crate::ComponentError;
use std::f64::consts::PI;

/// Reference dynamic viscosity [Pa·s] used for Re in channel DP models.
/// Matches the historical SimplifiedChannel implementation (Newton-friendly constant).
pub const BPHX_DP_MU_REF_PA_S: f64 = 0.0002;

/// Selectable BPHX frictional pressure-drop correlation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BphxDpCorrelation {
    /// Existing Blasius/laminar C¹ blend (default).
    #[default]
    SimplifiedChannel,
    /// Holger Martin chevron-plate single-phase friction (1996 / 1999 form).
    Martin1996,
}

impl BphxDpCorrelation {
    /// Stable serialization name.
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::SimplifiedChannel => "SimplifiedChannel",
            Self::Martin1996 => "Martin1996",
        }
    }

    /// Parse CLI/UI identifiers. Rejects unknown strings (no silent fallback).
    pub fn parse(name: &str) -> Result<Self, String> {
        let key = name
            .trim()
            .to_ascii_lowercase()
            .replace(['-', '_', ' '], "");
        match key.as_str() {
            "simplifiedchannel" | "simplified" | "channel" | "default" => {
                Ok(Self::SimplifiedChannel)
            }
            "martin1996" | "martin1999" | "martin" => Ok(Self::Martin1996),
            _ => Err(format!(
                "unsupported dp_correlation '{name}'. Use 'SimplifiedChannel' or 'Martin1996'."
            )),
        }
    }
}

/// Evaluated channel pressure drop and ∂ΔP/∂G at fixed ρ and z_dp.
#[derive(Debug, Clone, Copy)]
pub struct BphxDpEval {
    /// Pressure drop [Pa] (always ≥ 0 for the channel friction model).
    pub delta_p_pa: f64,
    /// ∂ΔP/∂G [Pa / (kg/(m²·s))].
    pub d_delta_p_d_g: f64,
}

/// Fanning friction factor and ∂f/∂Re for the selected correlation.
fn fanning_friction(
    corr: BphxDpCorrelation,
    re: f64,
    chevron_angle_deg: f64,
) -> Result<(f64, f64), ComponentError> {
    match corr {
        BphxDpCorrelation::SimplifiedChannel => Ok(simplified_channel_fanning(re)),
        BphxDpCorrelation::Martin1996 => {
            validate_martin_chevron(chevron_angle_deg)?;
            Ok(martin1996_fanning(re, chevron_angle_deg))
        }
    }
}

fn validate_martin_chevron(angle_deg: f64) -> Result<(), ComponentError> {
    if !angle_deg.is_finite() || !(0.0..90.0).contains(&angle_deg) {
        return Err(ComponentError::InvalidState(format!(
            "Martin1996 requires chevron_angle in (0, 90) degrees (got {angle_deg})"
        )));
    }
    Ok(())
}

/// SimplifiedChannel: laminar 64/Re, Blasius 0.079 Re^−0.25, C¹ blend on [2300, 4000].
///
/// Note: laminar uses the Darcy 64/Re coefficient inside the Fanning ΔP form —
/// historical Entropyk convention; kept identical for continuity.
fn simplified_channel_fanning(re: f64) -> (f64, f64) {
    const RE_LAMINAR: f64 = 2300.0;
    const RE_TURBULENT: f64 = 4000.0;
    let re_safe = re.max(1e-9);
    let f_lam = 64.0 / re_safe;
    let df_lam = -64.0 / (re_safe * re_safe);

    if re <= RE_LAMINAR {
        return (f_lam, df_lam);
    }

    let f_turb = 0.079 * re_safe.powf(-0.25);
    let df_turb = 0.079 * (-0.25) * re_safe.powf(-1.25);

    if re >= RE_TURBULENT {
        return (f_turb, df_turb);
    }

    let f = entropyk_core::smoothing::cubic_blend(f_lam, f_turb, re, RE_LAMINAR, RE_TURBULENT);
    // d/dRe of blend(f_lam(Re), f_turb(Re), Re): product rule on cubic Hermite.
    let df_blend_dx = entropyk_core::smoothing::cubic_blend_derivative(
        f_lam,
        f_turb,
        re,
        RE_LAMINAR,
        RE_TURBULENT,
    );
    // cubic_blend treats endpoints as constants w.r.t x; add chain-rule terms for
    // f_lam(Re) and f_turb(Re) via the complementary weights.
    let t = ((re - RE_LAMINAR) / (RE_TURBULENT - RE_LAMINAR)).clamp(0.0, 1.0);
    let w_b = t * t * (3.0 - 2.0 * t); // weight on b (= f_turb)
    let w_a = 1.0 - w_b;
    let df = df_blend_dx + w_a * df_lam + w_b * df_turb;
    (f, df)
}

/// Martin 1996/1999 Fanning friction factor vs Re and chevron angle φ [deg].
///
/// ```text
/// 1/√f = cosφ / √(0.045 tanφ + 0.09 sinφ + f0/cosφ) + (1−cosφ) / √(3.8 f1)
/// ```
/// with piecewise `f0`, `f1` (Shah/Martin 1999 appendix form).
/// Laminar/turbulent `f0`/`f1` branches are C¹-blended on Re ∈ [1800, 2200]
/// (same Newton rationale as SimplifiedChannel’s [2300, 4000] blend).
fn martin1996_fanning(re: f64, chevron_angle_deg: f64) -> (f64, f64) {
    const RE_LO: f64 = 1800.0;
    const RE_HI: f64 = 2200.0;
    let re_safe = re.max(1e-9);

    let lam = |re_v: f64| -> (f64, f64, f64, f64) {
        let f0 = 16.0 / re_v;
        let df0 = -16.0 / (re_v * re_v);
        let f1 = 149.0 / re_v + 0.9625;
        let df1 = -149.0 / (re_v * re_v);
        (f0, df0, f1, df1)
    };
    let turb = |re_v: f64| -> (f64, f64, f64, f64) {
        let u = 1.56 * re_v.ln() - 3.0;
        let u_safe = if u.abs() < 1e-12 {
            if u >= 0.0 {
                1e-12
            } else {
                -1e-12
            }
        } else {
            u
        };
        let f0 = u_safe.powi(-2);
        let df0 = -2.0 * u_safe.powi(-3) * (1.56 / re_v);
        let f1 = 9.75 * re_v.powf(-0.289);
        let df1 = 9.75 * (-0.289) * re_v.powf(-1.289);
        (f0, df0, f1, df1)
    };

    let (f0, df0, f1, df1) = if re_safe <= RE_LO {
        lam(re_safe)
    } else if re_safe >= RE_HI {
        turb(re_safe)
    } else {
        let (f0_l, df0_l, f1_l, df1_l) = lam(re_safe);
        let (f0_t, df0_t, f1_t, df1_t) = turb(re_safe);
        let f0 = entropyk_core::smoothing::cubic_blend(f0_l, f0_t, re_safe, RE_LO, RE_HI);
        let f1 = entropyk_core::smoothing::cubic_blend(f1_l, f1_t, re_safe, RE_LO, RE_HI);
        let t = ((re_safe - RE_LO) / (RE_HI - RE_LO)).clamp(0.0, 1.0);
        let w_b = t * t * (3.0 - 2.0 * t);
        let w_a = 1.0 - w_b;
        let df0_blend =
            entropyk_core::smoothing::cubic_blend_derivative(f0_l, f0_t, re_safe, RE_LO, RE_HI);
        let df1_blend =
            entropyk_core::smoothing::cubic_blend_derivative(f1_l, f1_t, re_safe, RE_LO, RE_HI);
        let df0 = df0_blend + w_a * df0_l + w_b * df0_t;
        let df1 = df1_blend + w_a * df1_l + w_b * df1_t;
        (f0, df0, f1, df1)
    };

    let phi = chevron_angle_deg * PI / 180.0;
    let cos_p = phi.cos();
    let sin_p = phi.sin();
    let tan_p = phi.tan();
    // Guard grazing angles so /cosφ stays finite inside (0, 90).
    let cos_safe = cos_p.signum() * cos_p.abs().max(1e-12);

    let a = 0.045 * tan_p + 0.09 * sin_p + f0 / cos_safe;
    let b = 3.8 * f1.max(1e-30);
    let a_safe = a.max(1e-30);

    let term0 = cos_p / a_safe.sqrt();
    let term1 = (1.0 - cos_p) / b.sqrt();
    let rhs = term0 + term1;
    let rhs_safe = rhs.max(1e-30);
    let f = rhs_safe.powi(-2);

    let da_dre = df0 / cos_safe;
    let db_dre = 3.8 * df1;
    let dterm0 = cos_p * (-0.5) * a_safe.powf(-1.5) * da_dre;
    let dterm1 = (1.0 - cos_p) * (-0.5) * b.powf(-1.5) * db_dre;
    let drhs = dterm0 + dterm1;
    let df = -2.0 * rhs_safe.powi(-3) * drhs;
    (f, df)
}

/// Channel frictional ΔP and ∂ΔP/∂G for the given correlation.
///
/// `mass_flux` may be signed; Re uses `|G|`, ΔP uses `G²` (existing convention).
pub fn evaluate_channel_pressure_drop(
    corr: BphxDpCorrelation,
    geometry: &BphxGeometry,
    mass_flux: f64,
    rho: f64,
    z_dp: f64,
) -> Result<BphxDpEval, ComponentError> {
    if !mass_flux.is_finite() {
        return Ok(BphxDpEval {
            delta_p_pa: 0.0,
            d_delta_p_d_g: 0.0,
        });
    }
    if !rho.is_finite() || rho < 1e-10 || geometry.dh < 1e-10 {
        return Ok(BphxDpEval {
            delta_p_pa: 0.0,
            d_delta_p_d_g: 0.0,
        });
    }
    if !geometry.plate_length.is_finite() || geometry.plate_length <= 0.0 {
        return Ok(BphxDpEval {
            delta_p_pa: 0.0,
            d_delta_p_d_g: 0.0,
        });
    }
    if !z_dp.is_finite() || z_dp < 0.0 {
        return Err(ComponentError::InvalidState(format!(
            "BPHX z_dp must be finite and >= 0 (got {z_dp})"
        )));
    }

    let g_abs = mass_flux.abs();
    let re = g_abs * geometry.dh / BPHX_DP_MU_REF_PA_S;
    let (f, df_dre) = fanning_friction(corr, re, geometry.chevron_angle)?;

    // ΔP = z_dp · 2 f L G² / (ρ dh)
    let k = z_dp * 2.0 * geometry.plate_length / (rho * geometry.dh);
    let g2 = mass_flux * mass_flux;
    let delta_p_pa = k * f * g2;

    // dRe/dG = sign(G) · dh/μ  (0 at stagnation)
    let d_re_d_g = if g_abs < 1e-30 {
        0.0
    } else {
        mass_flux.signum() * geometry.dh / BPHX_DP_MU_REF_PA_S
    };
    // d(G²)/dG = 2 G
    let d_delta_p_d_g = k * (df_dre * d_re_d_g * g2 + f * 2.0 * mass_flux);

    Ok(BphxDpEval {
        delta_p_pa,
        d_delta_p_d_g,
    })
}

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

    fn geo() -> BphxGeometry {
        BphxGeometry::from_dh_area(0.003, 0.5, 20)
    }

    #[test]
    fn parse_accepts_aliases_rejects_unknown() {
        assert_eq!(
            BphxDpCorrelation::parse("SimplifiedChannel").unwrap(),
            BphxDpCorrelation::SimplifiedChannel
        );
        assert_eq!(
            BphxDpCorrelation::parse("martin-1996").unwrap(),
            BphxDpCorrelation::Martin1996
        );
        assert!(BphxDpCorrelation::parse("Amalfi2016").is_err());
        assert!(BphxDpCorrelation::parse("friedel").is_err());
    }

    #[test]
    fn simplified_matches_historical_scale() {
        let g = 30.0;
        let rho = 1100.0;
        let eval = evaluate_channel_pressure_drop(
            BphxDpCorrelation::SimplifiedChannel,
            &geo(),
            g,
            rho,
            1.0,
        )
        .unwrap();
        assert!(eval.delta_p_pa > 0.0);
        assert!(eval.delta_p_pa.is_finite());
    }

    #[test]
    fn martin_differs_from_simplified_same_state() {
        let g = 80.0;
        let rho = 1000.0;
        let simp = evaluate_channel_pressure_drop(
            BphxDpCorrelation::SimplifiedChannel,
            &geo(),
            g,
            rho,
            1.0,
        )
        .unwrap();
        let martin =
            evaluate_channel_pressure_drop(BphxDpCorrelation::Martin1996, &geo(), g, rho, 1.0)
                .unwrap();
        assert!(
            (simp.delta_p_pa - martin.delta_p_pa).abs() > 1.0,
            "Martin and Simplified should differ: {} vs {}",
            simp.delta_p_pa,
            martin.delta_p_pa
        );
    }

    #[test]
    fn z_dp_scales_linearly() {
        let g = 40.0;
        let rho = 1100.0;
        let a = evaluate_channel_pressure_drop(
            BphxDpCorrelation::SimplifiedChannel,
            &geo(),
            g,
            rho,
            1.0,
        )
        .unwrap();
        let b = evaluate_channel_pressure_drop(
            BphxDpCorrelation::SimplifiedChannel,
            &geo(),
            g,
            rho,
            0.5,
        )
        .unwrap();
        assert!((b.delta_p_pa - 0.5 * a.delta_p_pa).abs() < 1e-9 * a.delta_p_pa.max(1.0));
    }

    #[test]
    fn zero_flow_smooth() {
        let eval = evaluate_channel_pressure_drop(
            BphxDpCorrelation::SimplifiedChannel,
            &geo(),
            0.0,
            1100.0,
            1.0,
        )
        .unwrap();
        assert_eq!(eval.delta_p_pa, 0.0);
        assert!(eval.d_delta_p_d_g.is_finite());
    }

    #[test]
    fn martin_rejects_invalid_chevron() {
        let mut g = geo();
        g.chevron_angle = f64::NAN;
        let err =
            evaluate_channel_pressure_drop(BphxDpCorrelation::Martin1996, &g, 30.0, 1000.0, 1.0)
                .unwrap_err();
        assert!(format!("{err}").contains("chevron"));
    }

    #[test]
    fn analytic_d_dg_matches_central_difference() {
        let geo = geo();
        let g0 = 55.0;
        let rho = 1050.0;
        for corr in [
            BphxDpCorrelation::SimplifiedChannel,
            BphxDpCorrelation::Martin1996,
        ] {
            let eval = evaluate_channel_pressure_drop(corr, &geo, g0, rho, 1.0).unwrap();
            let h = 1e-4;
            let plus = evaluate_channel_pressure_drop(corr, &geo, g0 + h, rho, 1.0).unwrap();
            let minus = evaluate_channel_pressure_drop(corr, &geo, g0 - h, rho, 1.0).unwrap();
            let fd = (plus.delta_p_pa - minus.delta_p_pa) / (2.0 * h);
            let rel = (eval.d_delta_p_d_g - fd).abs() / fd.abs().max(1.0);
            assert!(
                rel < 1e-4,
                "{:?}: analytic {} vs fd {} (rel {})",
                corr,
                eval.d_delta_p_d_g,
                fd,
                rel
            );
        }
    }
}
