//! Portfolio runner: combines multiple strategies into one portfolio.
//!
//! Each strategy gets a capital allocation and runs independently.
//! The runner tracks per-strategy and combined equity, applies risk
//! rules post-hoc, and aggregates results.

use std::collections::HashMap;
use std::sync::Arc;

use arrow::array::{ArrayRef, Float64Array};
use arrow::compute::concat_batches;
use bt_data::ParquetDataStore;
use bt_kernel::{BtError, Result};
use rayon::prelude::*;

use crate::orchestrator::{BacktestConfig, BacktestResult, SimpleBacktestRunner};
use crate::portfolio_model::{
    PerStrategyResult, PortfolioDef, PortfolioResult, RiskEvent, RiskRule,
};

/// Run a portfolio backtest: multiple strategies, partitioned capital.
///
/// Each strategy runs independently with its allocated fraction of capital.
/// Results are combined into a single portfolio view with per-strategy tracking.
pub fn run_portfolio(
    portfolio: &PortfolioDef,
    config: &BacktestConfig,
    data_store: &Arc<ParquetDataStore>,
) -> Result<PortfolioResult> {
    let t_start = std::time::Instant::now();

    // Validate weights sum to <= 1.0
    let total_weight: f64 = portfolio.strategies.iter().map(|s| s.weight).sum();
    if total_weight > 1.0 + 1e-9 {
        return Err(BtError::Strategy(format!(
            "strategy weights sum to {total_weight:.4}, must be <= 1.0"
        )));
    }

    let n_strategies = portfolio.strategies.len();
    if n_strategies == 0 {
        return Err(BtError::Strategy(
            "portfolio must have at least one strategy".to_string(),
        ));
    }

    // Load and align data once (shared across all strategies).
    let (aligned, universe_sorted, version, _, _) =
        SimpleBacktestRunner::load_and_align(config, data_store.as_ref())?;

    tracing::info!(
        strategies = n_strategies,
        universe = universe_sorted.len(),
        "portfolio: data loaded, running strategies"
    );

    // Run each strategy with its capital allocation (parallel).
    let strategy_results: Vec<(String, Result<BacktestResult>)> = portfolio
        .strategies
        .par_iter()
        .map(|slot| {
            let mut strategy_config = config.clone();
            strategy_config.initial_capital = config.initial_capital * slot.weight;

            let strategy_runner = SimpleBacktestRunner::new();
            let result = strategy_runner.run_on_aligned(
                &slot.plan,
                &strategy_config,
                &aligned,
                &universe_sorted,
                &version,
            );

            (slot.name.clone(), result)
        })
        .collect();

    // Collect results and check for errors.
    let mut per_strategy: HashMap<String, PerStrategyResult> = HashMap::new();
    let mut successful_results: Vec<(String, BacktestResult)> = Vec::new();
    let mut risk_events: Vec<RiskEvent> = Vec::new();

    for (name, result) in strategy_results {
        let bt_result =
            result.map_err(|e| BtError::Strategy(format!("strategy '{name}' failed: {e}")))?;

        // Check per-strategy kill switch.
        let killed = check_strategy_kill_switch(&name, &bt_result, &portfolio.risk_rules);
        if killed {
            risk_events.push(RiskEvent {
                bar: 0,
                timestamp_ns: 0,
                rule: format!("StrategyKillSwitch({})", name),
                action: format!("strategy '{}' exceeded loss threshold", name),
            });
        }

        let trade_count = bt_result.trades.num_rows();
        let equity_vec = bt_result
            .equity_curve
            .as_any()
            .downcast_ref::<Float64Array>()
            .map(|a| a.values().to_vec())
            .unwrap_or_default();
        let final_cap = equity_vec.last().copied().unwrap_or(0.0);

        per_strategy.insert(
            name.clone(),
            PerStrategyResult {
                name: name.clone(),
                equity_curve: equity_vec,
                trade_count,
                final_capital: final_cap,
                killed,
            },
        );

        successful_results.push((name, bt_result));
    }

    // Build combined result.
    let combined = build_combined_result(&successful_results, config)?;

    // Check portfolio-level risk rules.
    check_portfolio_risk_rules(&combined, &portfolio.risk_rules, &mut risk_events);

    tracing::info!(
        elapsed_ms = t_start.elapsed().as_millis() as u64,
        strategies = n_strategies,
        combined_trades = combined.trades.num_rows(),
        "portfolio: complete"
    );

    Ok(PortfolioResult {
        combined,
        per_strategy,
        rebalance_events: Vec::new(),
        risk_events,
    })
}

