//! Multi-Asset Portfolio Management for DQN
//!
//! This module provides multi-symbol portfolio tracking with:
//! - Independent position tracking per symbol
//! - Correlation-aware risk calculation (`VaR`)
//! - Transfer learning support (shared Q-network)
//! - Portfolio-level analytics (Sharpe, drawdown, win rate)
//!
//! # Architecture
//!
//! ```text
//! MultiAssetPortfolioTracker
//! ├── positions: HashMap<Symbol, PortfolioTracker>
//! ├── correlation_matrix: Array2<f64>
//! ├── opportunity_scores: HashMap<Symbol, f64>
//! └── portfolio_history: Vec<PortfolioSnapshot>
//! ```
//!
//! # Example
//!
//! ```rust
//! use ml::dqn::multi_asset::{MultiAssetPortfolioTracker, Symbol};
//! use rust_decimal::Decimal;
//!
//! let symbols = vec![Symbol::new("ES_FUT"), Symbol::new("NQ_FUT")];
//! let tracker = MultiAssetPortfolioTracker::new(symbols, Decimal::from(10_000));
//! ```

use super::action_space::FactoredAction;
use super::portfolio_tracker::PortfolioTracker;
use ndarray::Array2;
use rust_decimal::Decimal;
use std::collections::HashMap;
use tracing::debug;

/// Symbol identifier for multi-asset tracking
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Symbol(String);

impl Symbol {
    /// Create a new symbol
    pub fn new(name: &str) -> Self {
        Self(name.to_owned())
    }

    /// Get symbol name
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

/// Portfolio snapshot for historical tracking
#[derive(Debug, Clone)]
struct PortfolioSnapshot {
    /// Total portfolio value at snapshot time
    total_value: f64,
}

/// Multi-asset portfolio tracker with correlation-aware risk
#[derive(Debug, Clone)]
pub struct MultiAssetPortfolioTracker {
    /// Independent portfolio tracker per symbol
    positions: HashMap<Symbol, PortfolioTracker>,
    /// Correlation matrix (N x N where N = number of symbols)
    /// Element [i,j] = correlation between symbol i and symbol j
    /// Diagonal = 1.0, off-diagonal = -1.0 to +1.0
    correlation_matrix: Array2<f64>,
    /// List of all symbols in order (for indexing into correlation matrix)
    symbols: Vec<Symbol>,
    /// Opportunity scores for symbol selection (e.g., volatility, momentum)
    opportunity_scores: HashMap<Symbol, f64>,
    /// Portfolio value history for analytics
    portfolio_history: Vec<PortfolioSnapshot>,
    /// Trade P&L history for win rate calculation
    trade_pnls: Vec<f64>,
    /// Maximum position size per symbol
    max_positions: HashMap<Symbol, f32>,
}

impl MultiAssetPortfolioTracker {
    /// Create a new multi-asset portfolio tracker
    ///
    /// # Arguments
    ///
    /// * `symbols` - List of symbols to track
    /// * `initial_capital_per_symbol` - Starting capital for each symbol
    ///
    /// # Example
    ///
    /// ```
    /// use ml::dqn::multi_asset::{MultiAssetPortfolioTracker, Symbol};
    /// use rust_decimal::Decimal;
    ///
    /// let symbols = vec![Symbol::new("ES_FUT"), Symbol::new("NQ_FUT")];
    /// let tracker = MultiAssetPortfolioTracker::new(symbols, Decimal::from(10_000));
    /// ```
    pub fn new(symbols: Vec<Symbol>, initial_capital_per_symbol: Decimal) -> Self {
        let num_symbols = symbols.len();

        // Create independent portfolio tracker for each symbol
        let positions = symbols
            .iter()
            .map(|sym| {
                let capital = initial_capital_per_symbol
                    .to_string()
                    .parse::<f32>()
                    .unwrap_or(100_000.0);
                let tracker = PortfolioTracker::new(
                    capital,
                    0.0001, // Default spread
                    0.0,    // No cash reserve by default
                );
                (sym.clone(), tracker)
            })
            .collect();

        // Initialize correlation matrix as identity (uncorrelated)
        let correlation_matrix = Array2::eye(num_symbols);

        // Initialize opportunity scores to zero
        let opportunity_scores = symbols.iter().map(|sym| (sym.clone(), 0.0)).collect();

        Self {
            positions,
            correlation_matrix,
            symbols,
            opportunity_scores,
            portfolio_history: Vec::new(),
            trade_pnls: Vec::new(),
            max_positions: HashMap::new(),
        }
    }

