//! Calibration Z-factors (BOLT / Modelica convention) for matching simulation to
//! real machine test data.
//!
//! Short name: Calib. Default 1.0 = no correction. Typical range [0.8, 1.2].
//!
//! ## BOLT naming parity
//!
//! Entropyk uses the same multiplicative Z-factor semantics as BOLT/Modelica
//! product models (`Z_power`, `Z_flow_suc`, `Z_Ucd`, `Z_dpc`, …):
//!
//! | Entropyk field | Legacy `f_*` | BOLT equivalent | Effect |
//! |---|---|---|---|
//! | `z_flow` | `f_m` | `Z_flow_suc` | ṁ_suc,eff = z_flow × ṁ_suc,nominal (machine **capacity**) |
//! | `z_flow_eco` | — | `Z_flow_eco` | ṁ_eco,eff = z_flow_eco × ṁ_eco,nominal |
//! | `z_dp` | `f_dp` | `Z_dpc`, `Z_dp_ref` | ΔP_eff = z_dp × ΔP_nominal |
//! | `z_ua` | `f_ua` | `Z_UA`, `Z_Ucd`, `Z_Uev` | UA_eff = z_ua × UA_nominal |
//! | `z_power` | `f_power` | `Z_power` | Ẇ_eff = z_power × Ẇ_nominal |
//! | `z_etav` | `f_etav` | — (η_v correction) | η_v,eff = z_etav × η_v,nominal |
//! | `f_w` | `f_w` | energy retention | \(h_{dis}=h_{suc}+f_w\cdot\dot W/\dot m\) (**DGT**) |
//!
//! `f_w` is the fraction of compressor work **retained** in the refrigerant
//! (not a loss fraction): `f_w = 1` → adiabatic (no shell loss);
//! `f_w = 0.98` → ~2% lost to ambient; `f_w = 0` → all work lost (Δh → 0).
//!
//! ## Recommended calibration order
//!
//! 1. **z_flow** — machine capacity via ṁ (pair with Capacity probe)
//! 2. **f_w** — compressor energy retention / shell loss (pair with discharge T / DGT)
//! 3. **z_dp** — pressure drops (pair with P)
//! 4. **z_ua** — heat transfer (pair with Tsat / Tsh)
//! 5. **z_power** — compressor power map (if needed)

use serde::{Deserialize, Serialize};

fn one() -> f64 {
    1.0
}

/// Canonical name for the mass-flow Z-factor (`z_flow`, BOLT `Z_flow_suc`).
pub const Z_FLOW: &str = "z_flow";
/// Canonical name for the economizer mass-flow Z-factor (`z_flow_eco`, BOLT `Z_flow_eco`).
pub const Z_FLOW_ECO: &str = "z_flow_eco";
/// Canonical name for the pressure-drop Z-factor (`z_dp`, BOLT `Z_dpc`).
pub const Z_DP: &str = "z_dp";
/// Canonical name for the UA Z-factor (`z_ua`, BOLT `Z_UA` / `Z_Ucd` / `Z_Uev`).
pub const Z_UA: &str = "z_ua";
/// Canonical name for the power Z-factor (`z_power`, BOLT `Z_power`).
pub const Z_POWER: &str = "z_power";
/// Canonical name for the volumetric-efficiency Z-factor (`z_etav`).
pub const Z_ETAV: &str = "z_etav";
/// Canonical name for the compressor energy-retention factor (`f_w`).
///
/// Fraction of shaft work delivered to the refrigerant enthalpy rise.
/// `1.0` = adiabatic; `0.0` = all work lost to ambient.
pub const F_W: &str = "f_w";

/// Normalizes a user/config factor string to a canonical Z-factor name.
///
/// Accepts legacy `f_*` names, canonical `z_*` names, and common BOLT spellings
/// (`Z_power`, `Z_flow_suc`, `Z_Ucd`, …). Also accepts heat-loss aliases
/// (`f_w`, `fw`, `f_q`, `z_fw`).
pub fn normalize_factor_name(factor: &str) -> Option<&'static str> {
    match factor.trim().to_ascii_lowercase().replace('_', "").as_str() {
        "fm" | "zflow" | "zflowsuc" => Some(Z_FLOW),
        "zfloweco" => Some(Z_FLOW_ECO),
        "fdp" | "zdp" | "zdpc" | "zdprefer" => Some(Z_DP),
        "fua" | "zua" | "zucd" | "zuev" => Some(Z_UA),
        "fpower" | "zpower" => Some(Z_POWER),
        "fetav" | "zetav" => Some(Z_ETAV),
        "fw" | "fq" | "zfw" | "heatloss" => Some(F_W),
        _ => None,
    }
}

