use arrow::array::{Float64Array, TimestampNanosecondArray, UInt32Array, UInt8Array};
use arrow::record_batch::RecordBatch;
use serde::{Deserialize, Deserializer, Serialize};

use bt_kernel::{BtError, Result};

/// Deserialize a JSON null or NaN as 0.0 (NaN serializes to null in JSON).
fn null_as_zero<'de, D: Deserializer<'de>>(deserializer: D) -> std::result::Result<f64, D::Error> {
    Option::<f64>::deserialize(deserializer).map(|v| v.unwrap_or(0.0))
}

const NANOS_PER_DAY: i64 = 86_400_000_000_000;
const NANOS_PER_SEC: f64 = 1_000_000_000.0;

// ---------------------------------------------------------------------------
// PerformanceMetrics — equity-curve-based
// ---------------------------------------------------------------------------

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct PerformanceMetrics {
    // --- core ---
    #[serde(default, deserialize_with = "null_as_zero")]
    pub total_return: f64,
    #[serde(default, deserialize_with = "null_as_zero")]
    pub cagr: f64,
    #[serde(default, deserialize_with = "null_as_zero")]
    pub volatility: f64,
    #[serde(default, deserialize_with = "null_as_zero")]
    pub sharpe: f64,
    #[serde(default, deserialize_with = "null_as_zero")]
    pub sortino: f64,
    #[serde(default, deserialize_with = "null_as_zero")]
    pub calmar: f64,
    #[serde(default, deserialize_with = "null_as_zero")]
    pub max_drawdown: f64,

    // --- extended risk (serde default for backward compat with old fixtures) ---
    #[serde(default, deserialize_with = "null_as_zero")]
    pub skewness: f64,
    #[serde(default, deserialize_with = "null_as_zero")]
    pub kurtosis: f64,
    #[serde(default, deserialize_with = "null_as_zero")]
    pub tail_ratio: f64,
    #[serde(default, deserialize_with = "null_as_zero")]
    pub omega_ratio: f64,
    #[serde(default, deserialize_with = "null_as_zero")]
    pub ulcer_index: f64,

    // --- daily return stats ---
    #[serde(default, deserialize_with = "null_as_zero")]
    pub best_day: f64,
    #[serde(default, deserialize_with = "null_as_zero")]
    pub worst_day: f64,
    #[serde(default, deserialize_with = "null_as_zero")]
    pub avg_daily_return: f64,
    #[serde(default, deserialize_with = "null_as_zero")]
    pub pct_positive_days: f64,

    // --- drawdown duration ---
    #[serde(default, deserialize_with = "null_as_zero")]
    pub max_drawdown_duration_days: f64,

    // --- statistical significance ---
    /// t-statistic of the Sharpe ratio: sharpe * sqrt(n_years). |t| > 1.96 ≈ 95% significant.
    #[serde(default, deserialize_with = "null_as_zero")]
    pub tstat_sharpe: f64,

    // --- CAPM alpha ---
    /// Annualized CAPM alpha (excess return over beta-adjusted benchmark).
    #[serde(default, deserialize_with = "null_as_zero")]
    pub alpha: f64,
    /// Beta to the benchmark (equal-weight buy-and-hold of universe).
    #[serde(default, deserialize_with = "null_as_zero")]
    pub beta: f64,
    /// t-statistic of alpha from OLS regression. |t| > 1.96 ≈ 95% significant.
    #[serde(default, deserialize_with = "null_as_zero")]
    pub tstat_alpha: f64,

    // --- trade statistics (optional, populated when trades are available) ---
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub trade_stats: Option<TradeStatistics>,
}

// ---------------------------------------------------------------------------
// TradeStatistics — round-trip trade analysis
// ---------------------------------------------------------------------------

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TradeStatistics {
    pub total_trades: usize,
    pub round_trips: usize,

    // --- win/loss ---
    pub win_rate: f64,
    pub profit_factor: f64,
    pub expectancy: f64,
    pub avg_win: f64,
    pub avg_loss: f64,
    pub max_consecutive_wins: usize,
    pub max_consecutive_losses: usize,

    // --- holding periods (seconds) ---
    pub avg_holding_seconds: f64,
    pub median_holding_seconds: f64,
    pub min_holding_seconds: f64,
    pub max_holding_seconds: f64,

    // --- fees ---
    pub total_fees: f64,

    // --- exit reason counts (backward-compat via serde default) ---
    #[serde(default)]
    pub sl_exits: usize,
    #[serde(default)]
    pub tp_exits: usize,
    #[serde(default)]
    pub trailing_exits: usize,
}

