//! The energy order of dispatch (SPEC §4, *Energy order*; §7.1 step 2; browser-sink §4.7):
//! the order a sink tries the capabilities an envelope offers in when its deployer configures
//! none. It is a default order over what the sink can already decode, and never a route: what
//! the sink advertises (SPEC §4; in a page, the object's route, browser-sink §4.3) is decided
//! before it, and nothing here changes it.
//!
//! The ranks, lowest first, in the terms platform-decode's review settled on:
//!
//! - [`Rank::First`]: classical decode the platform answers supported, smooth and
//!   power-efficient. In a page this is read only for an object routed `ua`, the user
//!   agent's decoder: an object the page decodes another way is not ranked on the user
//!   agent's answer about a decoder that will not run it. `powerEfficient` is the user
//!   agent's hint, its meaning left to the user agent; it orders, and is never an energy
//!   figure or evidence of hardware.
//! - [`Rank::Smooth`]: classical decode the platform answers smooth, not power-efficient.
//! - [`Rank::Classical`]: any other decode the sink can make: classical decode with no
//!   platform answer, and a learned capability whose decode this sink has measured
//!   ([`qualifies`]).
//! - [`Rank::LearnedWithoutBudget`]: a learned capability ([`is_learned`]) with no such
//!   measurement.
//!
//! Within a rank, the envelope's order. So a learned capability is never decoded above a
//! classical alternative the sink can decode unless the sink holds a measurement of its
//! decode, and then not above a decode the platform answers smooth or power-efficient.
//!
//! The measurement a learned capability needs is the sink-metered figure browser-sink §6
//! carries in its energy entry: a figure with its class, its declared uncertainty, its
//! coverage and the fabrics it covers. It qualifies when it reads under §6 with the class
//! `OnChipCounter` or `CalibratedInstrument` and the coverage `Whole`. The order reads only
//! whether such a figure is held, never its value: a figure is reported, not steered on
//! (browser-sink §6). A deployer that wants to compare values sets its own policy
//! ([`DispatchPolicy::with_ceiling`], per decode, which this default never applies).
//!
//! Pure std plus `serde_json`, compiled unconditionally, so the browser sink's wasm build
//! applies the same order.

use serde_json::{json, Map, Value};

use crate::codecs::{self, ReplicateClass};

/// Whether a capability is decoded by a learned reconstruction: its registry class is not
/// one of the three standard classes (SPEC §7). A capability this revision does not register
/// counts as learned: nothing says it is classical.
pub fn is_learned(capability: &str) -> bool {
    !matches!(
        codecs::capability_replicate_class(capability),
        Some(ReplicateClass::StandardDefined | ReplicateClass::StandardTolerance | ReplicateClass::StandardUnclassified)
    )
}

/// A capability's rank in the energy order, lowest tried first.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Rank {
    First = 0,
    Smooth = 1,
    Classical = 2,
    LearnedWithoutBudget = 3,
}

impl Rank {
    pub const fn as_str(self) -> &'static str {
        match self {
            Rank::First => "first",
            Rank::Smooth => "smooth",
            Rank::Classical => "classical",
            Rank::LearnedWithoutBudget => "learned-without-budget",
        }
    }
}

/// The platform's answer about one decode, in browser-sink §3's shape: `supported` from the
/// decoder's own configuration check (WebCodecs `isConfigSupported()`, a native platform's
/// hardware-decode query), and `smooth` and `power_efficient` from Media Capabilities, asked
/// only once `supported` is true (http-delivery §12.2), or `None` where nothing was asked.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PlatformAnswer {
    pub supported: bool,
    pub smooth: Option<bool>,
    pub power_efficient: Option<bool>,
}

impl PlatformAnswer {
    /// The rank this answer gives a classical decode: `first` only for `supported`, `smooth`
    /// and `power_efficient` all `true`, `smooth` for `smooth` alone, `classical` otherwise. A
    /// `null` is not a `true`.
    pub fn rank(&self) -> Rank {
        match (self.supported, self.smooth, self.power_efficient) {
            (true, Some(true), Some(true)) => Rank::First,
            (true, Some(true), _) => Rank::Smooth,
            _ => Rank::Classical,
        }
    }
}

/// Why a measurement does not read under browser-sink §6, by that section's reason code.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MeasurementRefusal {
    ClassInvalid,
    UnmeteredAsZero,
    CountInvalid,
    EvidenceMissing,
}

