use std::collections::BTreeMap;

use chrono::{Datelike, NaiveDate};
use serde::{Deserialize, Serialize};

use crate::engine::DailyEquityPoint;
use crate::events::FillEvent;
use crate::portfolio::HoldingSummary;

const TRADING_DAYS_PER_YEAR: f64 = 252.0;
const MONTHS_PER_YEAR: f64 = 12.0;
const DEFAULT_RISK_FREE_RATE: f64 = 0.022;

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct BacktestMetrics {
    pub total_return: f64,
    pub annual_return: f64,
    pub sharpe: f64,
    pub max_drawdown: f64,
    pub win_rate: f64,
    pub alpha: f64,
    pub beta: f64,
    pub benchmark_cumulative_return: f64,
    pub benchmark_net_value: f64,
    pub risk_free_rate: f64,
    pub monthly_excess_win_rate: f64,
    pub excess_cumulative_return: f64,
    pub excess_annual_return: f64,
    pub max_drawdown_duration_days: usize,
    pub total_trade_days: usize,
    pub sortino: f64,
    pub information_ratio: f64,
    pub tracking_error: f64,
    pub volatility: f64,
    pub excess_return: f64,
    pub excess_sharpe: f64,
    pub excess_volatility: f64,
    pub excess_max_drawdown: f64,
    pub holding_count: usize,
    pub average_weight: f64,
    pub max_weight: f64,
    pub concentration: f64,
    pub weight_std_dev: f64,
    pub median_weight: f64,
    pub average_daily_turnover: f64,
    pub total_assets: f64,
    pub cash_balance: f64,
    pub unit_nav: f64,
    pub initial_cash: f64,
    pub excess_win_rate: f64,
    pub monthly_sharpe: f64,
    pub monthly_volatility: f64,
}

