// Milestone-18 fieldtest — scenario 2: MONTE-CARLO robustness over a seed set.
//
// Composes: axis 4 (Monte-Carlo = sweep over seeds), seed-as-input (SyntheticSpec
// ::source(seed) — the stream is fully seed-determined), the aggregate metric
// distribution (mean + quantiles), reproducibility (same seed set -> same family),
// and registry lineage (persist the MC family + round-trip).
//
// The task a downstream researcher does: "how robust is this fixed strategy's
// tail across 128 stochastic market realizations? give me the median and p95 of
// the metrics, and persist the run so I can re-list it later."
//
// PUBLIC INTERFACE ONLY: API from the design ledger (C12 axis 4), glossary, specs
// 0042/0043/0045, and `cargo doc` rustdoc. No crates/*/src was read.

use std::sync::mpsc;

use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
use aura_engine::{
    f64_field, monte_carlo, summarize, BlueprintNode, Composite, Edge, McFamily, OutField, Role,
    RunManifest, RunReport, SyntheticSpec, Target,
};
use aura_registry::{group_families, mc_member_reports, FamilyKind, Registry};
use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub};

fn sma_cross() -> Composite {
    Composite::new(
        "sma_cross",
        vec![
            Sma::builder().named("fast").into(),
            Sma::builder().named("slow").into(),
            Sub::builder().into(),
        ],
        vec![
            Edge { from: 0, to: 2, slot: 0, from_field: 0 },
            Edge { from: 1, to: 2, slot: 1, from_field: 0 },
        ],
        vec![Role {
            name: "price".into(),
            targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
            source: None,
        }],
        vec![OutField { node: 2, field: 0, name: "out".into() }],
    )
}

fn harness_with_sinks() -> (
    Composite,
    mpsc::Receiver<(Timestamp, Vec<Scalar>)>,
    mpsc::Receiver<(Timestamp, Vec<Scalar>)>,
) {
    let (tx_eq, rx_eq) = mpsc::channel();
    let (tx_ex, rx_ex) = mpsc::channel();
    let bp = Composite::new(
        "harness",
        vec![
            BlueprintNode::Composite(sma_cross()),
            Exposure::builder().into(),
            SimBroker::builder(1e-4).into(),
            Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq).into(),
            Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_ex).into(),
        ],
        vec![
            Edge { from: 0, to: 1, slot: 0, from_field: 0 },
            Edge { from: 1, to: 2, slot: 0, from_field: 0 },
            Edge { from: 2, to: 3, slot: 0, from_field: 0 },
            Edge { from: 1, to: 4, slot: 0, from_field: 0 },
        ],
        vec![Role {
            name: "price".into(),
            targets: vec![Target { node: 0, slot: 0 }, Target { node: 2, slot: 1 }],
            source: Some(ScalarKind::F64),
        }],
        vec![],
    );
    (bp, rx_eq, rx_ex)
}

// The per-draw closure: the SEED perturbs the synthetic market (seed-as-input,
// Fork B — the seed lives at the data-generation edge, never inside the engine).
// The strategy (base_point) is fixed across draws.
fn run_draw(seed: u64, point: &[Scalar]) -> RunReport {
    let (bp, rx_eq, rx_ex) = harness_with_sinks();
    let mut h = bp
        .bootstrap_with_params(point.to_vec())
        .expect("base point kind-checked against param_space");
    let spec = SyntheticSpec { start: 1.0, len: 256, step: 60_000 };
    let window = (Timestamp(1), Timestamp((spec.len as i64 - 1) * spec.step + 1));
    // The seed -> Source construction lives inside the closure body (#71 firewall):
    // a seeded *Source*, never a materialized Vec crossing the monte_carlo boundary.
    h.run(vec![Box::new(spec.source(seed))]);
    drop(h);
    let equity = f64_field(&rx_eq.try_iter().collect::<Vec<_>>(), 0);
    let exposure = f64_field(&rx_ex.try_iter().collect::<Vec<_>>(), 0);
    RunReport {
        manifest: RunManifest {
            commit: "fieldtest".into(),
            params: vec![],
            window,
            seed, // <- the seed that drove this draw is recorded (C12)
            broker: "sim-optimal(pip_size=0.0001)".into(),
        },
        metrics: summarize(&equity, &exposure),
    }
}

