// Milestone fieldtest — Topology-as-data (C24), Example 1.
//
// The headline round-trip the milestone promises: a researcher authors a small
// SMA-cross bias signal with the Rust GraphBuilder, serializes the topology to
// canonical JSON (blueprint_to_json), loads it back through the std vocabulary
// resolver (blueprint_from_json + aura_std::std_vocabulary), and proves
//   (a) the canonical JSON round-trips byte-identically (serialize -> load ->
//       re-serialize == original), and
//   (b) the RELOADED blueprint runs BIT-IDENTICALLY to its Rust-built twin
//       (C1: the recorded bias trace matches value-for-value).
//
// Discovery: the GraphBuilder / blueprint_to_json / blueprint_from_json surface
// was learned from `cargo doc` doc-comments + the C24 ledger entry + `aura graph
// introspect --node <T>` (which gives each node's port and bind shapes). No
// crates/*/src was read.
//
// Note on the run path: a recording sink (Recorder) is deliberately OUTSIDE the
// serializable round-trippable set (C24 ledger: it captures an mpsc::Sender —
// runtime identity, not param-generic data). So the *serializable* unit is the
// pure signal sub-composite (price source -> SMAs -> Sub -> Bias -> expose
// bias); to OBSERVE a run we nest that sub-composite under a tiny throwaway
// harness wrapper that adds a Recorder. The same wrapper is applied to both the
// original and the reloaded sub-composite, so the comparison is apples-to-apples.

use std::sync::mpsc;

use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
use aura_engine::{
    blueprint_from_json, blueprint_to_json, BlueprintNode, Composite, Edge, GraphBuilder, OutField,
    Role, Target, VecSource,
};
use aura_std::{std_vocabulary, Bias, Recorder, Sma, Sub};

/// Author the serializable signal sub-composite via the Rust builder API.
/// SMA(2) fast, SMA(4) slow, Sub = fast - slow, Bias over the spread, exposing
/// the single `bias` field. The two SMA lengths are baked (`.bind`); Bias.scale
/// is left as a tunable param (mirrors the canonical op-script form).
fn author_signal() -> Composite {
    let mut gb = GraphBuilder::new("graph");
    let price = gb.source_role("price", ScalarKind::F64);
    let fast = gb.add(Sma::builder().named("fast").bind("length", Scalar::I64(2)));
    let slow = gb.add(Sma::builder().named("slow").bind("length", Scalar::I64(4)));
    gb.feed(price, [fast.input("series"), slow.input("series")]);
    let sub = gb.add(Sub::builder().named("sub"));
    gb.connect(fast.output("value"), sub.input("lhs"));
    gb.connect(slow.output("value"), sub.input("rhs"));
    let bias = gb.add(Bias::builder().named("bias"));
    gb.connect(sub.output("value"), bias.input("signal"));
    gb.expose(bias.output("bias"), "bias");
    gb.build().expect("signal composite should build")
}

/// Run a signal sub-composite by nesting it under a Recorder harness and
/// recording the exposed `bias` each fired cycle. Returns (ts, bias) rows.
fn run_signal(signal: Composite, prices: &[f64]) -> Vec<(i64, f64)> {
    let (tx, rx) = mpsc::channel::<(Timestamp, Vec<Scalar>)>();
    let harness = Composite::new(
        "harness",
        vec![
            BlueprintNode::Composite(signal),
            Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx).into(),
        ],
        vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }], // bias -> recorder col 0
        vec![Role {
            name: "price".into(),
            targets: vec![Target { node: 0, slot: 0 }],
            source: Some(ScalarKind::F64),
        }],
        vec![] as Vec<OutField>,
    );
    let stream: Vec<(Timestamp, Scalar)> = prices
        .iter()
        .enumerate()
        .map(|(i, &p)| (Timestamp(60_000_000_000 * i as i64), Scalar::F64(p)))
        .collect();
    // bias.scale is the one tunable param left in the space; bind it at bootstrap.
    let mut h = harness
        .bootstrap_with_params(vec![Scalar::F64(1.0)])
        .expect("harness should bootstrap");
    h.run(vec![Box::new(VecSource::new(stream))]);
    drop(h); // release the recorder's tx clone
    rx.iter()
        .map(|(ts, row)| match row[0] {
            Scalar::F64(x) => (ts.0, x),
            other => panic!("expected f64 bias, got {other:?}"),
        })
        .collect()
}

fn main() {
    let prices: Vec<f64> = vec![
        1.00, 1.01, 1.02, 1.03, 1.05, 1.08, 1.06, 1.04, 1.02, 1.01, 1.03, 1.07,
    ];

    // --- (a) canonical JSON round-trip ---------------------------------------
    let orig = author_signal();
    let json1 = blueprint_to_json(&orig).expect("serialize original");
    std::fs::write("mt_1_rust_blueprint.json", &json1).expect("persist artifact");

    let loaded = blueprint_from_json(&json1, &std_vocabulary).expect("load from json");
    let json2 = blueprint_to_json(&loaded).expect("re-serialize loaded");
    assert_eq!(
        json1, json2,
        "canonical JSON must round-trip byte-identically (serialize->load->re-serialize)"
    );
    println!("(a) canonical JSON round-trip: IDENTICAL ({} bytes)", json1.len());

    // --- (b) run identity: reloaded twin runs bit-identical to the original ---
    let orig_run = run_signal(author_signal(), &prices);
    let loaded_run = run_signal(loaded, &prices);
    assert_eq!(
        orig_run, loaded_run,
        "C1: the reloaded blueprint must run bit-identically to its Rust-built twin"
    );
    println!(
        "(b) run identity: {} recorded bias rows, value-for-value IDENTICAL",
        orig_run.len()
    );
    for (ts, b) in &loaded_run {
        println!("    ts={ts:>14}  bias={b:+.6}");
    }
    println!("PASS: author -> serialize -> load -> reproduce (C1 round-trip).");
}