/// A single round-trip trade: entry → exit (or partial close to flat).
#[derive(Clone, Debug)]
pub struct RoundTrip {
    pub symbol_id: u32,
    pub entry_ts_ns: i64,
    pub exit_ts_ns: i64,
    pub side: u8, // 1=long, 2=short
    pub entry_price: f64,
    pub exit_price: f64,
    pub quantity: f64,
    pub fees: f64,
    pub pnl: f64,
}

// ---------------------------------------------------------------------------
// Equity-curve metrics
// ---------------------------------------------------------------------------

/// Compute performance metrics with daily-return-based risk statistics.
///
/// - **Sharpe, volatility, Sortino**: computed on daily returns (last equity per
///   UTC day). When the backtest spans fewer than 2 calendar days, falls back to
///   per-bar returns with `trading_days_per_year * 86400 / bar_seconds`.
/// - **max_drawdown**: always on full-resolution equity (no precision loss).
/// - **CAGR**: bar-count-based formula with bars_per_year derived from
///   `trading_days_per_year` and `bar_seconds`.
#[tracing::instrument(level = "debug", skip_all)]
pub fn compute_performance_metrics(
    equity: &[f64],
    timestamps_ns: &[i64],
    bar_seconds: f64,
    trading_days_per_year: f64,
) -> Result<PerformanceMetrics> {
    compute_performance_metrics_with_rf(
        equity,
        timestamps_ns,
        bar_seconds,
        trading_days_per_year,
        0.0,
    )
}

