// Milestone-18 fieldtest — scenario 1: RANDOM PARAM-SWEEP, optimized, persisted
// as a lineage family, round-tripped.
//
// Composes: axis 1 (random sample, the "one unified sweep path"), axis 2
// (optimize / argmax by a named metric), the Source ingestion seam (synthetic
// VecSource), and registry lineage (append_family + group_families round-trip).
//
// The task a downstream researcher does: "tune a 2-SMA-cross strategy by drawing
// 64 seeded random points over continuous ranges, pick the best by total_pips,
// then persist the whole sweep as ONE named family I can re-list and rank later."
//
// PUBLIC INTERFACE ONLY: API discovered from the design ledger (C12), glossary,
// specs 0028/0045/0046/0048/0049, and `cargo doc --workspace --no-deps` rustdoc.
// No crates/*/src was read.

use std::sync::mpsc;

use aura_core::{Cell, Firing, Scalar, ScalarKind, Timestamp};
use aura_engine::{
    f64_field, summarize, sweep, BlueprintNode, Composite, Edge, OutField, ParamRange, RandomSpace,
    Role, RunManifest, RunReport, SweepFamily, Target, VecSource,
};
use aura_registry::{group_families, optimize, sweep_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)
}

fn synthetic_prices() -> Vec<(Timestamp, Scalar)> {
    let prices = [
        1.00, 1.01, 1.02, 1.03, 1.05, 1.08, 1.11, 1.10, 1.06, 1.04, 1.02, 1.01, 1.03, 1.07, 1.12,
        1.15, 1.13, 1.09, 1.05, 1.02,
    ];
    prices
        .iter()
        .enumerate()
        .map(|(i, &p)| (Timestamp(60_000_000_000 * i as i64), Scalar::f64(p)))
        .collect()
}

fn run_one(point: &[Cell], prices: &[(Timestamp, Scalar)]) -> RunReport {
    let (bp, rx_eq, rx_ex) = harness_with_sinks();
    let mut h = bp
        .bootstrap_with_cells(point)
        .expect("random point is kind-checked against the param-space");
    // The Source ingestion seam: run() consumes Vec<Box<dyn Source>>, not a Vec.
    h.run(vec![Box::new(VecSource::new(prices.to_vec()))]);
    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: (Timestamp(0), Timestamp(60_000_000_000 * 20)),
            seed: 0,
            broker: "sim-optimal".into(),
        },
        metrics: summarize(&equity, &exposure),
    }
}

fn main() {
    let prices = synthetic_prices();
    let space = harness_with_sinks().0.param_space();

    // axis 1 (random): draw 64 seeded points over continuous ranges (positional,
    // parallel to param_space()). RandomBinder would let me name them; here the
    // grid-style positional ParamRange is what the headline sweep uses.
    let ranges = vec![
        ParamRange::i64(2, 8),     // sma_cross.fast.length
        ParamRange::i64(10, 30),   // sma_cross.slow.length
        ParamRange::f64(0.5, 4.0), // exposure.scale
    ];
    let seed = 0xA11CE;
    let rand_space =
        RandomSpace::new(&space, ranges, 64, seed).expect("ranges well-formed for the space");
    println!("axis 1 (random): {} points, seed {:#x}", rand_space.len(), seed);

    // The SAME sweep core the grid uses — `sweep` is generic over impl Space.
    let family: SweepFamily = sweep(&rand_space, |point: &[Cell]| run_one(point, &prices));
    assert_eq!(family.points.len(), 64);

    // Did the random sweep actually spread, or collapse? (the "one unified sweep
    // path" must produce a real family over the synthetic source).
    let distinct: std::collections::BTreeSet<String> = family
        .points
        .iter()
        .map(|p| format!("{:.6}", p.report.metrics.total_pips))
        .collect();
    println!("distinct total_pips across 64 points: {}", distinct.len());

    // axis 2 (optimize): pick the best point by total_pips — argmax over the family.
    let best = optimize(&family, "total_pips").expect("total_pips is a known metric");
    println!(
        "axis 2 (optimize): best total_pips = {:.4}",
        best.report.metrics.total_pips
    );
    // Confirm optimize really returned the argmax of the family.
    let manual_max = family
        .points
        .iter()
        .map(|p| p.report.metrics.total_pips)
        .fold(f64::NEG_INFINITY, f64::max);
    assert_eq!(best.report.metrics.total_pips, manual_max, "optimize == argmax");

    // Lineage: persist the whole sweep as ONE named family to a fresh temp store.
    // NOTE: the family store lives at `<dir>/families.jsonl` — keyed by the runs
    // path's DIRECTORY, not its filename (spec 0045: `path.with_file_name(
    // "families.jsonl")`). So a fresh, per-process *directory* is needed to isolate
    // the family store, not just a per-process runs *filename*. (A first attempt
    // that varied only the filename shared one /tmp/families.jsonl across runs and
    // round-tripped 2 families instead of 1 — recorded as a finding.)
    let dir = std::env::temp_dir().join(format!("mw1-{}", std::process::id()));
    let _ = std::fs::create_dir_all(&dir);
    let tmp = dir.join("runs.jsonl");
    let reg = Registry::open(&tmp);
    let id = reg
        .append_family("eurusd-tune", FamilyKind::Sweep, &sweep_member_reports(&family))
        .expect("family append");
    println!("lineage: persisted family id = {id}");
    assert_eq!(id, "eurusd-tune-0", "first family of this name is -0");

    // Round-trip: the stored links re-derive the family as a unit.
    let families = group_families(reg.load_family_members().expect("load members"));
    assert_eq!(families.len(), 1, "one family round-tripped");
    let f = &families[0];
    println!(
        "round-trip: family {} kind={:?} members={}",
        f.id,
        f.kind,
        f.members.len()
    );
    assert_eq!(f.id, "eurusd-tune-0");
    assert_eq!(f.kind, FamilyKind::Sweep);
    assert_eq!(f.members.len(), 64, "all 64 sweep points are members");

    // A second run of the same name mints a fresh family id (a new experiment run).
    let id2 = reg
        .append_family("eurusd-tune", FamilyKind::Sweep, &sweep_member_reports(&family))
        .expect("second family append");
    println!("re-run mints fresh id = {id2}");
    assert_eq!(id2, "eurusd-tune-1");

    let _ = std::fs::remove_dir_all(&dir); // removes runs.jsonl + the sibling families.jsonl

    println!("\nOK: random tune -> optimize -> persisted+round-tripped lineage family.");
}
