//! Runnable World-family demo: **`walk_forward`** the GER40 session-breakout
//! `Composite` blueprint across multi-year real GER40 history, with a
//! per-window in-sample optimize of its two genuine tuning knobs
//! (`entry_bar.target` × `exit_bar.target`). This is the executable #97
//! resolution: the roll is NON-DEGENERATE — a real, non-empty `param_space()` is
//! optimized per window, so every window's `chosen_params` is populated (contrast
//! the earlier fieldtest's empty-space degenerate roll over the raw FlatGraph, on
//! which no in-sample tuning was possible).
//!
//! Run:
//! ```text
//! cargo run -p aura-ingest --example ger40_breakout_walkforward
//! ```
//! Prints each IS/OOS window with the entry/exit it chose in-sample and its OOS
//! pips, plus the stitched out-of-sample equity curve. Skips cleanly where the
//! archive is absent.

use std::sync::Arc;

use aura_core::{Cell, Scalar, Timestamp};
use aura_engine::{
    sweep, summarize, walk_forward, GridSpace, RollMode, RunManifest, RunReport, WindowBounds,
    WindowRoller, WindowRun,
};
use chrono::TimeZone;
use chrono_tz::Europe::Berlin;
use aura_ingest::{default_data_server, open_ohlc, DataServer, DEFAULT_DATA_PATH};

#[path = "shared/breakout_real.rs"]
mod breakout_real;
use breakout_real::*;

// The in-sample grid the per-window optimize searches: entry ∈ {2,3,4}, exit ∈
// {4,5,6} — the two EqConst targets that ARE the blueprint's param_space().
const ENTRY_BARS: &[i64] = &[2, 3, 4];
const EXIT_BARS: &[i64] = &[4, 5, 6];

/// Bootstrap the blueprint at one grid point, run it over the epoch-ns
/// [`Timestamp`] window `[from, to]` of real GER40 OHLC, and fold the recorded
/// equity/held taps into `(RunReport, equity-segment)`. Fresh blueprint per call
/// (C1).
fn run_point(
    server: &Arc<DataServer>,
    point: &[Cell],
    from: Timestamp,
    to: Timestamp,
) -> (RunReport, Vec<(Timestamp, f64)>) {
    let (bp, taps) = ger40_breakout_blueprint(
        pip_size_of(SYMBOL),
        BAR_MINUTES,
        SESSION_HOUR,
        SESSION_MINUTE,
        Berlin,
    );
    let mut h = bp
        .bootstrap_with_cells(point)
        .expect("point kind-checked against param_space");
    let sources =
        open_ohlc(server, SYMBOL, from, to).expect("window overlaps GER40 data");
    h.run(sources);
    drop(h);
    let equity: Vec<(Timestamp, f64)> = taps
        .equity
        .try_iter()
        .map(|(t, r)| (t, as_f64(&r[0])))
        .collect();
    let exposure: Vec<(Timestamp, f64)> = taps
        .held
        .try_iter()
        .map(|(t, r)| (t, as_f64(&r[0])))
        .collect();
    let report = RunReport {
        manifest: RunManifest {
            commit: "ger40-breakout-wfo".to_string(),
            params: vec![],
            window: (from, to),
            seed: 0,
            broker: "sim-optimal(pip_size=1)".to_string(),
            selection: None,
            instrument: None,
            topology_hash: None,
            project: None,
        },
        metrics: summarize(&equity, &exposure),
    };
    (report, equity)
}