pub fn compute_backtest_metrics(
    equity_curve: &[DailyEquityPoint],
    fills: &[FillEvent],
    daily_holdings: &[HoldingSummary],
    initial_cash: f64,
) -> BacktestMetrics {
    let Some(first_point) = equity_curve.first() else {
        return BacktestMetrics {
            risk_free_rate: DEFAULT_RISK_FREE_RATE,
            initial_cash,
            ..BacktestMetrics::default()
        };
    };
    let Some(last_point) = equity_curve.last() else {
        return BacktestMetrics {
            risk_free_rate: DEFAULT_RISK_FREE_RATE,
            initial_cash,
            ..BacktestMetrics::default()
        };
    };

    let trade_days = equity_curve.len();
    let returns = equity_curve
        .windows(2)
        .map(|window| pct_change(window[0].total_equity, window[1].total_equity))
        .collect::<Vec<_>>();
    let benchmark_returns = equity_curve
        .windows(2)
        .map(|window| pct_change(window[0].benchmark_close, window[1].benchmark_close))
        .collect::<Vec<_>>();
    let excess_returns = returns
        .iter()
        .zip(benchmark_returns.iter())
        .map(|(lhs, rhs)| lhs - rhs)
        .collect::<Vec<_>>();

    let benchmark_net_value = if first_point.benchmark_close.abs() < f64::EPSILON {
        1.0
    } else {
        last_point.benchmark_close / first_point.benchmark_close
    };
    let benchmark_cumulative_return = benchmark_net_value - 1.0;
    let total_return = if initial_cash.abs() < f64::EPSILON {
        0.0
    } else {
        (last_point.total_equity / initial_cash) - 1.0
    };
    let excess_cumulative_return = if benchmark_net_value.abs() < f64::EPSILON {
        total_return
    } else {
        (last_point.total_equity / initial_cash) / benchmark_net_value - 1.0
    };
    let excess_return = total_return - benchmark_cumulative_return;
    let annual_return = annualize_return(total_return, trade_days);
    let excess_annual_return = annualize_return(excess_cumulative_return, trade_days);

    let risk_free_rate = DEFAULT_RISK_FREE_RATE;
    let daily_rf = risk_free_rate / TRADING_DAYS_PER_YEAR;
    let sharpe = annualized_sharpe(&returns, daily_rf, TRADING_DAYS_PER_YEAR);
    let sortino = annualized_sortino(&returns, daily_rf, TRADING_DAYS_PER_YEAR);
    let information_ratio = annualized_sharpe(&excess_returns, 0.0, TRADING_DAYS_PER_YEAR);
    let tracking_error = annualized_std(&excess_returns, TRADING_DAYS_PER_YEAR);
    let volatility = annualized_std(&returns, TRADING_DAYS_PER_YEAR);
    let excess_volatility = annualized_std(&excess_returns, TRADING_DAYS_PER_YEAR);
    let excess_sharpe = annualized_sharpe(&excess_returns, 0.0, TRADING_DAYS_PER_YEAR);
    let (alpha, beta) = alpha_beta(&returns, &benchmark_returns, daily_rf);

    let equity_nav = equity_curve
        .iter()
        .map(|point| safe_div(point.total_equity, initial_cash, 1.0))
        .collect::<Vec<_>>();
    let benchmark_nav_series = equity_curve
        .iter()
        .map(|point| safe_div(point.benchmark_close, first_point.benchmark_close, 1.0))
        .collect::<Vec<_>>();
    let excess_nav_series = equity_nav
        .iter()
        .zip(benchmark_nav_series.iter())
        .map(|(lhs, rhs)| safe_div(*lhs, *rhs, *lhs))
        .collect::<Vec<_>>();

    let (max_drawdown, max_drawdown_duration_days) = drawdown_stats(&equity_nav);
    let (excess_max_drawdown, _) = drawdown_stats(&excess_nav_series);

    let winning_days = returns.iter().filter(|value| **value > 0.0).count();
    let excess_winning_days = excess_returns.iter().filter(|value| **value > 0.0).count();
    let win_rate = ratio(winning_days, returns.len());
    let excess_win_rate = ratio(excess_winning_days, excess_returns.len());

    let monthly_portfolio_returns = group_monthly_returns(equity_curve, |point| point.total_equity);
    let monthly_benchmark_returns =
        group_monthly_returns(equity_curve, |point| point.benchmark_close);
    let monthly_excess_returns = monthly_portfolio_returns
        .iter()
        .zip(monthly_benchmark_returns.iter())
        .map(|(lhs, rhs)| lhs - rhs)
        .collect::<Vec<_>>();
    let monthly_excess_win_rate = ratio(
        monthly_excess_returns
            .iter()
            .filter(|value| **value > 0.0)
            .count(),
        monthly_excess_returns.len(),
    );
    let monthly_sharpe = annualized_sharpe(
        &monthly_portfolio_returns,
        risk_free_rate / MONTHS_PER_YEAR,
        MONTHS_PER_YEAR,
    );
    let monthly_volatility = annualized_std(&monthly_portfolio_returns, MONTHS_PER_YEAR);

    let turnover_by_date = fills
        .iter()
        .fold(BTreeMap::<NaiveDate, f64>::new(), |mut acc, fill| {
            *acc.entry(fill.date).or_default() += fill.gross_amount.abs();
            acc
        });
    let equity_by_date = equity_curve
        .iter()
        .map(|point| (point.date, point.total_equity))
        .collect::<BTreeMap<_, _>>();
    let average_daily_turnover = if equity_curve.is_empty() {
        0.0
    } else {
        equity_curve
            .iter()
            .map(|point| {
                let traded = turnover_by_date
                    .get(&point.date)
                    .copied()
                    .unwrap_or_default();
                safe_div(traded, point.total_equity.max(initial_cash * 0.5), 0.0)
            })
            .sum::<f64>()
            / equity_curve.len() as f64
    };

    let latest_date = last_point.date;
    let latest_holdings = daily_holdings
        .iter()
        .filter(|row| row.date == latest_date && row.quantity > 0)
        .collect::<Vec<_>>();
    let weights = latest_holdings
        .iter()
        .map(|holding| safe_div(holding.market_value, last_point.total_equity, 0.0))
        .collect::<Vec<_>>();
    let holding_count = latest_holdings.len();
    let average_weight = mean(&weights);
    let max_weight = weights
        .iter()
        .copied()
        .fold(0.0_f64, |acc, value| acc.max(value));
    let concentration = weights.iter().map(|weight| weight * weight).sum::<f64>();
    let weight_std_dev = std_dev(&weights);
    let median_weight = median(&weights);

    let total_trade_days = equity_by_date.len();

    BacktestMetrics {
        total_return,
        annual_return,
        sharpe,
        max_drawdown,
        win_rate,
        alpha,
        beta,
        benchmark_cumulative_return,
        benchmark_net_value,
        risk_free_rate,
        monthly_excess_win_rate,
        excess_cumulative_return,
        excess_annual_return,
        max_drawdown_duration_days,
        total_trade_days,
        sortino,
        information_ratio,
        tracking_error,
        volatility,
        excess_return,
        excess_sharpe,
        excess_volatility,
        excess_max_drawdown,
        holding_count,
        average_weight,
        max_weight,
        concentration,
        weight_std_dev,
        median_weight,
        average_daily_turnover,
        total_assets: last_point.total_equity,
        cash_balance: last_point.cash,
        unit_nav: safe_div(last_point.total_equity, initial_cash, 0.0),
        initial_cash,
        excess_win_rate,
        monthly_sharpe,
        monthly_volatility,
    }
}