/// Same as `compute_performance_metrics` but with an explicit annual risk-free rate.
/// Sharpe = (annualized_mean - rf) / vol, Sortino = (annualized_mean - rf) / downside_vol.
#[tracing::instrument(level = "debug", skip_all)]
pub fn compute_performance_metrics_with_rf(
    equity: &[f64],
    timestamps_ns: &[i64],
    bar_seconds: f64,
    trading_days_per_year: f64,
    risk_free_rate: f64,
) -> Result<PerformanceMetrics> {
    if equity.is_empty() {
        return Err(BtError::Data(
            "cannot compute metrics from empty equity curve".to_string(),
        ));
    }
    if equity.len() != timestamps_ns.len() {
        return Err(BtError::Data(
            "equity and timestamps must have the same length".to_string(),
        ));
    }

    let initial = equity[0];
    let final_value = *equity
        .last()
        .ok_or_else(|| BtError::Data("cannot resolve final equity value".to_string()))?;
    if initial <= 0.0 || final_value <= 0.0 {
        return Err(BtError::Data(format!(
            "equity values must be > 0, got initial={initial}, final={final_value}"
        )));
    }

    let total_return = final_value / initial - 1.0;

    // CAGR: bar-count-based (consistent with declared bar_interval).
    let bars_per_year = trading_days_per_year * 86_400.0 / bar_seconds.max(1.0);
    let periods = (equity.len() - 1).max(1) as f64;
    let cagr = (final_value / initial).powf(bars_per_year / periods) - 1.0;

    // Max drawdown on full-resolution equity.
    let max_drawdown = max_drawdown(equity);

    // Max drawdown duration on full-resolution equity + timestamps.
    let max_drawdown_duration_days = max_drawdown_duration(equity, timestamps_ns);

    // Resample to daily for risk metrics (Sharpe, vol, Sortino).
    let daily_equity = resample_to_daily(equity, timestamps_ns);
    let (returns, annualization_factor) = if daily_equity.len() >= 2 {
        (step_returns(&daily_equity), trading_days_per_year)
    } else {
        // Fallback for sub-day backtests: per-bar returns.
        (step_returns(equity), bars_per_year)
    };

    let volatility = annualized_std(&returns, annualization_factor);
    let ann_mean = annualized_mean(&returns, annualization_factor);
    let excess_return = ann_mean - risk_free_rate;
    let sharpe = if volatility == 0.0 {
        0.0
    } else {
        excess_return / volatility
    };

    let downside = downside_std(&returns, annualization_factor);
    let sortino = if downside == 0.0 {
        0.0
    } else {
        excess_return / downside
    };

    let calmar = if max_drawdown == 0.0 {
        0.0
    } else {
        cagr / max_drawdown.abs()
    };

    // Extended risk metrics on daily returns.
    let skewness = compute_skewness(&returns);
    let kurtosis = compute_kurtosis(&returns);
    let tail_ratio = compute_tail_ratio(&returns);
    let omega_ratio = compute_omega_ratio(&returns, 0.0);
    let ulcer_index = compute_ulcer_index(equity);

    // Daily return stats.
    let best_day = returns.iter().copied().fold(f64::NEG_INFINITY, f64::max);
    let worst_day = returns.iter().copied().fold(f64::INFINITY, f64::min);
    let best_day = if best_day.is_infinite() {
        0.0
    } else {
        best_day
    };
    let worst_day = if worst_day.is_infinite() {
        0.0
    } else {
        worst_day
    };

    let avg_daily_return = if returns.is_empty() {
        0.0
    } else {
        returns.iter().sum::<f64>() / returns.len() as f64
    };

    let pct_positive_days = if returns.is_empty() {
        0.0
    } else {
        returns.iter().filter(|&&r| r > 0.0).count() as f64 / returns.len() as f64
    };

    // t-stat of Sharpe: sharpe * sqrt(n_years)
    let n_years = periods / bars_per_year;
    let tstat_sharpe = sharpe * n_years.sqrt();

    Ok(PerformanceMetrics {
        total_return,
        cagr,
        volatility,
        sharpe,
        sortino,
        calmar,
        max_drawdown,
        skewness,
        kurtosis,
        tail_ratio,
        omega_ratio,
        ulcer_index,
        best_day,
        worst_day,
        avg_daily_return,
        pct_positive_days,
        max_drawdown_duration_days,
        tstat_sharpe,
        alpha: 0.0,
        beta: 0.0,
        tstat_alpha: 0.0,
        trade_stats: None,
    })
}

// ---------------------------------------------------------------------------
// CAPM alpha regression
// ---------------------------------------------------------------------------

/// Compute equal-weight buy-and-hold benchmark daily returns from per-symbol close prices.
///
/// Each symbol contributes 1/N weight. Daily returns are computed by resampling
/// each symbol to daily, computing returns, and averaging across symbols.
pub fn compute_benchmark_daily_returns(
    close_per_symbol: &[&[f64]],
    timestamps_ns: &[i64],
) -> Vec<f64> {
    if close_per_symbol.is_empty() || timestamps_ns.is_empty() {
        return Vec::new();
    }
    let n_sym = close_per_symbol.len();

    // Resample each symbol to daily and compute returns
    let mut symbol_daily_returns: Vec<Vec<f64>> = Vec::with_capacity(n_sym);
    for closes in close_per_symbol {
        let daily = resample_to_daily(closes, timestamps_ns);
        let rets = step_returns(&daily);
        symbol_daily_returns.push(rets);
    }

    // Find minimum length (symbols may differ by 1 due to rounding)
    let min_len = symbol_daily_returns
        .iter()
        .map(|r| r.len())
        .min()
        .unwrap_or(0);
    if min_len == 0 {
        return Vec::new();
    }

    // Equal-weight average across symbols
    let mut benchmark = vec![0.0; min_len];
    for rets in &symbol_daily_returns {
        for (i, &r) in rets.iter().take(min_len).enumerate() {
            benchmark[i] += r / n_sym as f64;
        }
    }
    benchmark
}

/// OLS regression result: y = alpha + beta * x.
pub struct OlsResult {
    pub alpha: f64,
    pub beta: f64,
    pub tstat_alpha: f64,
}