impl MeasurementRefusal {
    pub const fn code(self) -> &'static str {
        match self {
            MeasurementRefusal::ClassInvalid => "energy_class_invalid",
            MeasurementRefusal::UnmeteredAsZero => "unmetered_as_zero",
            MeasurementRefusal::CountInvalid => "energy_count_invalid",
            MeasurementRefusal::EvidenceMissing => "energy_evidence_missing",
        }
    }
}

const CLASSES: [&str; 4] = ["CalibratedInstrument", "OnChipCounter", "ModelBased", "Estimator"];
const MAX_SAFE: i64 = (1 << 53) - 1;

fn integer(v: &Value) -> Option<i64> {
    let i = v.as_number()?.as_i64()?;
    (-MAX_SAFE..=MAX_SAFE).contains(&i).then_some(i)
}

fn positive_count(v: Option<&Value>) -> bool {
    integer(v.unwrap_or(&Value::Null)).is_some_and(|i| i > 0)
}

/// browser-sink §6: a measurement read, with its `per_decode_micro`, or the reason it is
/// refused. One reader for the page's energy entry and for the order.
pub fn read_measurement(m: &Value) -> Result<Value, MeasurementRefusal> {
    let o = m.as_object().ok_or(MeasurementRefusal::CountInvalid)?;
    let class = o.get("class").and_then(Value::as_str).filter(|c| CLASSES.contains(c)).ok_or(MeasurementRefusal::ClassInvalid)?;
    if o.get("joules_micro").and_then(integer) == Some(0) {
        return Err(MeasurementRefusal::UnmeteredAsZero);
    }
    if !["joules_micro", "decodes", "window_us"].iter().all(|k| positive_count(o.get(*k))) {
        return Err(MeasurementRefusal::CountInvalid);
    }
    let hardware = matches!(class, "CalibratedInstrument" | "OnChipCounter");
    if hardware && !positive_count(o.get("relative_ppm")) {
        return Err(MeasurementRefusal::EvidenceMissing);
    }
    if class == "OnChipCounter" && !matches!(o.get("filtering").and_then(Value::as_str), Some("On" | "Off" | "Unknown")) {
        return Err(MeasurementRefusal::EvidenceMissing);
    }
    if class == "CalibratedInstrument" && !o.get("calibration_ref").and_then(Value::as_str).is_some_and(|s| !s.is_empty()) {
        return Err(MeasurementRefusal::EvidenceMissing);
    }
    if !matches!(o.get("coverage").and_then(Value::as_str), Some("Whole" | "Partial")) {
        return Err(MeasurementRefusal::EvidenceMissing);
    }
    let fabrics_ok = o.get("fabrics").and_then(Value::as_array).is_some_and(|f| {
        !f.is_empty() && f.iter().all(Value::is_string) && {
            let mut s: Vec<&str> = f.iter().filter_map(Value::as_str).collect();
            s.sort_unstable();
            s.windows(2).all(|w| w[0] != w[1])
        }
    });
    if !fabrics_ok {
        return Err(MeasurementRefusal::EvidenceMissing);
    }
    let joules = integer(&o["joules_micro"]).unwrap_or(0);
    let decodes = integer(&o["decodes"]).unwrap_or(1);
    let mut out: Map<String, Value> = o.clone();
    out.insert("per_decode_micro".into(), json!(joules / decodes));
    Ok(Value::Object(out))
}

/// Whether a measurement lets a learned capability rank with classical decode: it reads
/// under browser-sink §6, its class is `OnChipCounter` or `CalibratedInstrument`, and its
/// coverage is `Whole` (a figure that saw only some of the fabrics the decode ran on is not a
/// measurement of the decode). Its value is not read.
pub fn qualifies(m: &Value) -> bool {
    read_measurement(m).is_ok_and(|r| matches!(r["class"].as_str(), Some("OnChipCounter" | "CalibratedInstrument")) && r["coverage"] == "Whole")
}

/// Whether a sink applies the energy order or the envelope's own.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OrderKind {
    /// SPEC §4's default.
    Energy,
    /// The envelope's order, by the deployer's configuration: SPEC §4 has a sink that
    /// takes it say so where it reports a decode.
    Manifest,
}