fn pct_change(previous: f64, current: f64) -> f64 {
    if previous.abs() < f64::EPSILON {
        0.0
    } else {
        (current / previous) - 1.0
    }
}

fn annualize_return(total_return: f64, periods: usize) -> f64 {
    if periods == 0 {
        return 0.0;
    }
    let periods = periods as f64;
    let base = 1.0 + total_return;
    if base <= 0.0 {
        return -1.0;
    }
    base.powf(TRADING_DAYS_PER_YEAR / periods) - 1.0
}

fn annualized_sharpe(returns: &[f64], daily_rf: f64, periods_per_year: f64) -> f64 {
    if returns.len() < 2 {
        return 0.0;
    }
    let adjusted = returns
        .iter()
        .map(|value| value - daily_rf)
        .collect::<Vec<_>>();
    let mean_ret = mean(&adjusted);
    let std = std_dev(&adjusted);
    if std <= f64::EPSILON {
        0.0
    } else {
        mean_ret / std * periods_per_year.sqrt()
    }
}

fn annualized_sortino(returns: &[f64], daily_rf: f64, periods_per_year: f64) -> f64 {
    if returns.is_empty() {
        return 0.0;
    }
    let adjusted = returns
        .iter()
        .map(|value| value - daily_rf)
        .collect::<Vec<_>>();
    let downside = adjusted
        .iter()
        .filter(|value| **value < 0.0)
        .map(|value| value.powi(2))
        .collect::<Vec<_>>();
    if downside.is_empty() {
        return 0.0;
    }
    let downside_dev = (downside.iter().sum::<f64>() / downside.len() as f64).sqrt();
    if downside_dev <= f64::EPSILON {
        0.0
    } else {
        mean(&adjusted) / downside_dev * periods_per_year.sqrt()
    }
}

fn annualized_std(values: &[f64], periods_per_year: f64) -> f64 {
    std_dev(values) * periods_per_year.sqrt()
}

