//! Monte Carlo simulation on backtest results.
//!
//! Two methods, both operating as pure post-processing on an existing backtest
//! output — no data reload, no strategy re-evaluation.
//!
//! ## ReturnBootstrap
//! Samples bar-level equity returns with replacement (IID bootstrap) to
//! generate N synthetic equity paths. Tests whether observed performance is
//! sensitive to the ordering of returns.
//!
//! ## BlockBootstrap
//! Same as ReturnBootstrap but draws contiguous blocks of size `block_size`
//! to preserve short-term autocorrelation. Better for trend-following
//! strategies where returns are serially correlated.
//!
//! ## TradeBootstrap
//! Shuffles the sequence of round-trip trade PnLs and reconstructs capital
//! curves. Tests whether the observed max-drawdown is a structural property
//! of the strategy or a result of unlucky trade sequencing.

use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
use rayon::prelude::*;
use serde::{Deserialize, Serialize};

use bt_kernel::{BtError, Result};

// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MonteCarloMethod {
    /// IID bootstrap on bar-level equity returns (sample with replacement).
    ReturnBootstrap,
    /// Block bootstrap: draw contiguous return blocks to preserve autocorrelation.
    BlockBootstrap { block_size: usize },
    /// Shuffle round-trip trade PnL sequence; rebuild capital curve.
    TradeBootstrap,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MonteCarloConfig {
    /// Number of simulation paths to generate.
    pub n_paths: usize,
    pub method: MonteCarloMethod,
    /// Quantile levels to report. E.g. `[0.05, 0.25, 0.50, 0.75, 0.95]`.
    #[serde(default = "default_confidence_levels")]
    pub confidence_levels: Vec<f64>,
    /// Fixed RNG seed for reproducibility. `None` → random seed each run.
    pub rng_seed: Option<u64>,
    /// Initial capital, used to compute returns and detect ruin.
    pub initial_capital: f64,
    /// If `true`, store all full equity paths (memory-intensive).
    #[serde(default)]
    pub store_paths: bool,
}

fn default_confidence_levels() -> Vec<f64> {
    vec![0.05, 0.10, 0.25, 0.50, 0.75, 0.90, 0.95]
}

// ---------------------------------------------------------------------------
// Result
// ---------------------------------------------------------------------------

/// Percentile values for a single metric across all paths.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DistributionStats {
    /// Same ordering as `MonteCarloConfig::confidence_levels`.
    pub percentiles: Vec<(f64, f64)>,
    pub mean: f64,
    pub std: f64,
    pub min: f64,
    pub max: f64,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MonteCarloResult {
    pub n_paths: usize,
    pub method_name: String,
    /// Distribution of final total return (e.g. 0.45 = +45%).
    pub final_return: DistributionStats,
    /// Distribution of maximum drawdown (0.0–1.0, higher = worse).
    pub max_drawdown: DistributionStats,
    /// Fraction of paths where final equity < initial_capital (ruin).
    pub prob_of_ruin: f64,
    /// Full equity paths indexed as `paths[path_idx][bar_idx]`.
    /// `None` when `store_paths = false`.
    pub paths: Option<Vec<Vec<f64>>>,
}

// ---------------------------------------------------------------------------
// Public entry point
// ---------------------------------------------------------------------------