/// Run OLS regression: strategy_returns = alpha + beta * benchmark_returns.
/// Returns annualized alpha, beta, and t-stat of alpha.
///
/// `annualization_factor` is typically trading_days_per_year (e.g. 365.25 for crypto).
pub fn compute_capm_alpha(
    strategy_returns: &[f64],
    benchmark_returns: &[f64],
    annualization_factor: f64,
) -> OlsResult {
    let n = strategy_returns.len().min(benchmark_returns.len());
    if n < 3 {
        return OlsResult {
            alpha: 0.0,
            beta: 0.0,
            tstat_alpha: 0.0,
        };
    }

    let y = &strategy_returns[..n];
    let x = &benchmark_returns[..n];
    let nf = n as f64;

    // Means
    let x_mean = x.iter().sum::<f64>() / nf;
    let y_mean = y.iter().sum::<f64>() / nf;

    // Beta = Cov(x, y) / Var(x)
    let mut cov_xy = 0.0;
    let mut var_x = 0.0;
    for i in 0..n {
        let dx = x[i] - x_mean;
        let dy = y[i] - y_mean;
        cov_xy += dx * dy;
        var_x += dx * dx;
    }

    let beta = if var_x > 1e-30 { cov_xy / var_x } else { 0.0 };
    let alpha_daily = y_mean - beta * x_mean;

    // Residuals and SE(alpha)
    let mut sse = 0.0;
    for i in 0..n {
        let residual = y[i] - alpha_daily - beta * x[i];
        sse += residual * residual;
    }
    let mse = sse / (nf - 2.0);

    // SE(alpha) = sqrt(MSE * (1/n + x_mean^2 / sum((x - x_mean)^2)))
    let se_alpha = if var_x > 1e-30 {
        (mse * (1.0 / nf + x_mean * x_mean / var_x)).sqrt()
    } else {
        0.0
    };

    let tstat = if se_alpha > 1e-30 {
        alpha_daily / se_alpha
    } else {
        0.0
    };
    let alpha_ann = alpha_daily * annualization_factor;

    OlsResult {
        alpha: alpha_ann,
        beta,
        tstat_alpha: tstat,
    }
}

// ---------------------------------------------------------------------------
// Trade statistics from round-trip analysis
// ---------------------------------------------------------------------------