    /// Create multi-asset tracker with cash reserve requirement
    pub fn with_cash_reserve(
        symbols: Vec<Symbol>,
        initial_capital_per_symbol: Decimal,
        cash_reserve_percent: f64,
    ) -> Self {
        let num_symbols = symbols.len();

        let positions = symbols
            .iter()
            .map(|sym| {
                let capital = initial_capital_per_symbol
                    .to_string()
                    .parse::<f32>()
                    .unwrap_or(100_000.0);
                let tracker = PortfolioTracker::new(
                    capital,
                    0.0001,
                    cash_reserve_percent,
                );
                (sym.clone(), tracker)
            })
            .collect();

        let correlation_matrix = Array2::eye(num_symbols);
        let opportunity_scores = symbols.iter().map(|sym| (sym.clone(), 0.0)).collect();

        Self {
            positions,
            correlation_matrix,
            symbols,
            opportunity_scores,
            portfolio_history: Vec::new(),
            trade_pnls: Vec::new(),
            max_positions: HashMap::new(),
        }
    }

    /// Get number of symbols tracked
    pub fn num_symbols(&self) -> usize {
        self.symbols.len()
    }

    /// Get list of symbols
    pub const fn symbols(&self) -> &Vec<Symbol> {
        &self.symbols
    }

    /// Get portfolio tracker for a specific symbol
    pub fn get_position(&self, symbol: &Symbol) -> Option<&PortfolioTracker> {
        self.positions.get(symbol)
    }

    /// Get mutable portfolio tracker for a specific symbol
    fn get_position_mut(&mut self, symbol: &Symbol) -> Option<&mut PortfolioTracker> {
        self.positions.get_mut(symbol)
    }

    /// Execute trading action on a specific symbol
    ///
    /// # Arguments
    ///
    /// * `symbol` - Symbol to trade
    /// * `action` - Factored trading action (exposure, order type, urgency)
    /// * `price` - Current market price
    /// * `max_position` - Maximum position size for this trade
    pub fn execute_action(
        &mut self,
        symbol: &Symbol,
        action: FactoredAction,
        price: f32,
        max_position: f32,
    ) {
        if let Some(tracker) = self.get_position_mut(symbol) {
            tracker.execute_action(action, price, max_position);
            debug!(
                "Executed {:?} on {} at price {:.2}, position: {:.2}",
                action,
                symbol.as_str(),
                price,
                tracker.current_position()
            );
        }
    }

    /// Calculate total portfolio value across all symbols
    ///
    /// # Arguments
    ///
    /// * `prices` - Current market prices for all symbols
    ///
    /// # Returns
    ///
    /// Total portfolio value (sum of all symbol portfolios)
    pub fn total_portfolio_value(&self, prices: &HashMap<Symbol, f32>) -> f64 {
        self.positions
            .iter()
            .map(|(sym, tracker)| {
                let price = prices.get(sym).copied().unwrap_or(0.0);
                tracker.total_value(price) as f64
            })
            .sum()
    }

    /// Get correlation matrix
    pub const fn correlation_matrix(&self) -> &Array2<f64> {
        &self.correlation_matrix
    }

    /// Set correlation matrix
    ///
    /// # Arguments
    ///
    /// * `matrix` - Correlation matrix (must be N x N where N = number of symbols)
    ///
    /// # Panics
    ///
    /// Panics if matrix dimensions don't match number of symbols
    pub fn set_correlation_matrix(&mut self, matrix: Array2<f64>) {
        assert_eq!(
            matrix.shape(),
            &[self.symbols.len(), self.symbols.len()],
            "Correlation matrix dimensions must match number of symbols"
        );
        self.correlation_matrix = matrix;
    }

    /// Calculate portfolio Value-at-Risk (`VaR`) using correlation matrix
    ///
    /// # Formula
    ///
    /// `σ_p` = √(w' Σ w)
    ///
    /// Where:
    /// - w = position vector (`position_i` * `price_i` for each symbol)
    /// - Σ = correlation matrix (simplified from covariance matrix)
    /// - `σ_p` = portfolio volatility (`VaR` proxy)
    ///
    /// # Arguments
    ///
    /// * `prices` - Current market prices for all symbols
    ///
    /// # Returns
    ///
    /// Portfolio `VaR` (standard deviation of portfolio value)
    pub fn calculate_portfolio_var(&self, prices: &HashMap<Symbol, f32>) -> f64 {
        // Build position vector (position * price for each symbol)
        let positions: Vec<f64> = self
            .symbols
            .iter()
            .map(|sym| {
                let tracker = &self.positions[sym];
                let price = prices.get(sym).copied().unwrap_or(0.0);
                tracker.current_position() as f64 * price as f64
            })
            .collect();

        // Calculate portfolio variance: σ_p^2 = Σ_i Σ_j (w_i * w_j * ρ_ij)
        let mut variance = 0.0;
        for i in 0..self.symbols.len() {
            for j in 0..self.symbols.len() {
                variance += positions[i] * positions[j] * self.correlation_matrix[[i, j]];
            }
        }

        // Return standard deviation (VaR proxy)
        variance.abs().sqrt()
    }