/// Run Monte Carlo simulation.
///
/// - `equity`: bar-level equity curve from the backtest (used by Return/Block bootstrap).
/// - `trade_pnls`: net PnL per round-trip trade (used by TradeBootstrap).
///
/// Both slices may be empty for methods that don't use them.
pub fn run_monte_carlo(
    equity: &[f64],
    trade_pnls: &[f64],
    config: &MonteCarloConfig,
) -> Result<MonteCarloResult> {
    if config.n_paths == 0 {
        return Err(BtError::Strategy("monte_carlo: n_paths must be > 0".into()));
    }

    // License gate: Community tier is capped at 1000 simulations
    let max_sims = bt_license::max_monte_carlo_sims();
    if config.n_paths > max_sims {
        return Err(BtError::Strategy(format!(
            "monte_carlo: {} paths requested but Community license allows max {}. Upgrade to Pro for unlimited simulations.",
            config.n_paths, max_sims
        )));
    }
    if config.initial_capital <= 0.0 {
        return Err(BtError::Strategy(
            "monte_carlo: initial_capital must be > 0".into(),
        ));
    }

    let base_seed = resolve_base_seed(config.rng_seed);
    let method_name = method_name(&config.method);

    let (final_returns, max_drawdowns, paths) = match &config.method {
        MonteCarloMethod::ReturnBootstrap => {
            if equity.len() < 2 {
                return Err(BtError::Strategy(
                    "monte_carlo: equity curve needs at least 2 bars".into(),
                ));
            }
            run_return_bootstrap(equity, config, base_seed, false)
        }
        MonteCarloMethod::BlockBootstrap { block_size } => {
            if equity.len() < 2 {
                return Err(BtError::Strategy(
                    "monte_carlo: equity curve needs at least 2 bars".into(),
                ));
            }
            let bs = (*block_size).max(1);
            run_block_bootstrap(equity, config, base_seed, bs)
        }
        MonteCarloMethod::TradeBootstrap => {
            if trade_pnls.is_empty() {
                return Err(BtError::Strategy(
                    "monte_carlo: TradeBootstrap requires at least one trade".into(),
                ));
            }
            run_trade_bootstrap(trade_pnls, config, base_seed)
        }
    };

    let prob_of_ruin =
        final_returns.iter().filter(|&&r| r < 0.0).count() as f64 / config.n_paths as f64;

    Ok(MonteCarloResult {
        n_paths: config.n_paths,
        method_name,
        final_return: build_stats(&final_returns, &config.confidence_levels),
        max_drawdown: build_stats(&max_drawdowns, &config.confidence_levels),
        prob_of_ruin,
        paths,
    })
}

// ---------------------------------------------------------------------------
// Bootstrap implementations (rayon-parallel, deterministic per path)
// ---------------------------------------------------------------------------

fn run_return_bootstrap(
    equity: &[f64],
    config: &MonteCarloConfig,
    base_seed: u64,
    _block: bool,
) -> (Vec<f64>, Vec<f64>, Option<Vec<Vec<f64>>>) {
    let returns = bar_returns(equity);
    let n_returns = returns.len();
    let initial = config.initial_capital;
    let store = config.store_paths;

    let results: Vec<(f64, f64, Option<Vec<f64>>)> = (0..config.n_paths)
        .into_par_iter()
        .map(|i| {
            let mut rng = path_rng(base_seed, i);
            let sampled: Vec<f64> = (0..n_returns)
                .map(|_| returns[next_usize(&mut rng, n_returns)])
                .collect();
            let path = rebuild_equity(&sampled, initial);
            let fr = path.last().copied().unwrap_or(initial) / initial - 1.0;
            let mdd = max_drawdown(&path);
            let stored = if store { Some(path) } else { None };
            (fr, mdd, stored)
        })
        .collect();

    unzip_results(results)
}

fn run_block_bootstrap(
    equity: &[f64],
    config: &MonteCarloConfig,
    base_seed: u64,
    block_size: usize,
) -> (Vec<f64>, Vec<f64>, Option<Vec<Vec<f64>>>) {
    let returns = bar_returns(equity);
    let n_returns = returns.len();
    let initial = config.initial_capital;
    let store = config.store_paths;

    let results: Vec<(f64, f64, Option<Vec<f64>>)> = (0..config.n_paths)
        .into_par_iter()
        .map(|i| {
            let mut rng = path_rng(base_seed, i);
            let mut sampled = Vec::with_capacity(n_returns);
            while sampled.len() < n_returns {
                // Pick a random start for a block (wrap-around allowed).
                let start = next_usize(&mut rng, n_returns);
                let take = block_size.min(n_returns - sampled.len());
                for k in 0..take {
                    sampled.push(returns[(start + k) % n_returns]);
                }
            }
            sampled.truncate(n_returns);
            let path = rebuild_equity(&sampled, initial);
            let fr = path.last().copied().unwrap_or(initial) / initial - 1.0;
            let mdd = max_drawdown(&path);
            let stored = if store { Some(path) } else { None };
            (fr, mdd, stored)
        })
        .collect();

    unzip_results(results)
}

