//! `fxt-backtest` — Real-LOB GPU backtest CLI.
//!
//! Two subcommands:
//!
//!   fxt-backtest run --data <mbp10-dir> [--policy-grid <yaml>] ...
//!     Runs the LOB backtest harness on a directory of MBP-10 dbn files.
//!     Writes per-cell artifacts under --out/cell_<NNNN>/.
//!
//!   fxt-backtest aggregate <sweep-dir>
//!     Walks every cell directory under <sweep-dir>, parses summary.json,
//!     emits aggregate.parquet + pareto_frontier.json at the sweep-dir root.
//!
//! See docs/superpowers/specs/2026-05-18-real-lob-integration-design.md §7.

use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use ml_alpha::cfc::trunk::{CfcConfig, CfcTrunk};
use ml_backtesting::aggregate::aggregate_sweep_dir;
use ml_backtesting::harness::{BacktestHarness, BacktestHarnessConfig};
use ml_backtesting::policy::Strategy;
use ml_core::device::MlDevice;
use std::path::PathBuf;

#[derive(Parser, Debug)]
#[command(name = "fxt-backtest", version, about = "Real-LOB GPU backtest CLI", long_about = None)]
struct Cli {
    #[command(subcommand)]
    cmd: Cmd,
}

#[derive(Subcommand, Debug)]
enum Cmd {
    /// Run a backtest over an MBP-10 dataset.
    Run(RunArgs),
    /// Aggregate per-cell summary.json files in a sweep directory into
    /// aggregate.parquet + pareto_frontier.json.
    Aggregate(AggregateArgs),
}

#[derive(Parser, Debug)]
struct RunArgs {
    /// Root directory containing MBP-10 .dbn.zst files (and predecoded sidecars).
    #[arg(long)]
    data: PathBuf,
    /// Directory containing predecoded sidecars; defaults to --data.
    #[arg(long)]
    predecoded_dir: Option<PathBuf>,
    /// Number of parallel backtest cells (one CUDA block per cell).
    #[arg(long, default_value_t = 1)]
    n_parallel: usize,
    /// Optional YAML file listing per-cell Strategy compositions.
    /// If omitted, every cell gets Strategy::default_for(--max-lots).
    #[arg(long)]
    policy_grid: Option<PathBuf>,
    /// Decide every Sth event (matches training stride; default 4).
    #[arg(long, default_value_t = 4)]
    decision_stride: usize,
    /// Total submission→fill latency in nanoseconds (IBKR + Scaleway
    /// baseline = 100_000_000 = 100ms). Propagated to the resting-order
    /// engine so in-flight orders wait for arrival_ts_ns before crossing.
    /// 0 = immediate-match path (legacy).
    #[arg(long, default_value_t = 100_000_000)]
    latency_ns: u32,
    /// Target annual volatility in price-unit lot-equivalents
    /// (see spec §5 IsvKellyState.target_annual_vol).
    #[arg(long, default_value_t = 50.0)]
    target_annual_vol_units: f32,
    /// Trade-rate annualisation factor — non-overlapping convention,
    /// 825 (K=6000 holding × 250 trading days) per pearl_phase1d4 default.
    #[arg(long, default_value_t = 825.0)]
    annualisation_factor: f32,
    /// Per-leaf max concurrent lots (Strategy::default_for argument).
    #[arg(long, default_value_t = 5)]
    max_lots: u16,
    /// Cap on events processed; 0 = exhaust the input stream.
    #[arg(long, default_value_t = 0)]
    max_events: u64,
    /// Random seed for v1 trunk initialisation (a checkpoint loader is
    /// follow-up work — see harness.rs::new doc).
    #[arg(long, default_value_t = 0xC0FFEE)]
    seed: u64,
    /// Output directory; per-cell artifacts written to <out>/cell_NNNN/.
    #[arg(long)]
    out: PathBuf,
}

#[derive(Parser, Debug)]
struct AggregateArgs {
    /// Sweep directory containing one subdirectory per cell, each with
    /// a summary.json file.
    sweep_dir: PathBuf,
}

fn main() -> Result<()> {
    tracing_subscriber::fmt::init();
    let cli = Cli::parse();
    match cli.cmd {
        Cmd::Run(args) => run(args),
        Cmd::Aggregate(args) => {
            let out = aggregate_sweep_dir(&args.sweep_dir)?;
            println!(
                "aggregated {} cells; {} on Pareto frontier; parquet={}, frontier={}",
                out.n_cells,
                out.n_pareto,
                out.parquet_path.display(),
                out.frontier_path.display()
            );
            Ok(())
        }
    }
}

fn run(args: RunArgs) -> Result<()> {
    // Resolve strategies — either from YAML or default_for.
    let strategies: Vec<Strategy> = if let Some(grid_path) = &args.policy_grid {
        let yaml = std::fs::read_to_string(grid_path)
            .with_context(|| format!("read policy grid {}", grid_path.display()))?;
        let parsed: Vec<Strategy> =
            serde_yaml::from_str(&yaml).context("parse policy grid yaml")?;
        anyhow::ensure!(
            parsed.len() == args.n_parallel,
            "policy grid has {} cells, --n-parallel = {}",
            parsed.len(),
            args.n_parallel
        );
        parsed
    } else {
        (0..args.n_parallel)
            .map(|_| Strategy::default_for(args.max_lots))
            .collect()
    };
    // Strategies parsed but not yet plumbed into the v1 LobSimCuda which
    // hardcodes the WeightedByRealizedSharpe default policy (see C7
    // commit message). Keep the parse step so the YAML path is validated
    // end-to-end and ready when the v2 bytecode VM lands.
    let _ = strategies;

    let dev = MlDevice::cuda(0).map_err(|e| anyhow::anyhow!("MlDevice::cuda(0): {e}"))?;
    let trunk = CfcTrunk::new_random(&dev, &CfcConfig::default(), args.seed)
        .context("CfcTrunk::new_random")?;

    let cfg = BacktestHarnessConfig {
        data_root: args.data.clone(),
        predecoded_dir: args.predecoded_dir.clone().unwrap_or(args.data),
        n_parallel: args.n_parallel,
        decision_stride: args.decision_stride,
        target_annual_vol_units: args.target_annual_vol_units,
        annualisation_factor: args.annualisation_factor,
        max_lots: args.max_lots,
        max_events: args.max_events,
        latency_ns: args.latency_ns,
    };

    std::fs::create_dir_all(&args.out)
        .with_context(|| format!("create out dir {}", args.out.display()))?;

    let mut harness = BacktestHarness::new(cfg, &dev, trunk).context("BacktestHarness::new")?;
    let stats = harness.run().context("BacktestHarness::run")?;
    harness.write_artifacts(&args.out).context("write_artifacts")?;

    println!(
        "done: {} events processed, {} decisions taken; artifacts under {}",
        stats.events_processed,
        stats.decisions_taken,
        args.out.display()
    );
    Ok(())
}