    /// Set opportunity scores for symbol selection
    ///
    /// # Arguments
    ///
    /// * `scores` - Opportunity scores per symbol (higher = more attractive)
    pub fn set_opportunity_scores(&mut self, scores: HashMap<Symbol, f64>) {
        self.opportunity_scores = scores;
    }

    /// Select active symbol based on opportunity scores
    ///
    /// # Returns
    ///
    /// Symbol with highest opportunity score
    pub fn select_active_symbol(&self) -> Symbol {
        self.opportunity_scores
            .iter()
            .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
            .map(|(sym, _)| sym.clone())
            .unwrap_or_else(|| self.symbols[0].clone())
    }

    /// Build state vector for DQN input
    ///
    /// # Format
    ///
    /// [`market_features_128`, `portfolio_features_sym1_3`, `portfolio_features_sym2_3`, ...]
    ///
    /// For N symbols: 128 + 3*N features
    ///
    /// # Arguments
    ///
    /// * `market_features` - 128 market features (OHLCV, indicators, regime detection)
    /// * `prices` - Current market prices for all symbols
    ///
    /// # Returns
    ///
    /// State vector of size 128 + 3*N
    pub fn build_state_vector(
        &self,
        market_features: &[f64],
        prices: &HashMap<Symbol, f32>,
    ) -> Vec<f64> {
        let mut state = Vec::with_capacity(128 + 3 * self.symbols.len());

        // First 128 are market features
        state.extend_from_slice(market_features);

        // Next 3*N are portfolio features (3 per symbol)
        for symbol in &self.symbols {
            let tracker = &self.positions[symbol];
            let price = prices.get(symbol).copied().unwrap_or(0.0);
            let features = tracker.get_portfolio_features(price);
            state.extend_from_slice(&features.map(|f| f as f64));
        }

        state
    }

    /// Reset all portfolio trackers to initial state
    pub fn reset_all(&mut self) {
        for tracker in self.positions.values_mut() {
            tracker.reset();
        }
        self.portfolio_history.clear();
        self.trade_pnls.clear();
    }

    /// Record portfolio value snapshot for analytics
    ///
    /// # Arguments
    ///
    /// * `prices` - Current market prices
    pub fn record_portfolio_value(&mut self, prices: &HashMap<Symbol, f32>) {
        let total_value = self.total_portfolio_value(prices);
        self.portfolio_history.push(PortfolioSnapshot {
            total_value,
        });
    }

    /// Calculate portfolio Sharpe ratio
    ///
    /// # Formula
    ///
    /// Sharpe = (`mean_return` - `risk_free_rate`) / `std_dev(returns)`
    ///
    /// # Arguments
    ///
    /// * `risk_free_rate` - Risk-free rate (e.g., 0.02 for 2% annual)
    ///
    /// # Returns
    ///
    /// Sharpe ratio (higher is better)
    pub fn calculate_sharpe_ratio(&self, risk_free_rate: f64) -> f64 {
        if self.portfolio_history.len() < 2 {
            return 0.0;
        }

        // Calculate returns
        let returns: Vec<f64> = self
            .portfolio_history
            .windows(2)
            .map(|w| (w[1].total_value - w[0].total_value) / w[0].total_value)
            .collect();

        // Mean return
        let mean_return = returns.iter().sum::<f64>() / returns.len() as f64;

        // Standard deviation
        let variance = returns
            .iter()
            .map(|r| (r - mean_return).powi(2))
            .sum::<f64>()
            / returns.len() as f64;
        let std_dev = variance.sqrt();

        if std_dev == 0.0 {
            return 0.0;
        }

        (mean_return - risk_free_rate) / std_dev
    }

    /// Calculate maximum drawdown
    ///
    /// # Formula
    ///
    /// Max Drawdown = (Peak - Trough) / Peak
    ///
    /// # Returns
    ///
    /// Maximum drawdown percentage (0.0 to 1.0)
    pub fn calculate_max_drawdown(&self) -> f64 {
        if self.portfolio_history.is_empty() {
            return 0.0;
        }

        let mut peak = self.portfolio_history[0].total_value;
        let mut max_drawdown = 0.0;

        for snapshot in &self.portfolio_history {
            if snapshot.total_value > peak {
                peak = snapshot.total_value;
            }
            let drawdown = (peak - snapshot.total_value) / peak;
            if drawdown > max_drawdown {
                max_drawdown = drawdown;
            }
        }

        max_drawdown
    }