/// Compute round-trip trade statistics from raw trade fills.
///
/// `fills` are tuples: `(symbol_id, exec_ts_ns, side, quantity, fill_price, fees)`.
/// Side: 1 = buy, 2 = sell.
///
/// Round-trip = position goes from 0 → non-zero → 0.
pub fn compute_trade_statistics(
    fills: &[(u32, i64, u8, f64, f64, f64)],
    exit_reasons: Option<&[u8]>,
) -> TradeStatistics {
    // Count exit reasons when available.
    let (sl_exits, tp_exits, trailing_exits) = exit_reasons
        .map(|reasons| {
            let mut sl = 0usize;
            let mut tp = 0usize;
            let mut tr = 0usize;
            for &r in reasons {
                match r {
                    1 => sl += 1,
                    2 => tp += 1,
                    3 => tr += 1,
                    _ => {}
                }
            }
            (sl, tp, tr)
        })
        .unwrap_or((0, 0, 0));

    if fills.is_empty() {
        return TradeStatistics {
            total_trades: 0,
            round_trips: 0,
            win_rate: 0.0,
            profit_factor: 0.0,
            expectancy: 0.0,
            avg_win: 0.0,
            avg_loss: 0.0,
            max_consecutive_wins: 0,
            max_consecutive_losses: 0,
            avg_holding_seconds: 0.0,
            median_holding_seconds: 0.0,
            min_holding_seconds: 0.0,
            max_holding_seconds: 0.0,
            total_fees: 0.0,
            sl_exits,
            tp_exits,
            trailing_exits,
        };
    }

    let total_fees: f64 = fills.iter().map(|f| f.5).sum();
    let total_trades = fills.len();

    // Build round trips per symbol.
    let round_trips = build_round_trips(fills);
    let n_rt = round_trips.len();

    if n_rt == 0 {
        return TradeStatistics {
            total_trades,
            round_trips: 0,
            win_rate: 0.0,
            profit_factor: 0.0,
            expectancy: 0.0,
            avg_win: 0.0,
            avg_loss: 0.0,
            max_consecutive_wins: 0,
            max_consecutive_losses: 0,
            avg_holding_seconds: 0.0,
            median_holding_seconds: 0.0,
            min_holding_seconds: 0.0,
            max_holding_seconds: 0.0,
            total_fees,
            sl_exits,
            tp_exits,
            trailing_exits,
        };
    }

    // Win/loss analysis.
    let mut wins = Vec::new();
    let mut losses = Vec::new();
    let mut holding_secs = Vec::with_capacity(n_rt);

    for rt in &round_trips {
        if rt.pnl > 0.0 {
            wins.push(rt.pnl);
        } else {
            losses.push(rt.pnl);
        }
        let hold = (rt.exit_ts_ns - rt.entry_ts_ns) as f64 / NANOS_PER_SEC;
        holding_secs.push(hold);
    }

    let win_rate = wins.len() as f64 / n_rt as f64;

    let gross_profit: f64 = wins.iter().sum();
    let gross_loss: f64 = losses.iter().map(|l| l.abs()).sum();
    let profit_factor = if gross_loss == 0.0 {
        if gross_profit > 0.0 {
            f64::INFINITY
        } else {
            0.0
        }
    } else {
        gross_profit / gross_loss
    };

    let avg_win = if wins.is_empty() {
        0.0
    } else {
        gross_profit / wins.len() as f64
    };
    let avg_loss = if losses.is_empty() {
        0.0
    } else {
        losses.iter().sum::<f64>() / losses.len() as f64
    };

    let expectancy = round_trips.iter().map(|rt| rt.pnl).sum::<f64>() / n_rt as f64;

    // Consecutive wins/losses (sorted by exit time).
    let mut sorted_rts = round_trips.clone();
    sorted_rts.sort_by_key(|rt| rt.exit_ts_ns);

    let (max_consecutive_wins, max_consecutive_losses) = max_consecutive(&sorted_rts);

    // Holding period stats.
    holding_secs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
    let avg_holding_seconds = holding_secs.iter().sum::<f64>() / holding_secs.len() as f64;
    let median_holding_seconds = median_f64(&holding_secs);
    let min_holding_seconds = holding_secs.first().copied().unwrap_or(0.0);
    let max_holding_seconds = holding_secs.last().copied().unwrap_or(0.0);

    TradeStatistics {
        total_trades,
        round_trips: n_rt,
        win_rate,
        profit_factor,
        expectancy,
        avg_win,
        avg_loss,
        max_consecutive_wins,
        max_consecutive_losses,
        avg_holding_seconds,
        median_holding_seconds,
        min_holding_seconds,
        max_holding_seconds,
        total_fees,
        sl_exits,
        tp_exits,
        trailing_exits,
    }
}