/// Returns true if `id` ends with a recognized calibration-factor suffix
/// (canonical `z_*`, legacy `f_*`, or BOLT-style `Z_*` embedded in bounded-var ids).
pub fn id_ends_with_calib_suffix(id: &str) -> Option<&'static str> {
    let lower = id.to_ascii_lowercase();
    for suffix in [
        "z_flow_suc",
        "z_flow_eco",
        "z_flow",
        "z_power",
        "z_etav",
        "z_ua",
        "z_ucd",
        "z_uev",
        "z_dpc",
        "z_dp",
        "f_w",
        "f_q",
        "z_fw",
        "f_m",
        "f_dp",
        "f_ua",
        "f_power",
        "f_etav",
    ] {
        if lower.ends_with(suffix) {
            return normalize_factor_name(suffix);
        }
    }
    None
}

/// Calibration Z-factors for matching simulation to real machine test data.
///
/// Default 1.0 = no correction. Typical range [0.8, 1.2]. All factors are validated to lie in [0.5, 2.0].
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Calib {
    /// z_flow: ṁ_eff = z_flow × ṁ_nominal (BOLT `Z_flow_suc`)
    #[serde(
        default = "one",
        rename = "z_flow",
        alias = "zFlow",
        alias = "Z_flow_suc",
        alias = "Z_flow",
        alias = "f_m",
        alias = "calib_flow"
    )]
    pub z_flow: f64,
    /// z_flow_eco: economizer injection mass-flow multiplier (BOLT `Z_flow_eco`)
    #[serde(
        default = "one",
        rename = "z_flow_eco",
        alias = "zFlowEco",
        alias = "Z_flow_eco"
    )]
    pub z_flow_eco: f64,
    /// z_dp: ΔP_eff = z_dp × ΔP_nominal (BOLT `Z_dpc`)
    #[serde(
        default = "one",
        rename = "z_dp",
        alias = "zDp",
        alias = "Z_dpc",
        alias = "Z_dp",
        alias = "f_dp",
        alias = "calib_dpr"
    )]
    pub z_dp: f64,
    /// z_ua: UA_eff = z_ua × UA_nominal (BOLT `Z_UA` / `Z_Ucd` / `Z_Uev`)
    #[serde(
        default = "one",
        rename = "z_ua",
        alias = "zUa",
        alias = "Z_UA",
        alias = "Z_Ucd",
        alias = "Z_Uev",
        alias = "f_ua",
        alias = "calib_ua"
    )]
    pub z_ua: f64,
    /// z_power: Ẇ_eff = z_power × Ẇ_nominal (BOLT `Z_power`)
    #[serde(
        default = "one",
        rename = "z_power",
        alias = "zPower",
        alias = "Z_power",
        alias = "f_power"
    )]
    pub z_power: f64,
    /// z_etav: η_v,eff = z_etav × η_v,nominal (compressor displacement correction)
    #[serde(default = "one", rename = "z_etav", alias = "zEtav", alias = "f_etav")]
    pub z_etav: f64,
    /// Compressor energy-retention factor: fraction of shaft work kept in the
    /// refrigerant (\(h_{dis}=h_{suc}+f_w\cdot\dot W/\dot m\)).
    ///
    /// - `1.0` — adiabatic (nothing lost to ambient) — **default**
    /// - `0.98` — ~2% shell loss
    /// - `0.0` — all work lost (no discharge enthalpy rise)
    #[serde(
        default = "one",
        rename = "f_w",
        alias = "fw",
        alias = "f_q",
        alias = "fQ",
        alias = "z_fw",
        alias = "heat_loss"
    )]
    pub f_w: f64,
    /// Traceability: identifier or hash of the test data used to derive these factors.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub calibration_source: Option<String>,
}

impl Default for Calib {
    fn default() -> Self {
        Self {
            z_flow: 1.0,
            z_flow_eco: 1.0,
            z_dp: 1.0,
            z_ua: 1.0,
            z_power: 1.0,
            z_etav: 1.0,
            f_w: 1.0,
            calibration_source: None,
        }
    }
}

