// Milestone fieldtest — scenario 2: a SWEEP over named axes + compare across
// the resulting family.
//
// Promise probed: take ONE blueprint and spread it across a FAMILY of disjoint
// instances over NAMED axes (`.axis("sma_cross.fast", [2,3]).sweep(run)`), then
// index/compare manifests and metrics across the family members (#33's surface).
//
// Public interface only: ledger + specs 0028/0030 + rustdoc. No crates/*/src
// was read. Uses the ALIASED cross so the promise-form axis names are real
// (scenario 1 established the plain cross's two knobs collide on
// `sma_cross.length`; aliasing is the author's fix).

use std::sync::mpsc;

use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
use aura_engine::{
    f64_field, summarize, BlueprintNode, Composite, Edge, OutField, ParamAlias, Role,
    RunManifest, RunReport, SweepFamily, Target,
};
use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub};

/// The aliased 2-SMA cross (knobs surface as `fast` / `slow`).
fn sma_cross_aliased() -> Composite {
    Composite::new(
        "sma_cross",
        vec![
            Sma::builder().into(),
            Sma::builder().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![
            ParamAlias { name: "fast".into(), node: 0, slot: 0 },
            ParamAlias { name: "slow".into(), node: 1, slot: 0 },
        ],
        vec![OutField { node: 2, field: 0, name: "out".into() }],
    )
}

/// A fresh harness Composite plus the equity + exposure receivers it records to.
/// A fresh build per point gives each sweep member its OWN drainable channels
/// (the #32 spec's per-point-fresh-build pattern).
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_aliased()),
            Exposure::builder().into(),
            SimBroker::builder(1e-4).into(),
            // sink 0: broker equity (item 4)
            Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq).into(),
            // sink 1: exposure stream (item 5)
            Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_ex).into(),
        ],
        vec![
            Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // cross -> Exposure
            Edge { from: 1, to: 2, slot: 0, from_field: 0 }, // Exposure -> broker.exposure
            Edge { from: 2, to: 3, slot: 0, from_field: 0 }, // broker -> equity sink
            Edge { from: 1, to: 4, slot: 0, from_field: 0 }, // Exposure -> exposure sink
        ],
        vec![Role {
            name: "price".into(),
            targets: vec![Target { node: 0, slot: 0 }, Target { node: 2, slot: 1 }],
            source: Some(ScalarKind::F64),
        }],
        vec![],
        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.06, 1.04, 1.02, 1.01, 1.03, 1.07,
    ];
    prices
        .iter()
        .enumerate()
        .map(|(i, &p)| (Timestamp(60_000_000_000 * i as i64), Scalar::F64(p)))
        .collect()
}

fn main() {
    let prices = synthetic_prices();

    // --- The sweep: ONE blueprint, named axes, a family of disjoint runs. -----
    // 2 (fast) x 2 (slow) x 1 (scale) = 4 points.
    let family: SweepFamily = harness_with_sinks()
        .0
        .axis("sma_cross.fast", [2, 3])
        .axis("sma_cross.slow", [4, 5])
        .axis("scale", [0.5])
        .sweep(|point| {
            // Each point builds fresh (fresh channels), bootstraps by the SAME
            // positional point vector the named axes resolved to, runs, drains,
            // summarizes — author-side glue (the engine cannot know which sink
            // is equity vs exposure; #28 "closure-driven" boundary).
            let (bp, rx_eq, rx_ex) = harness_with_sinks();
            let mut h = bp
                .bootstrap_with_params(point.to_vec())
                .expect("named-resolved point is kind-checked");
            h.run(vec![prices.clone()]);
            drop(h);
            let eq_rows: Vec<_> = rx_eq.try_iter().collect();
            let ex_rows: Vec<_> = rx_ex.try_iter().collect();
            let equity = f64_field(&eq_rows, 0);
            let exposure = f64_field(&ex_rows, 0);
            let metrics = summarize(&equity, &exposure);
            // The author also supplies the manifest (the sweep is domain-free;
            // #29 made SweepPoint carry a full RunReport).
            RunReport {
                manifest: RunManifest {
                    commit: "fieldtest".into(),
                    // name the point's coords for the comparison below
                    params: vec![],
                    window: (Timestamp(0), Timestamp(660_000_000_000)),
                    seed: 0,
                    broker: "sim-optimal".into(),
                },
                metrics,
            }
        })
        .expect("named axes resolve to a valid grid");

    // --- Compare across the family (the #33 index/compare surface). -----------
    println!("sweep family: {} members\n", family.points.len());
    println!(
        "{:>5} {:>5} {:>6}  {:>12} {:>12} {:>8}",
        "fast", "slow", "scale", "total_pips", "max_dd", "flips"
    );
    let mut best: Option<(usize, f64)> = None;
    for (i, pt) in family.points.iter().enumerate() {
        // pt.params is the point's coordinate in param_space() slot order.
        let coord: Vec<String> = pt.params.iter().map(scalar_str).collect();
        let m = &pt.report.metrics;
        println!(
            "{:>5} {:>5} {:>6}  {:>12.4} {:>12.4} {:>8}",
            coord[0], coord[1], coord[2], m.total_pips, m.max_drawdown, m.exposure_sign_flips
        );
        if best.is_none() || m.total_pips > best.unwrap().1 {
            best = Some((i, m.total_pips));
        }
    }

    let (bi, bp) = best.unwrap();
    let bc: Vec<String> = family.points[bi].params.iter().map(scalar_str).collect();
    println!(
        "\nbest by total_pips: point #{bi} (fast={}, slow={}, scale={}) = {:.4} pips",
        bc[0], bc[1], bc[2], bp
    );

    // --- Confirm members are disjoint / independently meaningful. -------------
    // Distinct points must produce distinct metrics (the family is not constant).
    let distinct: std::collections::BTreeSet<String> = family
        .points
        .iter()
        .map(|p| format!("{:.6}", p.report.metrics.total_pips))
        .collect();
    println!(
        "\ndistinct total_pips values across {} points: {}",
        family.points.len(),
        distinct.len()
    );
    assert!(
        distinct.len() > 1,
        "a sweep over distinct lengths must not collapse to one metric"
    );
    for pt in &family.points {
        assert!(pt.report.metrics.total_pips.is_finite(), "every point produces metrics");
    }

    // Odometer order check (last axis fastest) — pin the family is ordered.
    println!("\nfamily params, in order:");
    for (i, pt) in family.points.iter().enumerate() {
        let c: Vec<String> = pt.params.iter().map(scalar_str).collect();
        println!("  #{i}: [{}]", c.join(", "));
    }

    println!("\nOK: named-axis sweep produced a comparable, ordered, non-constant family.");
}

fn scalar_str(s: &Scalar) -> String {
    match s {
        Scalar::I64(v) => v.to_string(),
        Scalar::F64(v) => format!("{v}"),
        Scalar::Bool(v) => v.to_string(),
        Scalar::Ts(t) => t.0.to_string(),
    }
}