/// Build round-trip trades from a chronological list of fills.
///
/// Tracks position per symbol. A round trip is emitted when position returns to 0
/// (or crosses zero, which closes the previous direction and opens a new one).
fn build_round_trips(fills: &[(u32, i64, u8, f64, f64, f64)]) -> Vec<RoundTrip> {
    use std::collections::HashMap;

    struct OpenPosition {
        entry_ts_ns: i64,
        side: u8,
        avg_entry_price: f64,
        quantity: f64,
        fees: f64,
    }

    let mut positions: HashMap<u32, OpenPosition> = HashMap::new();
    let mut round_trips = Vec::new();

    for &(symbol_id, exec_ts_ns, side, qty, fill_price, fees) in fills {
        let signed_delta = if side == 1 { qty } else { -qty };

        if let Some(open) = positions.get_mut(&symbol_id) {
            let old_pos = if open.side == 1 {
                open.quantity
            } else {
                -open.quantity
            };
            let new_pos = old_pos + signed_delta;

            // Same direction: add to position.
            if (old_pos > 0.0 && new_pos > old_pos) || (old_pos < 0.0 && new_pos < old_pos) {
                let total_cost = open.avg_entry_price * open.quantity + fill_price * qty;
                open.quantity = new_pos.abs();
                open.avg_entry_price = total_cost / open.quantity;
                open.fees += fees;
                continue;
            }

            // Position reduced or closed.
            let close_qty = qty.min(open.quantity);
            let pnl = if open.side == 1 {
                // Long: profit = (exit - entry) * qty
                (fill_price - open.avg_entry_price) * close_qty - open.fees - fees
            } else {
                // Short: profit = (entry - exit) * qty
                (open.avg_entry_price - fill_price) * close_qty - open.fees - fees
            };

            round_trips.push(RoundTrip {
                symbol_id,
                entry_ts_ns: open.entry_ts_ns,
                exit_ts_ns: exec_ts_ns,
                side: open.side,
                entry_price: open.avg_entry_price,
                exit_price: fill_price,
                quantity: close_qty,
                fees: open.fees + fees,
                pnl,
            });

            if new_pos.abs() < 1e-12 {
                // Fully closed.
                positions.remove(&symbol_id);
            } else if (old_pos > 0.0 && new_pos < 0.0) || (old_pos < 0.0 && new_pos > 0.0) {
                // Position flipped direction — open new position with remainder.
                let new_side = if new_pos > 0.0 { 1_u8 } else { 2_u8 };
                positions.insert(
                    symbol_id,
                    OpenPosition {
                        entry_ts_ns: exec_ts_ns,
                        side: new_side,
                        avg_entry_price: fill_price,
                        quantity: new_pos.abs(),
                        fees: 0.0,
                    },
                );
            } else {
                // Partial close, same direction.
                open.quantity = new_pos.abs();
                open.fees = 0.0; // fees already attributed to closed portion
            }
        } else {
            // New position.
            positions.insert(
                symbol_id,
                OpenPosition {
                    entry_ts_ns: exec_ts_ns,
                    side,
                    avg_entry_price: fill_price,
                    quantity: qty,
                    fees,
                },
            );
        }
    }

    round_trips
}

fn max_consecutive(round_trips: &[RoundTrip]) -> (usize, usize) {
    let mut max_wins = 0_usize;
    let mut max_losses = 0_usize;
    let mut cur_wins = 0_usize;
    let mut cur_losses = 0_usize;

    for rt in round_trips {
        if rt.pnl > 0.0 {
            cur_wins += 1;
            cur_losses = 0;
            max_wins = max_wins.max(cur_wins);
        } else {
            cur_losses += 1;
            cur_wins = 0;
            max_losses = max_losses.max(cur_losses);
        }
    }

    (max_wins, max_losses)
}

fn median_f64(sorted: &[f64]) -> f64 {
    if sorted.is_empty() {
        return 0.0;
    }
    let mid = sorted.len() / 2;
    if sorted.len().is_multiple_of(2) {
        (sorted[mid - 1] + sorted[mid]) / 2.0
    } else {
        sorted[mid]
    }
}

// ---------------------------------------------------------------------------
// Daily resampling & return helpers
// ---------------------------------------------------------------------------

/// Resample equity to daily resolution (last value per UTC day).
pub fn resample_to_daily(equity: &[f64], timestamps_ns: &[i64]) -> Vec<f64> {
    if equity.is_empty() {
        return Vec::new();
    }

    let mut daily = Vec::new();
    let mut current_day = timestamps_ns[0] / NANOS_PER_DAY;
    let mut last_equity = equity[0];

    for (&eq, &ts) in equity.iter().zip(timestamps_ns.iter()) {
        let day = ts / NANOS_PER_DAY;
        if day != current_day {
            daily.push(last_equity);
            current_day = day;
        }
        last_equity = eq;
    }
    // Emit the last day.
    daily.push(last_equity);

    daily
}

/// Compute actual daily step returns from equity + timestamps.
/// Returns empty Vec when fewer than 2 calendar days of data.
pub fn compute_daily_returns(equity: &[f64], timestamps_ns: &[i64]) -> Vec<f64> {
    let daily_equity = resample_to_daily(equity, timestamps_ns);
    if daily_equity.len() < 2 {
        return Vec::new();
    }
    step_returns(&daily_equity)
}

