//! Back-compatible read shapes for the C18 flat runs store.
//!
//! [`Registry::load`](crate::Registry::load) is the durable read-path and must
//! stay tolerant of the param-value wire shape across the cycle-0047 typed-`Scalar`
//! migration (`86746e3`). The current format tags each param value as an
//! externally-tagged [`Scalar`] (`{"F64":2.0}` / `{"I64":10}`); a `runs.jsonl`
//! line written *before* that migration carries the value as a **bare JSON
//! number** (`2.0`). These mirror types deserialize either shape and convert into
//! the canonical [`RunReport`] — the read side only. The forward write path
//! (`Registry::append`, `RunReport::to_json`) is untouched and still emits the
//! tagged form, so this is a one-directional widening, not a format change.

use aura_engine::{
    FamilySelection, ProjectProvenance, RunManifest, RunMetrics, RunReport, Scalar, Timestamp,
};
use serde::de::{self, Deserializer};
use serde::Deserialize;

/// A `RunReport` whose param values read back from either the tagged or the
/// legacy bare-float shape. Field-for-field structural mirror of `RunReport`;
/// only [`ScalarRead`] differs from the engine type.
#[derive(Deserialize)]
pub(crate) struct RunReportRead {
    manifest: RunManifestRead,
    metrics: RunMetrics,
}

#[derive(Deserialize)]
struct RunManifestRead {
    commit: String,
    params: Vec<(String, ScalarRead)>,
    window: (Timestamp, Timestamp),
    seed: u64,
    broker: String,
    #[serde(default)]
    selection: Option<FamilySelection>,
    #[serde(default)]
    instrument: Option<String>,
    #[serde(default)]
    topology_hash: Option<String>,
    #[serde(default)]
    project: Option<ProjectProvenance>,
}

/// A param value that accepts BOTH the current tagged [`Scalar`] form
/// (`{"F64":2.0}` / `{"I64":10}`) and the legacy pre-0047 bare JSON number
/// (`2.0`, coerced to `Scalar::F64` per the documented `f64` coercion).
struct ScalarRead(Scalar);

impl<'de> Deserialize<'de> for ScalarRead {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        // serde_json's untagged `Value` lets us peek the shape without committing
        // the parse: an object/string deserializes as the tagged `Scalar`; a bare
        // number is the legacy form, coerced to `Scalar::f64`.
        let value = serde_json::Value::deserialize(deserializer)?;
        match value {
            serde_json::Value::Number(n) => {
                let f = n.as_f64().ok_or_else(|| {
                    de::Error::custom(format!("legacy bare-float param value out of f64 range: {n}"))
                })?;
                Ok(ScalarRead(Scalar::f64(f)))
            }
            other => Scalar::deserialize(other)
                .map(ScalarRead)
                .map_err(de::Error::custom),
        }
    }
}

impl From<RunReportRead> for RunReport {
    fn from(r: RunReportRead) -> Self {
        let RunManifestRead {
            commit, params, window, seed, broker, selection, instrument, topology_hash, project,
        } = r.manifest;
        RunReport {
            manifest: RunManifest {
                commit,
                params: params.into_iter().map(|(name, v)| (name, v.0)).collect(),
                window,
                seed,
                broker,
                selection,
                instrument,
                topology_hash,
                project,
            },
            metrics: r.metrics,
        }
    }
}
