//! C¹/C∞ flow regularization for zero-flow-safe heat-exchanger residuals.
//!
//! **Reference specialized pattern** (Story 0.2): keep these HX helpers here;
//! the canonical general toolkit is [`entropyk_core::smoothing`]. See
//! `docs/components/phantom-gradient-regularization.md`.
//!
//! Zero mass flow is a **valid** operating state (circuit staging, isolation,
//! Newton trial steps). Hard branches `if |m| < ε { Q = 0 }` create Jacobian
//! discontinuities and can leave energy residuals under-ranked.
//!
//! This module builds on [`entropyk_core::smoothing::smooth_abs`] and provides:
//!
//! - [`flow_activity`] — smooth activity α(m) ∈ [0, 1), α(0)=0, α→1 as |m| grows
//! - [`effective_duty`] — Q scaled by product of stream activities
//! - [`blend_transport_residual`] — blend active energy residual with no-flow hold
//!
//! # Residual units
//!
//! Active energy residuals are in **watts** (`ṁ·Δh − Q`). The no-flow hold term
//! uses `m_scale * Δh` so residual units remain watts when `m_scale` is a
//! characteristic mass flow [kg/s].
//!
//! # Derivatives
//!
//! All helpers expose analytic first derivatives for exact Jacobians.

use entropyk_core::smoothing::{smooth_abs, smooth_abs_derivative};

/// Default mass-flow transition scale [kg/s] (~0.1 g/s).
///
/// Below this order of magnitude, activity drops toward zero and duty is
/// smoothly extinguished. Chosen small relative to HVAC branch flows (0.01–1 kg/s)
/// so normal operation is unaffected.
pub const DEFAULT_M_EPS_KG_S: f64 = 1e-4;

/// Default characteristic mass flow [kg/s] for residual scaling of no-flow holds.
pub const DEFAULT_M_SCALE_KG_S: f64 = 0.05;

/// Smooth flow activity α(m) = m² / (m² + ε²) ∈ [0, 1).
///
/// - α(0) = 0
/// - α → 1 as |m| ≫ ε
/// - C∞ everywhere
///
/// Equivalent to `(|m|/smooth_abs(m,ε))²` for ε>0.
#[inline]
pub fn flow_activity(m: f64, m_eps: f64) -> f64 {
    let e = m_eps.abs().max(1e-30);
    let m2 = m * m;
    m2 / (m2 + e * e)
}

/// Analytic dα/dm for [`flow_activity`].
#[inline]
pub fn flow_activity_derivative(m: f64, m_eps: f64) -> f64 {
    let e = m_eps.abs().max(1e-30);
    let e2 = e * e;
    let d = m * m + e2;
    2.0 * m * e2 / (d * d)
}

/// Smooth |m| for capacity-rate constructions C = |ṁ|·cp (never zero denominator).
#[inline]
pub fn smooth_mass_magnitude(m: f64, m_eps: f64) -> f64 {
    smooth_abs(m, m_eps.abs().max(1e-30))
}

/// d(|m|_smooth)/dm.
#[inline]
pub fn smooth_mass_magnitude_derivative(m: f64, m_eps: f64) -> f64 {
    smooth_abs_derivative(m, m_eps.abs().max(1e-30))
}

/// Effective heat duty after stream activity gating: Q_eff = α_a · α_b · Q.
#[inline]
pub fn effective_duty(q: f64, alpha_a: f64, alpha_b: f64) -> f64 {
    alpha_a * alpha_b * q
}

/// Blend an active transport residual with a no-flow hold on Δh.
///
/// ```text
/// r = α · r_active + (1 − α) · m_scale · (h_out − h_in)
/// ```
///
/// When α→0 (zero flow), the residual forces `h_out ≈ h_in` (no transport)
/// without a hard branch. Residual units stay [W] if `m_scale` is [kg/s].
#[inline]
pub fn blend_transport_residual(
    alpha: f64,
    r_active: f64,
    m_scale: f64,
    h_out: f64,
    h_in: f64,
) -> f64 {
    let a = alpha.clamp(0.0, 1.0);
    a * r_active + (1.0 - a) * m_scale * (h_out - h_in)
}