/// The platform's answer about a capability's decode, where the sink asks one.
pub type PlatformAnswers<'a> = &'a dyn Fn(&str) -> Option<PlatformAnswer>;

/// The sink-metered measurement a sink holds of a capability's decode, in browser-sink §6's
/// shape.
pub type Measurements<'a> = &'a dyn Fn(&str) -> Option<Value>;

/// The order a sink tries an envelope's capabilities in.
pub struct DispatchPolicy<'a> {
    pub kind: OrderKind,
    /// The platform's answer about a capability's decode, where the sink asks one; `None`
    /// where it asks nothing, or decodes the capability with a decoder of its own.
    pub platform: Option<PlatformAnswers<'a>>,
    /// The sink-metered measurement it holds of a capability's decode on this sink.
    pub measured: Option<Measurements<'a>>,
    /// A deployer's ceiling on a learned capability's measured figure, in microjoules per
    /// decode: a policy of its own, never the default.
    pub ceiling_per_decode_micro: Option<u64>,
}

impl Default for DispatchPolicy<'_> {
    fn default() -> Self {
        DispatchPolicy::energy()
    }
}

impl<'a> DispatchPolicy<'a> {
    /// SPEC §4's default: the energy order, with no platform answer and no measurement.
    pub fn energy() -> DispatchPolicy<'a> {
        DispatchPolicy { kind: OrderKind::Energy, platform: None, measured: None, ceiling_per_decode_micro: None }
    }

    /// The envelope's own order, as a deployer may configure.
    pub fn manifest_order() -> DispatchPolicy<'a> {
        DispatchPolicy { kind: OrderKind::Manifest, ..DispatchPolicy::energy() }
    }

    /// The energy order with the platform's answers.
    pub fn with_platform(mut self, platform: PlatformAnswers<'a>) -> Self {
        self.platform = Some(platform);
        self
    }

    /// The energy order with the measurements the sink holds.
    pub fn with_measured(mut self, measured: Measurements<'a>) -> Self {
        self.measured = Some(measured);
        self
    }

    /// A deployer's policy: a learned capability whose measured figure is above
    /// `per_decode_micro` microjoules a decode ranks as unmeasured. Both are per decode, so
    /// the comparison is unit for unit.
    pub fn with_ceiling(mut self, per_decode_micro: u64) -> Self {
        self.ceiling_per_decode_micro = Some(per_decode_micro);
        self
    }

    /// `capability`'s rank. Under the envelope's order every capability ranks alike.
    pub fn rank(&self, capability: &str) -> Rank {
        if self.kind == OrderKind::Manifest {
            return Rank::Classical;
        }
        if is_learned(capability) {
            let measured = self.measured.and_then(|f| f(capability)).filter(qualifies).is_some_and(|m| {
                let per = read_measurement(&m).ok().and_then(|r| r["per_decode_micro"].as_u64());
                self.ceiling_per_decode_micro.is_none_or(|c| per.is_some_and(|p| p <= c))
            });
            return if measured { Rank::Classical } else { Rank::LearnedWithoutBudget };
        }
        self.platform.and_then(|p| p(capability)).map_or(Rank::Classical, |a| a.rank())
    }

    /// The positions of `capabilities` in the order this policy tries them: by rank, the
    /// envelope's order within a rank.
    pub fn order<'c>(&self, capabilities: impl IntoIterator<Item = &'c str>) -> Vec<usize> {
        let mut ranked: Vec<(Rank, usize)> = capabilities.into_iter().enumerate().map(|(i, c)| (self.rank(c), i)).collect();
        ranked.sort();
        ranked.into_iter().map(|(_, i)| i).collect()
    }
}

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

    /// A sink-metered figure in browser-sink §6's shape. Synthetic: no measurement; it
    /// exists only to exercise the rule.
    fn figure(class: &str, coverage: &str) -> Value {
        let mut m = json!({
            "joules_micro": 4_125_000, "decodes": 100, "window_us": 7_000_000, "class": class,
            "relative_ppm": 25_000, "filtering": "Unknown", "coverage": coverage, "fabrics": ["cpu", "gpu"],
            "note": "synthetic: no measurement",
        });
        if class == "CalibratedInstrument" {
            m.as_object_mut().unwrap().remove("filtering");
            m["calibration_ref"] = json!("synthetic: no certificate");
        }
        m
    }

    #[test]
    fn learned_is_every_class_but_the_standard_ones() {
        for c in ["wai.neural.int_hyper", "wai.video.int_hyper", "wai.image.jpegai", "wai.neural.bmshj2018", "wai.semantic.clip", "wai.generate.spectral", "wai.unregistered.string"] {
            assert!(is_learned(c), "{c}");
        }
        for c in ["wai.video.av1", "wai.image.png", "wai.video.int_motion", "wai.video.hevc"] {
            assert!(!is_learned(c), "{c}");
        }
    }

    /// Only a hardware-class figure of the whole decode qualifies, and its value is not read.
    #[test]
    fn a_measurement_qualifies_by_its_class_and_coverage_not_its_value() {
        assert!(qualifies(&figure("OnChipCounter", "Whole")));
        assert!(qualifies(&figure("CalibratedInstrument", "Whole")));
        assert!(!qualifies(&figure("OnChipCounter", "Partial")), "a figure of part of the decode");
        for class in ["ModelBased", "Estimator", "HwShunt"] {
            assert!(!qualifies(&figure(class, "Whole")), "{class}");
        }
        let mut no_filtering = figure("OnChipCounter", "Whole");
        no_filtering.as_object_mut().unwrap().remove("filtering");
        assert!(!qualifies(&no_filtering));
        let mut zero = figure("OnChipCounter", "Whole");
        zero["joules_micro"] = json!(0);
        assert_eq!(read_measurement(&zero).unwrap_err(), MeasurementRefusal::UnmeteredAsZero);
        let mut huge = figure("OnChipCounter", "Whole");
        huge["joules_micro"] = json!(9_000_000_000_000_i64);
        assert!(qualifies(&huge), "the value does not decide");
    }

    /// The default order: a learned capability with no measurement after a classical one; with
    /// one it ranks classical; the platform's answers rank classical decode; the envelope's
    /// order within a rank; and a manifest order that keeps it whole.
    #[test]
    fn the_energy_order() {
        let (neural, av1, png, hevc) = ("wai.neural.int_hyper", "wai.video.av1", "wai.image.png", "wai.video.hevc");
        let e = DispatchPolicy::energy();
        assert_eq!(e.order([neural, av1]), vec![1, 0]);
        assert_eq!(e.order([av1, neural]), vec![0, 1]);
        assert_eq!(e.order([neural, "wai.image.jpegai", av1, png]), vec![2, 3, 0, 1], "two learned, kept in their order, after both classical");
        assert_eq!(DispatchPolicy::manifest_order().order([neural, av1]), vec![0, 1]);
        let held = |c: &str| (c == neural).then(|| figure("OnChipCounter", "Whole"));
        assert_eq!(DispatchPolicy::energy().with_measured(&held).order([neural, av1]), vec![0, 1], "measured: classical, the envelope's order");
        let partial = |c: &str| (c == neural).then(|| figure("OnChipCounter", "Partial"));
        assert_eq!(DispatchPolicy::energy().with_measured(&partial).order([neural, av1]), vec![1, 0]);
        assert_eq!(DispatchPolicy::energy().with_measured(&held).with_ceiling(1_000).order([neural, av1]), vec![1, 0], "a deployer's ceiling below the figure");
        assert_eq!(DispatchPolicy::energy().with_measured(&held).with_ceiling(41_250).order([neural, av1]), vec![0, 1], "at the ceiling, per decode");
        let answers = |c: &str| match c {
            "wai.video.av1" => Some(PlatformAnswer { supported: true, smooth: Some(true), power_efficient: Some(true) }),
            "wai.video.hevc" => Some(PlatformAnswer { supported: true, smooth: Some(true), power_efficient: None }),
            "wai.image.png" => Some(PlatformAnswer { supported: true, smooth: Some(false), power_efficient: Some(true) }),
            _ => None,
        };
        let p = DispatchPolicy::energy().with_platform(&answers).with_measured(&held);
        assert_eq!(p.order([neural, png, hevc, av1]), vec![3, 2, 0, 1], "first, smooth, then classical (the measured learned one, then the power-efficient but not smooth one), in the envelope's order");
        assert_eq!(p.rank(png), Rank::Classical, "power-efficient but not smooth drops frames");
        assert_eq!(PlatformAnswer { supported: false, smooth: Some(true), power_efficient: Some(true) }.rank(), Rank::Classical, "an answer that is not supported puts nothing first");
    }
}