fn alpha_beta(returns: &[f64], benchmark_returns: &[f64], daily_rf: f64) -> (f64, f64) {
    if returns.len() < 2 || returns.len() != benchmark_returns.len() {
        return (0.0, 0.0);
    }
    let strategy_excess = returns
        .iter()
        .map(|value| value - daily_rf)
        .collect::<Vec<_>>();
    let benchmark_excess = benchmark_returns
        .iter()
        .map(|value| value - daily_rf)
        .collect::<Vec<_>>();
    let mean_strategy = mean(&strategy_excess);
    let mean_benchmark = mean(&benchmark_excess);
    let variance_benchmark = variance(&benchmark_excess);
    if variance_benchmark <= f64::EPSILON {
        return (0.0, 0.0);
    }
    let covariance = strategy_excess
        .iter()
        .zip(benchmark_excess.iter())
        .map(|(lhs, rhs)| (lhs - mean_strategy) * (rhs - mean_benchmark))
        .sum::<f64>()
        / (strategy_excess.len() - 1) as f64;
    let beta = covariance / variance_benchmark;
    let alpha = (mean_strategy - beta * mean_benchmark) * TRADING_DAYS_PER_YEAR;
    (alpha, beta)
}

fn drawdown_stats(nav: &[f64]) -> (f64, usize) {
    let mut peak = 0.0_f64;
    let mut max_drawdown = 0.0_f64;
    let mut duration = 0_usize;
    let mut max_duration = 0_usize;
    for value in nav {
        if *value >= peak {
            peak = *value;
            duration = 0;
            continue;
        }
        if peak > f64::EPSILON {
            let drawdown = (*value / peak) - 1.0;
            if drawdown < max_drawdown {
                max_drawdown = drawdown;
            }
        }
        duration += 1;
        if duration > max_duration {
            max_duration = duration;
        }
    }
    (max_drawdown, max_duration)
}

fn group_monthly_returns<F>(equity_curve: &[DailyEquityPoint], value_fn: F) -> Vec<f64>
where
    F: Fn(&DailyEquityPoint) -> f64,
{
    let mut month_last = BTreeMap::<(i32, u32), f64>::new();
    let mut month_first = BTreeMap::<(i32, u32), f64>::new();
    for point in equity_curve {
        let key = (point.date.year(), point.date.month());
        month_first.entry(key).or_insert_with(|| value_fn(point));
        month_last.insert(key, value_fn(point));
    }
    let mut keys = month_last.keys().copied().collect::<Vec<_>>();
    keys.sort_unstable();
    keys.into_iter()
        .filter_map(|key| {
            let first = month_first.get(&key).copied().unwrap_or_default();
            let last = month_last.get(&key).copied().unwrap_or_default();
            if first.abs() < f64::EPSILON {
                None
            } else {
                Some((last / first) - 1.0)
            }
        })
        .collect()
}

fn mean(values: &[f64]) -> f64 {
    if values.is_empty() {
        0.0
    } else {
        values.iter().sum::<f64>() / values.len() as f64
    }
}

fn variance(values: &[f64]) -> f64 {
    if values.len() < 2 {
        return 0.0;
    }
    let avg = mean(values);
    values
        .iter()
        .map(|value| (value - avg).powi(2))
        .sum::<f64>()
        / (values.len() - 1) as f64
}

fn std_dev(values: &[f64]) -> f64 {
    variance(values).sqrt()
}

fn median(values: &[f64]) -> f64 {
    if values.is_empty() {
        return 0.0;
    }
    let mut sorted = values.to_vec();
    sorted.sort_by(|lhs, rhs| lhs.partial_cmp(rhs).unwrap_or(std::cmp::Ordering::Equal));
    let mid = sorted.len() / 2;
    if sorted.len() % 2 == 0 {
        (sorted[mid - 1] + sorted[mid]) / 2.0
    } else {
        sorted[mid]
    }
}

fn ratio(numerator: usize, denominator: usize) -> f64 {
    if denominator == 0 {
        0.0
    } else {
        numerator as f64 / denominator as f64
    }
}

fn safe_div(numerator: f64, denominator: f64, fallback: f64) -> f64 {
    if denominator.abs() < f64::EPSILON {
        fallback
    } else {
        numerator / denominator
    }
}
