//! Epoch-level financial metrics computed from GPU trade statistics
//! and action counts. These are pushed to Prometheus at the end of each epoch.

use crate::cuda_pipeline::gpu_experience_collector::TradeStats;

/// Financial metrics summary for a single training epoch.
#[derive(Debug, Clone, Default)]
pub(crate) struct EpochFinancials {
    pub sharpe: f64,
    pub sortino: f64,
    pub win_rate: f64,
    pub max_drawdown: f64,
    pub profit_factor: f64,
    pub total_return: f64,
    pub avg_return: f64,
    pub total_trades: usize,
    pub buy_pct: f64,
    pub sell_pct: f64,
    pub hold_pct: f64,
}

/// Compute financial metrics from GPU-sourced trade statistics and action counts.
///
/// - `trade_stats`: per-epoch trade statistics from `collector.collect_trade_stats()`
/// - `action_counts`: 9-element array (exposure levels), grouped into BUY/SELL/HOLD
/// - `initial_capital`: starting equity for return calculation (default 100_000)
/// - `bars_per_day`: number of bars in one trading day (390 for 1-min, 78 for 5-min, etc.)
///   Used for Sharpe/Sortino annualization and return scaling. Pass from data pipeline's BarSize.
pub(crate) fn compute_epoch_financials(
    trade_stats: &TradeStats,
    action_counts: &[usize; 9],
    initial_capital: f64,
    bars_per_day: f64,
) -> EpochFinancials {
    if trade_stats.total_trades == 0 && trade_stats.step_returns.is_empty() {
        return EpochFinancials::default();
    }

    let total_trades = trade_stats.total_trades;
    let winning_trades = trade_stats.winning_trades;

    // Win rate from actual trade completions
    let win_rate = if total_trades > 0 {
        winning_trades as f64 / total_trades as f64
    } else {
        0.0
    };

    // Profit factor from GPU-accumulated sums
    let profit_factor = if trade_stats.sum_losses > 1e-10 {
        trade_stats.sum_wins / trade_stats.sum_losses
    } else if trade_stats.sum_wins > 0.0 {
        f64::INFINITY
    } else {
        0.0
    };

    // Annualized return from mean per-bar return (not compounded).
    // Compounding even 10K bars produces extreme numbers when per-bar returns
    // are biased. Annualized mean is more informative for monitoring:
    //   annual_return = mean_per_bar × bars_per_year
    let bars_per_year = bars_per_day * 252.0;
    let returns_slice = &trade_stats.step_returns;
    let n_returns = returns_slice.len();
    let mean_return = if n_returns > 0 {
        returns_slice.iter().sum::<f64>() / n_returns as f64
    } else {
        0.0
    };
    let total_return = mean_return * bars_per_year; // annualized fractional return
    let avg_return = if total_trades > 0 {
        total_return / total_trades as f64
    } else {
        0.0
    };

    // Sharpe ratio from step_returns (per-bar, annualized)
    let annualization = bars_per_year.sqrt();

    let returns = &trade_stats.step_returns;
    let n = returns.len();
    let (sharpe, sortino) = if n > 1 {
        let mean: f64 = returns.iter().sum::<f64>() / n as f64;
        let variance: f64 = returns.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / n as f64;
        let std = variance.sqrt();

        let s = if std > 1e-10 {
            (mean / std) * annualization
        } else {
            0.0
        };

        // Sortino from negative step_returns only
        let downside: Vec<f64> = returns.iter().filter(|&&r| r < 0.0).copied().collect();
        let sort = if downside.len() > 1 {
            let down_var: f64 = downside.iter().map(|r| r.powi(2)).sum::<f64>()
                / downside.len() as f64;
            let down_std = down_var.sqrt();
            if down_std > 1e-10 {
                (mean / down_std) * annualization
            } else {
                0.0
            }
        } else {
            0.0
        };

        (s, sort)
    } else {
        (0.0, 0.0)
    };

    // MaxDD from step_returns equity curve over last 10K bars, capped at 100%.
    // Full-epoch compounding (2M+ bars) produces >100% drawdowns from numerical noise.
    const MAX_DD_WINDOW: usize = 10_000;
    let dd_start = returns.len().saturating_sub(MAX_DD_WINDOW);
    let mut equity = initial_capital;
    let mut peak = equity;
    let mut max_dd = 0.0_f64;
    for &ret in &returns[dd_start..] {
        equity *= 1.0 + ret;
        if equity > peak {
            peak = equity;
        }
        let dd = if peak > 1e-10 {
            (peak - equity) / peak
        } else {
            0.0
        };
        if dd > max_dd {
            max_dd = dd;
        }
    }
    // Cap at 100% — drawdown cannot exceed total liquidation
    let max_dd = max_dd.min(1.0);

    // Action distribution: 9 exposure actions -> BUY/SELL/HOLD
    // [S100, S75, S50, S25, Flat, L25, L50, L75, L100]
    //  0     1    2    3    4     5    6    7    8
    // Sell = indices 0-3, Hold = index 4, Buy = indices 5-8
    let total_actions: usize = action_counts.iter().sum();
    let (buy_pct, sell_pct, hold_pct) = if total_actions > 0 {
        let sell: usize = action_counts.iter().take(4).sum();  // S100+S75+S50+S25
        let hold = *action_counts.get(4).unwrap_or(&0);       // Flat
        let buy: usize = action_counts.iter().skip(5).sum();   // L25+L50+L75+L100
        let t = total_actions as f64;
        (buy as f64 / t, sell as f64 / t, hold as f64 / t)
    } else {
        (0.0, 0.0, 0.0)
    };

    EpochFinancials {
        sharpe,
        sortino,
        win_rate,
        max_drawdown: max_dd,
        profit_factor,
        total_return,
        avg_return,
        total_trades,
        buy_pct,
        sell_pct,
        hold_pct,
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use super::*;

    #[test]
    fn test_empty_trade_stats() {
        let ts = TradeStats::default();
        let f = compute_epoch_financials(&ts, &[0; 9], 100_000.0, 390.0);
        assert_eq!(f.total_trades, 0);
        assert_eq!(f.sharpe, 0.0);
    }

    #[test]
    fn test_all_winning_trades() {
        let ts = TradeStats {
            total_trades: 5,
            winning_trades: 5,
            losing_trades: 0,
            sum_wins: 0.05,
            sum_losses: 0.0,
            sum_returns: 0.05,
            sum_sq_returns: 0.0005,
            step_returns: vec![0.01, 0.02, 0.03, 0.015, 0.025],
        };
        let f = compute_epoch_financials(&ts, &[0; 9], 100_000.0, 390.0);
        assert_eq!(f.win_rate, 1.0);
        assert_eq!(f.total_trades, 5);
        assert!(f.sharpe > 0.0, "sharpe={}", f.sharpe);
        assert!(f.max_drawdown < 1e-10, "max_dd={}", f.max_drawdown);
        assert!(f.profit_factor.is_infinite(), "pf={}", f.profit_factor);
    }

    #[test]
    fn test_mixed_trades() {
        let ts = TradeStats {
            total_trades: 5,
            winning_trades: 3,
            losing_trades: 2,
            sum_wins: 0.03,
            sum_losses: 0.01,
            sum_returns: 0.02,
            sum_sq_returns: 0.001,
            step_returns: vec![0.01, -0.005, 0.008, -0.003, 0.005],
        };
        let f = compute_epoch_financials(&ts, &[0; 9], 100_000.0, 390.0);
        assert_eq!(f.total_trades, 5);
        assert!((f.win_rate - 0.6).abs() < 1e-10, "win_rate={}", f.win_rate);
        assert!(f.total_return > 0.0, "total_return={}", f.total_return);
        assert!(f.profit_factor > 1.0, "profit_factor={}", f.profit_factor);
        assert!(f.max_drawdown > 0.0, "max_dd={}", f.max_drawdown);
        assert!(f.sortino != 0.0, "sortino should be non-zero with losses");
    }

    #[test]
    fn test_action_distribution_9_actions() {
        let mut actions = [0usize; 9];
        actions[0] = 10; // S100 -> SELL
        actions[1] = 10; // S75 -> SELL
        actions[4] = 20; // Flat -> HOLD
        actions[7] = 30; // L75 -> BUY
        actions[8] = 30; // L100 -> BUY
        let ts = TradeStats {
            total_trades: 1,
            winning_trades: 1,
            losing_trades: 0,
            sum_wins: 0.01,
            sum_losses: 0.0,
            sum_returns: 0.01,
            sum_sq_returns: 0.0001,
            step_returns: vec![0.01],
        };
        let f = compute_epoch_financials(&ts, &actions, 100_000.0, 390.0);
        // SELL=20/100=0.2, HOLD=20/100=0.2, BUY=60/100=0.6
        assert!((f.sell_pct - 0.2).abs() < 1e-10, "sell_pct={}", f.sell_pct);
        assert!((f.hold_pct - 0.2).abs() < 1e-10, "hold_pct={}", f.hold_pct);
        assert!((f.buy_pct - 0.6).abs() < 1e-10, "buy_pct={}", f.buy_pct);
    }

    #[test]
    fn test_action_distribution_all_indices_counted() {
        let mut actions = [0usize; 9];
        for i in 0..9 { actions[i] = 10; }
        let ts = TradeStats {
            total_trades: 1,
            winning_trades: 1,
            losing_trades: 0,
            sum_wins: 0.01,
            sum_losses: 0.0,
            sum_returns: 0.01,
            sum_sq_returns: 0.0001,
            step_returns: vec![0.01],
        };
        let f = compute_epoch_financials(&ts, &actions, 100_000.0, 390.0);
        let total = f.buy_pct + f.sell_pct + f.hold_pct;
        assert!(
            (total - 1.0).abs() < 1e-10,
            "BUY+SELL+HOLD must sum to 1.0, got {total:.6} (buy={}, sell={}, hold={})",
            f.buy_pct, f.sell_pct, f.hold_pct
        );
    }
}
