// Cycle-0049 fieldtest — example 4: the Space-trait PAYOFF (axis-hint 4).
//
// Swap a GridSpace sweep for a RandomSpace sweep behind the `Space` trait with
// the SAME downstream consumer code. A research helper that takes `&impl Space`
// and runs the sweep + ranks the family is written ONCE and called with both
// enumerations — the abstraction's whole point.
//
// PUBLIC INTERFACE ONLY: ledger (C12.1 + the Space trait) + glossary + spec
// 0049 + 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, GridSpace, OutField, ParamRange,
    RandomSpace, Role, RunManifest, RunReport, Space, SweepFamily, Target, VecSource,
};
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_sink() -> (
    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 prices() -> Vec<(Timestamp, Scalar)> {
    let p = [
        1.00, 1.02, 1.05, 1.04, 1.06, 1.10, 1.08, 1.05, 1.03, 1.07, 1.12, 1.09, 1.11, 1.14, 1.10,
    ];
    p.iter()
        .enumerate()
        .map(|(i, &v)| (Timestamp(60_000_000_000 * i as i64), Scalar::f64(v)))
        .collect()
}

fn run_one(point: &[Cell]) -> RunReport {
    let (bp, rx_eq, rx_ex) = harness_with_sink();
    let mut h = bp.bootstrap_with_cells(point).expect("kind-checked point");
    h.run(vec![Box::new(VecSource::new(prices()))]);
    drop(h);
    let eq: Vec<_> = rx_eq.try_iter().collect();
    let ex: Vec<_> = rx_ex.try_iter().collect();
    let metrics = summarize(&f64_field(&eq, 0), &f64_field(&ex, 0));
    RunReport {
        manifest: RunManifest {
            commit: "fieldtest".into(),
            params: vec![],
            window: (Timestamp(0), Timestamp(60_000_000_000 * 15)),
            seed: 0,
            broker: "sim-optimal".into(),
        },
        metrics,
    }
}

/// The ONE downstream consumer, generic over the enumeration. It does not know
/// (or care) whether the points came from a cartesian product or seeded draws —
/// it just runs the sweep and returns the best point by total_pips. This is the
/// code that previously could only take &GridSpace.
fn tune_and_rank<S: Space>(label: &str, space: &S) -> (SweepFamily, usize) {
    let family = sweep(space, run_one);
    let mut best = 0usize;
    for (i, pt) in family.points.iter().enumerate() {
        if pt.report.metrics.total_pips > family.points[best].report.metrics.total_pips {
            best = i;
        }
    }
    println!(
        "[{label}] {} points; best #{best} = {:.4} pips",
        family.points.len(),
        family.points[best].report.metrics.total_pips
    );
    (family, best)
}

fn main() {
    let space = harness_with_sink().0.param_space();

    // GridSpace: an explicit discrete lattice over the same three slots.
    let grid = GridSpace::new(
        &space,
        vec![
            vec![Scalar::i64(2), Scalar::i64(4), Scalar::i64(6)],
            vec![Scalar::i64(10), Scalar::i64(20)],
            vec![Scalar::f64(0.5), Scalar::f64(1.5)],
        ],
    )
    .expect("well-formed grid");
    println!("GridSpace declares {} points (3 x 2 x 2)", grid.len());

    // RandomSpace: seeded draws over continuous ranges spanning the same slots.
    let rand = RandomSpace::new(
        &space,
        vec![ParamRange::i64(2, 6), ParamRange::i64(10, 20), ParamRange::f64(0.5, 1.5)],
        12,
        0xBEEF,
    )
    .expect("well-formed ranges");
    println!("RandomSpace draws {} points\n", rand.len());

    // THE PAYOFF: the exact same `tune_and_rank` over both, no per-enumeration
    // branching in the consumer.
    let (gfam, gbest) = tune_and_rank("grid  ", &grid);
    let (rfam, rbest) = tune_and_rank("random", &rand);

    // Both carry the same param-space schema (names + kinds) for the named view.
    assert_eq!(gfam.space, rfam.space, "both Spaces carry the same param-space");
    println!("\nshared param-space schema across both families:");
    for ps in &gfam.space {
        println!("  {} : {:?}", ps.name, ps.kind);
    }

    // The named view works identically off either family.
    println!("\ngrid   best coords: {:?}", named(&gfam, gbest));
    println!("random best coords: {:?}", named(&rfam, rbest));

    // Both families are non-degenerate (the sweep ran real disjoint sims).
    for (lbl, f) in [("grid", &gfam), ("random", &rfam)] {
        let distinct: std::collections::BTreeSet<String> = f
            .points
            .iter()
            .map(|p| format!("{:.6}", p.report.metrics.total_pips))
            .collect();
        println!("{lbl}: {} distinct total_pips over {} points", distinct.len(), f.points.len());
        assert!(distinct.len() > 1, "{lbl} family must not collapse");
    }

    println!("\nOK: one `&impl Space` consumer drove both Grid and Random sweeps unchanged.");
}

fn named(f: &SweepFamily, i: usize) -> Vec<String> {
    f.named_params(i)
        .into_iter()
        .map(|(n, v)| {
            let s = match v {
                Scalar::I64(x) => x.to_string(),
                Scalar::F64(x) => format!("{x:.4}"),
                Scalar::Bool(x) => x.to_string(),
                Scalar::Timestamp(t) => t.0.to_string(),
            };
            format!("{n}={s}")
        })
        .collect()
}