/// Stores the state vector indices of calibration Z-factors if they are defined as control variables.
///
/// Used for Inverse Control (Story 5.5). If an index is `Some(i)`, the component should
/// read its calibration factor from `state[i]` instead of using its nominal internal value.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct CalibIndices {
    /// State index for z_flow multiplier (BOLT Z_flow_suc)
    pub z_flow: Option<usize>,
    /// State index for z_flow_eco multiplier (BOLT Z_flow_eco)
    pub z_flow_eco: Option<usize>,
    /// State index for z_dp multiplier (BOLT Z_dpc)
    pub z_dp: Option<usize>,
    /// State index for z_ua multiplier (BOLT Z_UA)
    pub z_ua: Option<usize>,
    /// State index for z_power multiplier (BOLT Z_power)
    pub z_power: Option<usize>,
    /// State index for z_etav multiplier
    pub z_etav: Option<usize>,
    /// State index for compressor energy-retention factor `f_w`
    pub f_w: Option<usize>,
    /// State index for a *physical actuator* free variable (dimensioned, not a
    /// multiplier). Interpreted per component: EXV/orifice opening [0..1], fan
    /// speed ratio, screw slide position, injection-valve opening, condenser
    /// liquid level, etc.
    pub actuator: Option<usize>,
}

/// Error returned when a calibration factor is outside the allowed range [0.5, 2.0].
#[derive(Debug, Clone, PartialEq)]
pub struct CalibValidationError {
    /// Factor name (e.g. "z_flow")
    pub factor: &'static str,
    /// Value that failed validation
    pub value: f64,
}

impl std::fmt::Display for CalibValidationError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let range = if self.factor == F_W {
            "[0.0, 1.0]"
        } else {
            "[0.2, 3.0]"
        };
        write!(
            f,
            "calib {} = {} is outside allowed range {}",
            self.factor, self.value, range
        )
    }
}

impl std::error::Error for CalibValidationError {}

const MIN_Z: f64 = 0.2;
const MAX_Z: f64 = 3.0;
const MIN_F_W: f64 = 0.0;
const MAX_F_W: f64 = 1.0;

impl Calib {
    /// Updates a single factor by name (accepts canonical `z_*`, legacy `f_*`, BOLT `Z_*`).
    pub fn set_factor(&mut self, factor: &str, value: f64) -> bool {
        match normalize_factor_name(factor) {
            Some(Z_FLOW) => {
                self.z_flow = value;
                true
            }
            Some(Z_FLOW_ECO) => {
                self.z_flow_eco = value;
                true
            }
            Some(Z_DP) => {
                self.z_dp = value;
                true
            }
            Some(Z_UA) => {
                self.z_ua = value;
                true
            }
            Some(Z_POWER) => {
                self.z_power = value;
                true
            }
            Some(Z_ETAV) => {
                self.z_etav = value;
                true
            }
            Some(F_W) => {
                self.f_w = value;
                true
            }
            _ => false,
        }
    }

