// Cycle-0049 fieldtest — example 1: the HEADLINE task.
//
// Tune a 3-param SMA-cross strategy by drawing N random points over declared
// continuous `ParamRange`s and running the SAME `sweep` a grid would use — then
// pick the best point by total_pips. A grid would explode here (fast x slow x
// scale over continuous ranges is the curse of dimensionality); random sampling
// over declared ranges is the standard tool. This is the code a researcher
// writes (axis-hint 1).
//
// PUBLIC INTERFACE ONLY: API discovered from the design ledger
// (docs/design/INDEX.md C12/C12.1), the glossary, spec 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_std::{Exposure, Recorder, SimBroker, Sma, Sub};

/// The reusable 2-SMA-cross composite (legs named so their knobs surface as
/// `sma_cross.fast.length` / `sma_cross.slow.length`).
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() }],
    )
}

/// A fresh root harness Composite plus the equity + exposure receivers it
/// records to. A fresh build per point gives each sweep member its OWN
/// drainable channels.
///
/// param_space() = [sma_cross.fast.length: I64, sma_cross.slow.length: I64,
///                  exposure.scale: F64].
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(), // sink 0: equity
            Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_ex).into(), // sink 1: exposure
        ],
        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![],
    );
    (bp, rx_eq, rx_ex)
}

fn synthetic_prices() -> Vec<(Timestamp, Scalar)> {
    // A trending-then-reverting series so different (fast, slow, scale) actually
    // produce different pip outcomes.
    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()
}

/// The author's per-point closure: build fresh, bootstrap by the point's cells,
/// run, drain, summarize into a RunReport. `Fn(&[Cell]) -> RunReport`.
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");
    h.run(vec![Box::new(VecSource::new(prices.to_vec()))]);
    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);
    RunReport {
        manifest: RunManifest {
            commit: "fieldtest".into(),
            params: vec![],
            window: (Timestamp(0), Timestamp(60_000_000_000 * 20)),
            seed: 0,
            broker: "sim-optimal".into(),
        },
        metrics,
    }
}

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

    // The param-space the random ranges are declared against (positional-parallel).
    let space = harness_with_sinks().0.param_space();
    println!("param-space ({} slots):", space.len());
    for (i, ps) in space.iter().enumerate() {
        println!("  slot {i}: {} : {:?}", ps.name, ps.kind);
    }

    // ONE declared continuous range per slot, in param_space() order.
    let ranges = vec![
        ParamRange::i64(2, 8),     // sma_cross.fast.length in [2, 8]   (inclusive)
        ParamRange::i64(10, 30),   // sma_cross.slow.length in [10, 30] (inclusive)
        ParamRange::f64(0.5, 4.0), // exposure.scale        in [0.5, 4.0) (half-open)
    ];

    // Draw 200 seeded points; validated against the param-space IN new().
    let count = 200;
    let seed = 0xC0FFEE;
    let rand_space = RandomSpace::new(&space, ranges, count, seed)
        .expect("ranges are well-formed for the space");
    println!("\nRandomSpace: {} points, seed {:#x}", rand_space.len(), seed);

    // SAME execution layer as the grid sweep — sweep is generic over impl Space.
    let family: SweepFamily = sweep(&rand_space, |point: &[Cell]| run_one(point, &prices));
    assert_eq!(family.points.len(), count, "one point per draw");

    // Pick the best point by total_pips.
    let mut best: Option<(usize, f64)> = None;
    for (i, pt) in family.points.iter().enumerate() {
        let p = pt.report.metrics.total_pips;
        if best.is_none() || p > best.unwrap().1 {
            best = Some((i, p));
        }
        assert!(p.is_finite(), "every drawn point produces finite metrics");
    }
    let (bi, bp) = best.unwrap();

    // Readable named view of the winning coordinate (named_params reuses zip).
    let named = family.named_params(bi);
    println!("\nbest point #{bi} = {:.4} pips at:", bp);
    for (name, val) in &named {
        println!("  {name} = {}", scalar_str(val));
    }

    // Confirm the family genuinely spread: more than one distinct metric.
    let distinct: std::collections::BTreeSet<String> = family
        .points
        .iter()
        .map(|p| format!("{:.6}", p.report.metrics.total_pips))
        .collect();
    println!(
        "\ndistinct total_pips across {} points: {}",
        family.points.len(),
        distinct.len()
    );
    assert!(distinct.len() > 1, "a random sweep over real ranges must not collapse");

    // Show every drawn point landed inside its declared range (in-range draws).
    let mut fast_min = i64::MAX;
    let mut fast_max = i64::MIN;
    let mut scale_min = f64::INFINITY;
    let mut scale_max = f64::NEG_INFINITY;
    for pt in &family.points {
        fast_min = fast_min.min(pt.params[0].i64());
        fast_max = fast_max.max(pt.params[0].i64());
        scale_min = scale_min.min(pt.params[2].f64());
        scale_max = scale_max.max(pt.params[2].f64());
    }
    println!(
        "\nsampled ranges: sma_cross.fast.length in [{fast_min}, {fast_max}] (declared [2, 8]); \
         exposure.scale in [{scale_min:.4}, {scale_max:.4}) (declared [0.5, 4.0))"
    );
    assert!((2..=8).contains(&fast_min) && (2..=8).contains(&fast_max));
    assert!(scale_min >= 0.5 && scale_max < 4.0);

    println!("\nOK: random continuous-range tuning produced a comparable, spread family.");
}

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