/// Build a combined BacktestResult by summing equity curves and concatenating trades.
fn build_combined_result(
    results: &[(String, BacktestResult)],
    config: &BacktestConfig,
) -> Result<BacktestResult> {
    if results.is_empty() {
        return Err(BtError::Strategy(
            "no strategy results to combine".to_string(),
        ));
    }

    let first = &results[0].1;

    // Sum equity curves across strategies.
    let n_bars = first
        .equity_curve
        .as_any()
        .downcast_ref::<Float64Array>()
        .map(|a| a.len())
        .unwrap_or(0);

    let mut combined_equity = vec![0.0f64; n_bars];
    for (_, result) in results {
        if let Some(eq) = result.equity_curve.as_any().downcast_ref::<Float64Array>() {
            for i in 0..n_bars.min(eq.len()) {
                combined_equity[i] += eq.value(i);
            }
        }
    }

    let equity_array: ArrayRef = Arc::new(Float64Array::from(combined_equity.clone()));

    // Concatenate trades from all strategies.
    let schema = first.trades.schema();
    let batches: Vec<_> = results.iter().map(|r| &r.1.trades).collect();
    let combined_trades = concat_batches(&schema, batches)
        .map_err(|e| BtError::Data(format!("failed to concatenate trade batches: {e}")))?;

    // Extract timestamps from first result's positions.
    let timestamps_ns: Vec<i64> = first
        .positions
        .column_by_name("timestamp")
        .and_then(|c| {
            c.as_any()
                .downcast_ref::<arrow::array::TimestampNanosecondArray>()
        })
        .map(|a| {
            // Deduplicate timestamps (positions has one row per symbol per bar).
            let mut seen = std::collections::HashSet::new();
            a.values()
                .iter()
                .filter(|&&v| seen.insert(v))
                .copied()
                .collect()
        })
        .unwrap_or_default();

    // Recompute daily returns on combined equity.
    let daily_returns = bt_analytics::compute_daily_returns(&combined_equity, &timestamps_ns);
    let daily_returns_array: ArrayRef = Arc::new(Float64Array::from(daily_returns));

    // Recompute metrics on combined equity.
    let bar_seconds = config.bar_interval.as_seconds();
    let metrics = bt_analytics::compute_performance_metrics_with_rf(
        &combined_equity,
        &timestamps_ns,
        bar_seconds,
        config.trading_days_per_year,
        config.risk_free_rate,
    )?;

    // Collect all warnings.
    let mut warnings: Vec<String> = Vec::new();
    for (name, result) in results {
        for w in &result.warnings {
            warnings.push(format!("[{name}] {w}"));
        }
    }

    // Use first result's manifest as template, rename to "portfolio".
    let mut manifest = first.manifest.clone();
    manifest.strategy_name = "portfolio".to_string();

    Ok(BacktestResult {
        manifest,
        equity_curve: equity_array,
        positions: first.positions.clone(), // simplified — uses first strategy's positions
        trades: combined_trades,
        daily_returns: daily_returns_array,
        metrics,
        warnings,
        profile: Default::default(),
    })
}

/// Check if a strategy should be killed based on risk rules.
fn check_strategy_kill_switch(name: &str, result: &BacktestResult, rules: &[RiskRule]) -> bool {
    for rule in rules {
        if let RiskRule::StrategyKillSwitch {
            strategy,
            max_loss_pct,
        } = rule
        {
            if strategy == name && result.metrics.total_return < -max_loss_pct / 100.0 {
                return true;
            }
        }
    }
    false
}

/// Check portfolio-level risk rules and record events.
fn check_portfolio_risk_rules(
    combined: &BacktestResult,
    rules: &[RiskRule],
    events: &mut Vec<RiskEvent>,
) {
    for rule in rules {
        if let RiskRule::MaxDrawdown { threshold_pct } = rule {
            if combined.metrics.max_drawdown.abs() > threshold_pct / 100.0 {
                events.push(RiskEvent {
                    bar: 0,
                    timestamp_ns: 0,
                    rule: format!("MaxDrawdown({threshold_pct}%)"),
                    action: format!(
                        "portfolio drawdown {:.2}% exceeds threshold {threshold_pct}%",
                        combined.metrics.max_drawdown * 100.0
                    ),
                });
            }
        }
    }
}