    /// Record trade P&L for win rate calculation
    ///
    /// # Arguments
    ///
    /// * `pnl` - Trade profit/loss
    pub fn record_trade_pnl(&mut self, pnl: f64) {
        self.trade_pnls.push(pnl);
    }

    /// Calculate win rate
    ///
    /// # Returns
    ///
    /// Win rate (0.0 to 1.0, where 1.0 = 100% wins)
    pub fn calculate_win_rate(&self) -> f64 {
        if self.trade_pnls.is_empty() {
            return 0.0;
        }

        let wins = self.trade_pnls.iter().filter(|&&pnl| pnl > 0.0).count();
        wins as f64 / self.trade_pnls.len() as f64
    }

    /// Set maximum position size for a symbol
    pub fn set_max_position(&mut self, symbol: &Symbol, max_position: f32) {
        self.max_positions.insert(symbol.clone(), max_position);
    }

    /// Get total transaction costs across all symbols
    pub fn total_transaction_costs(&self) -> f32 {
        self.positions
            .values()
            .map(|tracker| tracker.transaction_costs())
            .sum()
    }
}

#[cfg(test)]
#[allow(
    clippy::redundant_clone,
    clippy::needless_range_loop
)]
mod tests {
    use super::*;
    use crate::action_space::{ExposureLevel, OrderType, Urgency};

    #[test]
    fn test_multi_asset_initialization() {
        let symbols = vec![Symbol::new("ES_FUT"), Symbol::new("NQ_FUT")];
        let tracker = MultiAssetPortfolioTracker::new(symbols.clone(), Decimal::from(10_000));

        assert_eq!(tracker.num_symbols(), 2);
        assert_eq!(tracker.symbols(), &symbols);

        // Check initial capital
        for symbol in &symbols {
            let position = tracker.get_position(symbol).unwrap();
            assert_eq!(position.cash_balance(), 10_000.0);
            assert_eq!(position.current_position(), 0.0);
        }
    }

    #[test]
    fn test_correlation_matrix_identity() {
        let symbols = vec![Symbol::new("ES_FUT"), Symbol::new("NQ_FUT")];
        let tracker = MultiAssetPortfolioTracker::new(symbols, Decimal::from(10_000));

        let corr = tracker.correlation_matrix();
        assert_eq!(corr[[0, 0]], 1.0);
        assert_eq!(corr[[1, 1]], 1.0);
        assert_eq!(corr[[0, 1]], 0.0);
        assert_eq!(corr[[1, 0]], 0.0);
    }

    #[test]
    fn test_total_portfolio_value() {
        let symbols = vec![Symbol::new("ES_FUT"), Symbol::new("NQ_FUT")];
        let mut tracker = MultiAssetPortfolioTracker::new(symbols.clone(), Decimal::from(10_000));

        // ES: Long 10 at 4500
        let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal);
        tracker.execute_action(&Symbol::new("ES_FUT"), action, 4500.0, 10.0);

        let prices = HashMap::from([
            (Symbol::new("ES_FUT"), 4600.0),
            (Symbol::new("NQ_FUT"), 0.0),
        ]);

        let total_value = tracker.total_portfolio_value(&prices);

        // ES: 10_000 - (10*4500) + (10*4600) = 11_000
        // NQ: 10_000
        // Total: 21_000 (approximately, minus transaction costs)
        assert!(total_value > 20_900.0);
        assert!(total_value < 21_100.0);
    }

    #[test]
    fn test_build_state_vector() {
        let symbols = vec![Symbol::new("ES_FUT"), Symbol::new("NQ_FUT")];
        let tracker = MultiAssetPortfolioTracker::new(symbols, Decimal::from(10_000));

        let market_features = vec![0.0; 128];
        let prices = HashMap::from([
            (Symbol::new("ES_FUT"), 4500.0),
            (Symbol::new("NQ_FUT"), 15000.0),
        ]);

        let state = tracker.build_state_vector(&market_features, &prices);

        // 128 market + 2*3 portfolio = 134 features
        assert_eq!(state.len(), 134);

        // First 128 are zeros
        for i in 0..128 {
            assert_eq!(state[i], 0.0);
        }

        // Portfolio features start at index 128
        // Centered at 0.0: portfolio_value / initial_capital - 1.0 = 0.0 at start
        assert_eq!(state[128], 0.0); // ES normalized value (centered)
    }
}
