//! Parsing for the `derivedMetrics:` block.
//!
//! Pulled out of [`super::test_parse`] so the matcher/test parser stays under
//! the module-size ceiling. A derived metric is a named weighted aggregation
//! over named assertion, assert-set, and earlier-derived-metric scores; the
//! parser also rejects a forward or self reference (which could never resolve,
//! since metrics evaluate in declaration order). Reuses the shared scalar
//! helpers from [`super::test_parse`].

use serde_yaml::Value as YamlValue;

use super::test_parse::{parse_unit_threshold, parse_weight, require_str};
use super::types::{DerivedAggregation, DerivedKind, DerivedMetric, DerivedTerm};
use super::ParseError;

/// Parse the optional `derivedMetrics:` list on a test or `defaultTest` block.
/// Each entry is `{ name, value }` where `value` is a weighted aggregation over
/// named scores. Returns an empty Vec when the key is absent.
pub(super) fn parse_derived_metrics(
    entry: &YamlValue,
    path: &str,
) -> Result<Vec<DerivedMetric>, ParseError> {
    let mut out = Vec::new();
    let Some(seq) = entry.get("derivedMetrics").and_then(|v| v.as_sequence()) else {
        return Ok(out);
    };
    for (i, item) in seq.iter().enumerate() {
        let item_path = format!("{path}/derivedMetrics/{i}");
        out.push(parse_one_derived_metric(item, &item_path)?);
    }
    // Reject a forward/self derived-metric reference (it can never resolve).
    if let Some((metric, reference)) = crate::suite::scoring::forward_or_self_ref(&out) {
        return Err(ParseError::DerivedMetricForwardRef(format!(
            "derived metric `{metric}` at {path} references `{reference}`, which is declared \
             later or is itself; reference only earlier derived metrics, named assertions, \
             or assert-sets"
        )));
    }
    Ok(out)
}

fn parse_one_derived_metric(
    item: &YamlValue,
    item_path: &str,
) -> Result<DerivedMetric, ParseError> {
    let name = require_str(item, item_path, "name")?.to_string();
    let value_node = item.get("value").ok_or(ParseError::Missing {
        path: item_path.to_string(),
        field: "value",
    })?;
    let value = parse_derived_aggregation(value_node, item_path)?;
    let threshold = parse_unit_threshold(item, item_path, "threshold")?;
    Ok(DerivedMetric {
        name,
        value,
        threshold,
    })
}

/// Parse the `value:` aggregation of a derived metric. The shape is
/// `{ weighted_sum | weighted_average: [ { ref, weight } ] }`: a single key
/// naming the combination, whose value is the list of weighted terms.
fn parse_derived_aggregation(
    node: &YamlValue,
    path: &str,
) -> Result<DerivedAggregation, ParseError> {
    let map = node.as_mapping().ok_or(ParseError::BadShape {
        path: path.to_string(),
        field: "value",
        expected: "object with weighted_sum or weighted_average",
    })?;
    let (kind, key) = if map.contains_key(YamlValue::String("weighted_sum".into())) {
        (DerivedKind::WeightedSum, "weighted_sum")
    } else if map.contains_key(YamlValue::String("weighted_average".into())) {
        (DerivedKind::WeightedAverage, "weighted_average")
    } else {
        return Err(ParseError::BadShape {
            path: path.to_string(),
            field: "value",
            expected: "object with weighted_sum or weighted_average",
        });
    };
    let terms_node = node
        .get(key)
        .and_then(|v| v.as_sequence())
        .ok_or(ParseError::BadShape {
            path: path.to_string(),
            field: key,
            expected: "a list of { ref, weight } terms",
        })?;
    let mut terms = Vec::with_capacity(terms_node.len());
    for (i, term) in terms_node.iter().enumerate() {
        let term_path = format!("{path}/{key}/{i}");
        let reference = require_str(term, &term_path, "ref")?.to_string();
        let weight = parse_weight(term, &term_path, "weight")?;
        terms.push(DerivedTerm { reference, weight });
    }
    Ok(DerivedAggregation { kind, terms })
}