fn main() {
    let server = default_data_server();
    if !server.has_symbol(SYMBOL) {
        println!("skip: no local data at {DEFAULT_DATA_PATH} (symbol {SYMBOL} absent).");
        return;
    }

    println!("=== GER40 session-breakout — World `walk_forward` (2018..2024) ===");

    // The roll span: 2018-01 .. 2024-12. is=12mo, oos=3mo, step=3mo.
    let day_ns: i64 = 86_400_000 * 1_000_000;
    let month_ns: i64 = 30 * day_ns;
    let origin = aura_ingest::unix_ms_to_epoch_ns(
        chrono::Utc.with_ymd_and_hms(2018, 1, 1, 0, 0, 0).unwrap().timestamp_millis(),
    );
    let end = aura_ingest::unix_ms_to_epoch_ns(
        chrono::Utc.with_ymd_and_hms(2024, 12, 31, 23, 59, 59).unwrap().timestamp_millis(),
    );
    let roller = WindowRoller::new(
        (origin, end),
        12 * month_ns,
        3 * month_ns,
        3 * month_ns,
        RollMode::Rolling,
    )
    .expect("span fits >= 1 window");

    // The non-empty space the per-window in-sample sweep optimizes over: the two
    // genuine tuning knobs (the #97 resolution — a real space to optimize).
    let space = ger40_breakout_blueprint(
        pip_size_of(SYMBOL),
        BAR_MINUTES,
        SESSION_HOUR,
        SESSION_MINUTE,
        Berlin,
    )
    .0
    .param_space();
    println!(
        "optimizing param_space() per window: {:?}  (in-sample grid {}×{})",
        space.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(),
        ENTRY_BARS.len(),
        EXIT_BARS.len(),
    );
    println!();

    let server_for_closure = Arc::clone(&server);
    let space_for_closure = space.clone();
    let result = walk_forward(roller, space, move |w: WindowBounds| -> WindowRun {
        // In-sample optimize: sweep the entry×exit grid over the IS window, choose
        // the point with the best in-sample total_pips. The blueprint is consumed
        // directly via its param_space() — no re-authoring.
        let is_grid = GridSpace::new(
            &space_for_closure,
            space_for_closure
                .iter()
                .map(|ps| match ps.name.as_str() {
                    "entry_bar.target" => ENTRY_BARS.iter().map(|&v| Scalar::i64(v)).collect(),
                    "exit_bar.target" => EXIT_BARS.iter().map(|&v| Scalar::i64(v)).collect(),
                    other => panic!("unexpected param slot {other}"),
                })
                .collect(),
        )
        .expect("IS grid well-formed");
        let is_family = sweep(&is_grid, |pt: &[Cell]| {
            run_point(&server_for_closure, pt, w.is.0, w.is.1).0
        });
        let best = is_family
            .points
            .iter()
            .max_by(|a, b| {
                a.report
                    .metrics
                    .total_pips
                    .partial_cmp(&b.report.metrics.total_pips)
                    .unwrap_or(std::cmp::Ordering::Equal)
            })
            .expect("the IS grid is non-empty");
        let chosen = best.params.clone();

        // Apply the chosen params on the OOS window — the honest out-of-sample run.
        let (oos_report, oos_equity) =
            run_point(&server_for_closure, &chosen, w.oos.0, w.oos.1);
        WindowRun { chosen_params: chosen, oos_equity, oos_report }
    });

    println!("walk-forward produced {} windows\n", result.windows.len());
    println!(
        "{:>3}  {:<23} {:<10} {:<9} {:>10} {:>9}",
        "win", "oos-window (UTC date)", "entry_bar", "exit_bar", "oos_pips", "oos_dd"
    );
    println!("{}", "-".repeat(72));
    for (k, w) in result.windows.iter().enumerate() {
        let named = result.named_params(k);
        let entry = named.iter().find(|(n, _)| n == "entry_bar.target").map(|(_, v)| v.as_i64()).unwrap_or(-1);
        let exit = named.iter().find(|(n, _)| n == "exit_bar.target").map(|(_, v)| v.as_i64()).unwrap_or(-1);
        let m = &w.run.oos_report.metrics;
        let win = format!(
            "{}..{}",
            fmt_date(w.bounds.oos.0),
            fmt_date(w.bounds.oos.1),
        );
        println!(
            "{:>3}  {:<23} {:<10} {:<9} {:>10.1} {:>9.1}",
            k,
            win,
            entry,
            exit,
            m.total_pips,
            m.max_drawdown,
        );
    }

    let stitched = &result.stitched_oos_equity;
    println!(
        "\nstitched OOS curve: {} points, final cumulative = {:.1} pips",
        stitched.len(),
        stitched.last().map(|(_, v)| *v).unwrap_or(0.0),
    );
    println!(
        "\nOK: NON-DEGENERATE walk-forward — every window optimized the breakout's\n\
         {}-param space in-sample (chosen_params populated). The #97 empty-space\n\
         degenerate roll is resolved: the strategy ships with a real space to tune.",
        space_param_count(&result.space),
    );
}

fn space_param_count(space: &[aura_engine::ParamSpec]) -> usize {
    space.len()
}

/// Render an epoch-ns timestamp as a UTC `YYYY-MM-DD` for the window column.
/// Display only — never feeds the graph.
fn fmt_date(ts: Timestamp) -> String {
    let secs = ts.0.div_euclid(1_000_000_000);
    match chrono::Utc.timestamp_opt(secs, 0).single() {
        Some(dt) => dt.format("%Y-%m-%d").to_string(),
        None => format!("ts={}", ts.0),
    }
}

fn as_f64(s: &Scalar) -> f64 {
    match s {
        Scalar::F64(v) => *v,
        other => panic!("expected f64, got {other:?}"),
    }
}