fn step_returns(equity: &[f64]) -> Vec<f64> {
    let mut out = Vec::with_capacity(equity.len().saturating_sub(1));
    for window in equity.windows(2) {
        let prev = window[0];
        let curr = window[1];
        if prev == 0.0 {
            out.push(0.0);
        } else {
            out.push(curr / prev - 1.0);
        }
    }
    out
}

fn annualized_mean(returns: &[f64], bars_per_year: f64) -> f64 {
    if returns.is_empty() {
        return 0.0;
    }
    let mean = returns.iter().sum::<f64>() / returns.len() as f64;
    mean * bars_per_year
}

fn annualized_std(returns: &[f64], bars_per_year: f64) -> f64 {
    if returns.len() < 2 {
        return 0.0;
    }

    let mean = returns.iter().sum::<f64>() / returns.len() as f64;
    let variance = returns
        .iter()
        .map(|value| {
            let delta = value - mean;
            delta * delta
        })
        .sum::<f64>()
        / (returns.len() as f64 - 1.0);

    variance.sqrt() * bars_per_year.sqrt()
}

fn downside_std(returns: &[f64], bars_per_year: f64) -> f64 {
    let downside = returns
        .iter()
        .copied()
        .filter(|value| *value < 0.0)
        .collect::<Vec<_>>();

    annualized_std(&downside, bars_per_year)
}

fn max_drawdown(equity: &[f64]) -> f64 {
    if equity.is_empty() {
        return 0.0;
    }

    let mut peak = equity[0];
    let mut max_dd: f64 = 0.0;
    for value in equity {
        peak = peak.max(*value);
        if peak != 0.0 {
            max_dd = max_dd.min(value / peak - 1.0);
        }
    }
    max_dd
}

// ---------------------------------------------------------------------------
// Extended risk metrics
// ---------------------------------------------------------------------------

/// Maximum drawdown duration in days (longest time between equity peaks).
fn max_drawdown_duration(equity: &[f64], timestamps_ns: &[i64]) -> f64 {
    if equity.len() < 2 {
        return 0.0;
    }

    let mut peak = equity[0];
    let mut peak_ts = timestamps_ns[0];
    let mut max_duration_ns: i64 = 0;

    for (i, &eq) in equity.iter().enumerate() {
        if eq >= peak {
            // New peak — measure duration of the drawdown that just ended.
            let duration = timestamps_ns[i] - peak_ts;
            if duration > max_duration_ns && peak_ts != timestamps_ns[i] {
                max_duration_ns = duration;
            }
            peak = eq;
            peak_ts = timestamps_ns[i];
        }
    }

    // If we end in drawdown, measure from last peak to end.
    let final_duration = timestamps_ns.last().unwrap_or(&0) - peak_ts;
    if final_duration > max_duration_ns {
        max_duration_ns = final_duration;
    }

    max_duration_ns as f64 / NANOS_PER_DAY as f64
}

/// Skewness of returns (Fisher's definition, sample skewness).
fn compute_skewness(returns: &[f64]) -> f64 {
    let n = returns.len();
    if n < 3 {
        return 0.0;
    }

    let mean = returns.iter().sum::<f64>() / n as f64;
    let m2: f64 = returns.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / n as f64;
    let m3: f64 = returns.iter().map(|r| (r - mean).powi(3)).sum::<f64>() / n as f64;

    if m2 == 0.0 {
        return 0.0;
    }

    let std = m2.sqrt();
    m3 / std.powi(3)
}

/// Excess kurtosis of returns (Fisher's definition).
fn compute_kurtosis(returns: &[f64]) -> f64 {
    let n = returns.len();
    if n < 4 {
        return 0.0;
    }

    let mean = returns.iter().sum::<f64>() / n as f64;
    let m2: f64 = returns.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / n as f64;
    let m4: f64 = returns.iter().map(|r| (r - mean).powi(4)).sum::<f64>() / n as f64;

    if m2 == 0.0 {
        return 0.0;
    }

    m4 / m2.powi(2) - 3.0
}