fn main() {
    // The strategy is fixed; only the market realization (the seed) varies.
    let base_point = vec![Scalar::I64(3), Scalar::I64(8), Scalar::F64(1.0)];
    let seeds: Vec<u64> = (0..128).collect();

    // axis 4: monte_carlo varies the SEED across the seed set.
    let family: McFamily = monte_carlo(&base_point, &seeds, run_draw);
    assert_eq!(family.draws.len(), 128, "one draw per seed");

    // Each draw recorded the seed that drove it, in seed-INPUT order.
    for (i, d) in family.draws.iter().enumerate() {
        assert_eq!(d.seed, i as u64, "draw order == seed input order");
        assert_eq!(d.report.manifest.seed, i as u64, "seed captured in manifest");
    }
    println!("axis 4: {} draws, seeds {}..{}", family.draws.len(), 0, 127);

    // The aggregate metric distribution — the robustness answer (mean + quantiles).
    let tp = &family.aggregate.total_pips;
    let dd = &family.aggregate.max_drawdown;
    println!("total_pips:   mean {:.4}  p5 {:.4}  p50 {:.4}  p95 {:.4}", tp.mean, tp.p5, tp.p50, tp.p95);
    println!("max_drawdown: mean {:.4}  p50 {:.4}  p95 (tail) {:.4}", dd.mean, dd.p50, dd.p95);
    // The aggregate is a pure reduction over the retained draws (recomputable):
    assert!(tp.p5 <= tp.p50 && tp.p50 <= tp.p95, "quantiles ordered");
    // Custom statistics always read the raw draws directly:
    let worst_dd = family.draws.iter().map(|d| d.report.metrics.max_drawdown).fold(0.0_f64, f64::max);
    println!("worst single-draw drawdown (raw draws): {worst_dd:.4}");

    // The family must not collapse: distinct seeds perturb the run.
    let distinct: std::collections::BTreeSet<String> = family
        .draws
        .iter()
        .map(|d| format!("{:.6}", d.report.metrics.total_pips))
        .collect();
    println!("distinct total_pips across 128 draws: {}", distinct.len());
    assert!(distinct.len() > 1, "a Monte-Carlo over distinct seeds must spread");

    // Reproducibility (C1): same seed set -> same McFamily, byte-for-byte.
    let family2: McFamily = monte_carlo(&base_point, &seeds, run_draw);
    assert_eq!(family, family2, "same seed set reproduces the whole family (C1)");
    println!("reproducibility: re-run produced an identical McFamily (C1) ✓");

    // Lineage: persist the MC family + round-trip.
    let tmp = std::env::temp_dir().join(format!("mw2-runs-{}.jsonl", std::process::id()));
    let _ = std::fs::remove_file(&tmp);
    let _ = std::fs::remove_file(tmp.with_file_name("families.jsonl"));
    let reg = Registry::open(&tmp);
    let id = reg
        .append_family("eurusd-robustness", FamilyKind::MonteCarlo, &mc_member_reports(&family))
        .expect("mc family append");
    let families = group_families(reg.load_family_members().expect("load"));
    let f = families.iter().find(|f| f.id == id).expect("our family is present");
    println!("lineage: family {} kind={:?} members={}", f.id, f.kind, f.members.len());
    assert_eq!(f.kind, FamilyKind::MonteCarlo);
    assert_eq!(f.members.len(), 128);
    let _ = std::fs::remove_file(&tmp);

    println!("\nOK: monte-carlo robustness -> aggregate distribution -> reproducible -> persisted lineage.");
}