fn run_trade_bootstrap(
    trade_pnls: &[f64],
    config: &MonteCarloConfig,
    base_seed: u64,
) -> (Vec<f64>, Vec<f64>, Option<Vec<Vec<f64>>>) {
    let n_trades = trade_pnls.len();
    let initial = config.initial_capital;
    let store = config.store_paths;

    let results: Vec<(f64, f64, Option<Vec<f64>>)> = (0..config.n_paths)
        .into_par_iter()
        .map(|i| {
            let mut rng = path_rng(base_seed, i);
            // Shuffle trade indices (Fisher-Yates on indices).
            let mut indices: Vec<usize> = (0..n_trades).collect();
            for j in (1..n_trades).rev() {
                let k = next_usize(&mut rng, j + 1);
                indices.swap(j, k);
            }
            // Reconstruct capital curve: start at initial, add each trade PnL.
            let mut path = Vec::with_capacity(n_trades + 1);
            let mut capital = initial;
            path.push(capital);
            for &idx in &indices {
                capital += trade_pnls[idx];
                path.push(capital);
            }
            let fr = (capital - initial) / initial;
            let mdd = max_drawdown(&path);
            let stored = if store { Some(path) } else { None };
            (fr, mdd, stored)
        })
        .collect();

    unzip_results(results)
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

fn bar_returns(equity: &[f64]) -> Vec<f64> {
    equity
        .windows(2)
        .map(|w| {
            if w[0].abs() > 1e-15 {
                w[1] / w[0] - 1.0
            } else {
                0.0
            }
        })
        .collect()
}

fn rebuild_equity(returns: &[f64], initial: f64) -> Vec<f64> {
    let mut eq = Vec::with_capacity(returns.len() + 1);
    eq.push(initial);
    for &r in returns {
        let prev = *eq.last().unwrap();
        eq.push(prev * (1.0 + r));
    }
    eq
}

fn max_drawdown(equity: &[f64]) -> f64 {
    let mut peak = f64::NEG_INFINITY;
    let mut max_dd = 0.0_f64;
    for &e in equity {
        if e > peak {
            peak = e;
        }
        if peak > 0.0 {
            let dd = (peak - e) / peak;
            max_dd = max_dd.max(dd);
        }
    }
    max_dd
}

/// Deterministic per-path RNG: base_seed mixed with path index.
fn path_rng(base_seed: u64, path_idx: usize) -> ChaCha8Rng {
    // Fibonacci hashing to spread seeds uniformly.
    let seed = base_seed
        .wrapping_add(path_idx as u64)
        .wrapping_mul(0x9e3779b97f4a7c15);
    ChaCha8Rng::seed_from_u64(seed)
}

/// Draw a random usize in [0, n).
#[inline]
fn next_usize(rng: &mut ChaCha8Rng, n: usize) -> usize {
    rng.random_range(0..n)
}

/// Resolve base seed: use provided value or draw from thread-local entropy.
fn resolve_base_seed(seed: Option<u64>) -> u64 {
    seed.unwrap_or_else(rand::random::<u64>)
}

fn method_name(m: &MonteCarloMethod) -> String {
    match m {
        MonteCarloMethod::ReturnBootstrap => "return_bootstrap".into(),
        MonteCarloMethod::BlockBootstrap { block_size } => {
            format!("block_bootstrap(block_size={block_size})")
        }
        MonteCarloMethod::TradeBootstrap => "trade_bootstrap".into(),
    }
}

fn unzip_results(
    results: Vec<(f64, f64, Option<Vec<f64>>)>,
) -> (Vec<f64>, Vec<f64>, Option<Vec<Vec<f64>>>) {
    let mut final_returns = Vec::with_capacity(results.len());
    let mut max_drawdowns = Vec::with_capacity(results.len());
    let mut paths: Option<Vec<Vec<f64>>> = None;

    for (fr, mdd, path) in results {
        final_returns.push(fr);
        max_drawdowns.push(mdd);
        if let Some(p) = path {
            paths.get_or_insert_with(Vec::new).push(p);
        }
    }

    (final_returns, max_drawdowns, paths)
}

// ---------------------------------------------------------------------------
// Statistics
// ---------------------------------------------------------------------------

fn build_stats(values: &[f64], confidence_levels: &[f64]) -> DistributionStats {
    if values.is_empty() {
        return DistributionStats {
            percentiles: confidence_levels.iter().map(|&q| (q, 0.0)).collect(),
            mean: 0.0,
            std: 0.0,
            min: 0.0,
            max: 0.0,
        };
    }

    let mut sorted = values.to_vec();
    sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));

    let n = sorted.len();
    let mean = sorted.iter().sum::<f64>() / n as f64;
    let variance = sorted.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n as f64;
    let std = variance.sqrt();

    let percentiles = confidence_levels
        .iter()
        .map(|&q| {
            let idx = ((q * (n - 1) as f64).round() as usize).min(n - 1);
            (q, sorted[idx])
        })
        .collect();

    DistributionStats {
        percentiles,
        mean,
        std,
        min: sorted[0],
        max: sorted[n - 1],
    }
}