    /// Validates multiplicative Z-factors in [0.2, 3.0] and `f_w` in [0, 1].
    pub fn validate(&self) -> Result<(), CalibValidationError> {
        let check = |name: &'static str, value: f64, min: f64, max: f64| {
            if !(min..=max).contains(&value) {
                Err(CalibValidationError {
                    factor: name,
                    value,
                })
            } else {
                Ok(())
            }
        };
        check(Z_FLOW, self.z_flow, MIN_Z, MAX_Z)?;
        check(Z_FLOW_ECO, self.z_flow_eco, MIN_Z, MAX_Z)?;
        check(Z_DP, self.z_dp, MIN_Z, MAX_Z)?;
        check(Z_UA, self.z_ua, MIN_Z, MAX_Z)?;
        check(Z_POWER, self.z_power, MIN_Z, MAX_Z)?;
        check(Z_ETAV, self.z_etav, MIN_Z, MAX_Z)?;
        check(F_W, self.f_w, MIN_F_W, MAX_F_W)?;
        Ok(())
    }
}

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

    #[test]
    fn test_calib_default_all_one() {
        let c = Calib::default();
        assert_eq!(c.z_flow, 1.0);
        assert_eq!(c.z_flow_eco, 1.0);
        assert_eq!(c.z_dp, 1.0);
        assert_eq!(c.z_ua, 1.0);
        assert_eq!(c.z_power, 1.0);
        assert_eq!(c.z_etav, 1.0);
        assert_eq!(c.f_w, 1.0); // retention: 1 = adiabatic
        assert!(c.validate().is_ok());
    }

    #[test]
    fn test_calib_validation_bounds() {
        let ok = Calib {
            z_flow: 0.5,
            z_flow_eco: 2.0,
            z_dp: 1.0,
            z_ua: 2.0,
            z_power: 1.0,
            z_etav: 1.0,
            f_w: 0.98, // ~2% shell loss
            calibration_source: None,
        };
        assert!(ok.validate().is_ok());

        let bad_m = Calib {
            z_flow: 0.1,
            ..Default::default()
        };
        let err = bad_m.validate().unwrap_err();
        assert_eq!(err.factor, Z_FLOW);

        let bad_high = Calib {
            z_ua: 3.1,
            ..Default::default()
        };
        let err2 = bad_high.validate().unwrap_err();
        assert_eq!(err2.factor, Z_UA);

        let bad_fw = Calib {
            f_w: 1.1,
            ..Default::default()
        };
        let err3 = bad_fw.validate().unwrap_err();
        assert_eq!(err3.factor, F_W);
    }

    #[test]
    fn test_calib_json_roundtrip() {
        let c = Calib {
            z_flow: 1.1,
            z_flow_eco: 0.97,
            z_dp: 0.9,
            z_ua: 1.0,
            z_power: 1.05,
            z_etav: 1.0,
            f_w: 0.98,
            calibration_source: None,
        };
        let json = serde_json::to_string(&c).unwrap();
        assert!(json.contains("z_flow"));
        assert!(json.contains("z_flow_eco"));
        let c2: Calib = serde_json::from_str(&json).unwrap();
        assert_eq!(c, c2);
    }

    #[test]
    fn test_calib_legacy_f_aliases() {
        let json = r#"{"f_m": 1.2, "f_dp": 0.9, "f_ua": 1.1, "f_power": 1.05, "f_etav": 0.98}"#;
        let c: Calib = serde_json::from_str(json).unwrap();
        assert_eq!(c.z_flow, 1.2);
        assert_eq!(c.z_flow_eco, 1.0);
        assert_eq!(c.z_dp, 0.9);
        assert_eq!(c.z_ua, 1.1);
        assert_eq!(c.z_power, 1.05);
        assert_eq!(c.z_etav, 0.98);
    }

    #[test]
    fn test_calib_bolt_z_aliases() {
        let json = r#"{"Z_flow_suc": 1.002, "Z_flow_eco": 0.94, "Z_dpc": 0.95, "Z_Ucd": 1.04, "Z_power": 1.02}"#;
        let c: Calib = serde_json::from_str(json).unwrap();
        assert!((c.z_flow - 1.002).abs() < 1e-9);
        assert!((c.z_flow_eco - 0.94).abs() < 1e-9);
        assert!((c.z_dp - 0.95).abs() < 1e-9);
        assert!((c.z_ua - 1.04).abs() < 1e-9);
        assert!((c.z_power - 1.02).abs() < 1e-9);
    }

    #[test]
    fn test_normalize_factor_name() {
        assert_eq!(normalize_factor_name("f_m"), Some(Z_FLOW));
        assert_eq!(normalize_factor_name("z_flow"), Some(Z_FLOW));
        assert_eq!(normalize_factor_name("Z_flow_suc"), Some(Z_FLOW));
        assert_eq!(normalize_factor_name("Z_flow_eco"), Some(Z_FLOW_ECO));
        assert_eq!(normalize_factor_name("Z_power"), Some(Z_POWER));
        assert_eq!(normalize_factor_name("Z_Ucd"), Some(Z_UA));
        assert_eq!(normalize_factor_name("f_w"), Some(F_W));
        assert_eq!(normalize_factor_name("f_q"), Some(F_W));
        assert_eq!(normalize_factor_name("unknown"), None);
    }

    #[test]
    fn test_id_ends_with_calib_suffix() {
        assert_eq!(id_ends_with_calib_suffix("comp__z_flow"), Some(Z_FLOW));
        assert_eq!(
            id_ends_with_calib_suffix("comp__z_flow_eco"),
            Some(Z_FLOW_ECO)
        );
        assert_eq!(id_ends_with_calib_suffix("evap__f_ua"), Some(Z_UA));
        assert_eq!(id_ends_with_calib_suffix("comp__actuator"), None);
    }
}