/// Tail ratio: |95th percentile| / |5th percentile| of returns.
/// Values > 1 indicate fatter right tail (positive skew).
fn compute_tail_ratio(returns: &[f64]) -> f64 {
    if returns.len() < 20 {
        return 0.0;
    }

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

    let p5_idx = (sorted.len() as f64 * 0.05).floor() as usize;
    let p95_idx = (sorted.len() as f64 * 0.95).floor() as usize;
    let p95_idx = p95_idx.min(sorted.len() - 1);

    let p5 = sorted[p5_idx].abs();
    let p95 = sorted[p95_idx].abs();

    if p5 == 0.0 {
        if p95 > 0.0 {
            f64::INFINITY
        } else {
            0.0
        }
    } else {
        p95 / p5
    }
}

/// Omega ratio: probability-weighted ratio of gains to losses above/below threshold.
fn compute_omega_ratio(returns: &[f64], threshold: f64) -> f64 {
    if returns.is_empty() {
        return 0.0;
    }

    let mut sum_above = 0.0_f64;
    let mut sum_below = 0.0_f64;

    for &r in returns {
        if r > threshold {
            sum_above += r - threshold;
        } else {
            sum_below += threshold - r;
        }
    }

    if sum_below == 0.0 {
        if sum_above > 0.0 {
            f64::INFINITY
        } else {
            0.0
        }
    } else {
        sum_above / sum_below
    }
}

/// Ulcer Index: root-mean-square of percentage drawdowns.
/// Measures the depth and duration of drawdowns (lower = better).
fn compute_ulcer_index(equity: &[f64]) -> f64 {
    if equity.len() < 2 {
        return 0.0;
    }

    let mut peak = equity[0];
    let mut sum_sq = 0.0_f64;
    let mut count = 0_usize;

    for &eq in equity {
        peak = peak.max(eq);
        if peak > 0.0 {
            let dd_pct = (eq - peak) / peak;
            sum_sq += dd_pct * dd_pct;
            count += 1;
        }
    }

    if count == 0 {
        return 0.0;
    }

    (sum_sq / count as f64).sqrt()
}

// ---------------------------------------------------------------------------
// Round-trip PnL extraction from trades RecordBatch
// ---------------------------------------------------------------------------

/// Extract net PnL per completed round-trip trade from a trades `RecordBatch`.
///
/// The batch is the one returned by `BacktestResult::trades` — it contains
/// individual fill events (one row per position change). This function
/// reconstructs round trips using the same FIFO matching logic as the main
/// analytics pipeline and returns each round trip's net PnL (after fees).
///
/// Returns an empty Vec when there are no trades or the batch columns are
/// missing / have unexpected types.
pub fn extract_round_trip_pnls(trades: &RecordBatch) -> Vec<f64> {
    // Extract required columns; return empty on any failure.
    let Some(symbol_ids) = trades
        .column_by_name("symbol_id")
        .and_then(|a| a.as_any().downcast_ref::<UInt32Array>())
    else {
        return vec![];
    };
    let Some(exec_ts) = trades
        .column_by_name("execution_timestamp")
        .and_then(|a| a.as_any().downcast_ref::<TimestampNanosecondArray>())
    else {
        return vec![];
    };
    let Some(sides) = trades
        .column_by_name("side")
        .and_then(|a| a.as_any().downcast_ref::<UInt8Array>())
    else {
        return vec![];
    };
    let Some(quantities) = trades
        .column_by_name("quantity")
        .and_then(|a| a.as_any().downcast_ref::<Float64Array>())
    else {
        return vec![];
    };
    let Some(fill_prices) = trades
        .column_by_name("fill_price")
        .and_then(|a| a.as_any().downcast_ref::<Float64Array>())
    else {
        return vec![];
    };
    let Some(fees) = trades
        .column_by_name("fees")
        .and_then(|a| a.as_any().downcast_ref::<Float64Array>())
    else {
        return vec![];
    };

    let n = trades.num_rows();
    let fills: Vec<(u32, i64, u8, f64, f64, f64)> = (0..n)
        .map(|i| {
            (
                symbol_ids.value(i),
                exec_ts.value(i),
                sides.value(i),
                quantities.value(i),
                fill_prices.value(i),
                fees.value(i),
            )
        })
        .collect();

    build_round_trips(&fills)
        .into_iter()
        .map(|rt| rt.pnl)
        .collect()
}