/// Partials of [`blend_transport_residual`] for analytic Jacobians.
///
/// Returns `(∂r/∂α, ∂r/∂r_active, ∂r/∂h_out, ∂r/∂h_in)` treating `r_active` as
/// independent of `h_*` (caller adds chain rule on `r_active`).
#[inline]
pub fn blend_transport_partials(
    alpha: f64,
    r_active: f64,
    m_scale: f64,
    h_out: f64,
    h_in: f64,
) -> (f64, f64, f64, f64) {
    let a = alpha.clamp(0.0, 1.0);
    let dr_d_alpha = r_active - m_scale * (h_out - h_in);
    let dr_d_r_active = a;
    let dr_d_h_out = (1.0 - a) * m_scale;
    let dr_d_h_in = -(1.0 - a) * m_scale;
    (dr_d_alpha, dr_d_r_active, dr_d_h_out, dr_d_h_in)
}

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

    #[test]
    fn activity_zero_at_origin_and_near_one_at_large_flow() {
        let eps = DEFAULT_M_EPS_KG_S;
        assert!((flow_activity(0.0, eps)).abs() < 1e-15);
        assert!(flow_activity(1.0, eps) > 0.999);
        assert!(flow_activity(-1.0, eps) > 0.999);
        assert!(flow_activity(eps, eps) > 0.4 && flow_activity(eps, eps) < 0.6);
    }

    #[test]
    fn activity_derivative_matches_central_fd() {
        let eps = 1e-3;
        for m in [-1e-2, -1e-4, 0.0, 1e-4, 1e-2, 0.5] {
            let analytic = flow_activity_derivative(m, eps);
            let h = 1e-8_f64.max(m.abs() * 1e-6);
            let fd = (flow_activity(m + h, eps) - flow_activity(m - h, eps)) / (2.0 * h);
            assert!(
                (analytic - fd).abs() < 1e-5,
                "α' mismatch at m={m}: analytic={analytic} fd={fd}"
            );
        }
    }

    #[test]
    fn effective_duty_vanishes_when_either_stream_inactive() {
        let q = 10_000.0;
        assert_eq!(effective_duty(q, 0.0, 1.0), 0.0);
        assert_eq!(effective_duty(q, 1.0, 0.0), 0.0);
        assert!((effective_duty(q, 1.0, 1.0) - q).abs() < 1e-12);
    }

    #[test]
    fn blend_hold_at_zero_activity_forces_dh_zero() {
        let r = blend_transport_residual(0.0, 999.0, 0.05, 400e3, 250e3);
        // (1-0)*0.05*(150e3) = 7500
        assert!((r - 0.05 * 150e3).abs() < 1e-6);
    }

    #[test]
    fn blend_at_full_activity_is_active_residual() {
        let r = blend_transport_residual(1.0, 123.0, 0.05, 400e3, 250e3);
        assert!((r - 123.0).abs() < 1e-12);
    }

    #[test]
    fn smooth_mass_magnitude_positive_at_zero() {
        let eps = DEFAULT_M_EPS_KG_S;
        assert!(smooth_mass_magnitude(0.0, eps) > 0.0);
        assert!((smooth_mass_magnitude(0.0, eps) - eps).abs() < 1e-15);
    }

    #[test]
    fn no_nan_across_zero_crossing() {
        let eps = DEFAULT_M_EPS_KG_S;
        for i in -20..=20 {
            let m = i as f64 * 1e-5;
            let a = flow_activity(m, eps);
            let q = effective_duty(5e3, a, a);
            let r = blend_transport_residual(a, m * 1e5 - q, DEFAULT_M_SCALE_KG_S, 3e5, 2e5);
            assert!(a.is_finite() && q.is_finite() && r.is_finite(), "m={m}");
        }
    }
}
