using System;
using System.Collections.Generic;
using System.Linq;
using NT8.Core.Logging;

namespace NT8.Core.Analytics
{
    /// <summary>
    /// Calculates aggregate performance metrics for trade sets.
    /// </summary>
    public class PerformanceCalculator
    {
        private readonly ILogger _logger;

        /// <summary>
        /// Initializes a new calculator instance.
        /// </summary>
        /// <param name="logger">Logger dependency.</param>
        public PerformanceCalculator(ILogger logger)
        {
            if (logger == null)
                throw new ArgumentNullException("logger");

            _logger = logger;
        }

        /// <summary>
        /// Calculates all core metrics from trades.
        /// </summary>
        /// <param name="trades">Trade records.</param>
        /// <returns>Performance metrics snapshot.</returns>
        public PerformanceMetrics Calculate(List<TradeRecord> trades)
        {
            if (trades == null)
                throw new ArgumentNullException("trades");

            try
            {
                var metrics = new PerformanceMetrics();
                metrics.TotalTrades = trades.Count;
                metrics.Wins = trades.Count(t => t.RealizedPnL > 0.0);
                metrics.Losses = trades.Count(t => t.RealizedPnL < 0.0);
                metrics.WinRate = CalculateWinRate(trades);
                metrics.LossRate = metrics.TotalTrades > 0 ? (double)metrics.Losses / metrics.TotalTrades : 0.0;

                metrics.GrossProfit = trades.Where(t => t.RealizedPnL > 0.0).Sum(t => t.RealizedPnL);
                metrics.GrossLoss = Math.Abs(trades.Where(t => t.RealizedPnL < 0.0).Sum(t => t.RealizedPnL));
                metrics.NetProfit = metrics.GrossProfit - metrics.GrossLoss;

                metrics.AverageWin = metrics.Wins > 0
                    ? trades.Where(t => t.RealizedPnL > 0.0).Average(t => t.RealizedPnL)
                    : 0.0;
                metrics.AverageLoss = metrics.Losses > 0
                    ? Math.Abs(trades.Where(t => t.RealizedPnL < 0.0).Average(t => t.RealizedPnL))
                    : 0.0;

                metrics.ProfitFactor = CalculateProfitFactor(trades);
                metrics.Expectancy = CalculateExpectancy(trades);
                metrics.SharpeRatio = CalculateSharpeRatio(trades, 0.0);
                metrics.SortinoRatio = CalculateSortinoRatio(trades, 0.0);
                metrics.MaxDrawdownPercent = CalculateMaxDrawdown(trades);
                metrics.RecoveryFactor = metrics.MaxDrawdownPercent > 0.0
                    ? metrics.NetProfit / metrics.MaxDrawdownPercent
                    : 0.0;

                return metrics;
            }
            catch (Exception ex)
            {
                _logger.LogError("Calculate performance metrics failed: {0}", ex.Message);
                throw;
            }
        }

        /// <summary>
        /// Calculates win rate.
        /// </summary>
        /// <param name="trades">Trade records.</param>
        /// <returns>Win rate in range [0,1].</returns>
        public double CalculateWinRate(List<TradeRecord> trades)
        {
            if (trades == null)
                throw new ArgumentNullException("trades");

            try
            {
                if (trades.Count == 0)
                    return 0.0;

                var wins = trades.Count(t => t.RealizedPnL > 0.0);
                return (double)wins / trades.Count;
            }
            catch (Exception ex)
            {
                _logger.LogError("CalculateWinRate failed: {0}", ex.Message);
                throw;
            }
        }

        /// <summary>
        /// Calculates profit factor.
        /// </summary>
        /// <param name="trades">Trade records.</param>
        /// <returns>Profit factor ratio.</returns>
        public double CalculateProfitFactor(List<TradeRecord> trades)
        {
            if (trades == null)
                throw new ArgumentNullException("trades");

            try
            {
                var grossProfit = trades.Where(t => t.RealizedPnL > 0.0).Sum(t => t.RealizedPnL);
                var grossLoss = Math.Abs(trades.Where(t => t.RealizedPnL < 0.0).Sum(t => t.RealizedPnL));
                if (grossLoss <= 0.0)
                    return grossProfit > 0.0 ? double.PositiveInfinity : 0.0;

                return grossProfit / grossLoss;
            }
            catch (Exception ex)
            {
                _logger.LogError("CalculateProfitFactor failed: {0}", ex.Message);
                throw;
            }
        }

        /// <summary>
        /// Calculates expectancy per trade.
        /// </summary>
        /// <param name="trades">Trade records.</param>
        /// <returns>Expectancy value.</returns>
        public double CalculateExpectancy(List<TradeRecord> trades)
        {
            if (trades == null)
                throw new ArgumentNullException("trades");

            try
            {
                if (trades.Count == 0)
                    return 0.0;

                var wins = trades.Where(t => t.RealizedPnL > 0.0).ToList();
                var losses = trades.Where(t => t.RealizedPnL < 0.0).ToList();

                var winRate = (double)wins.Count / trades.Count;
                var lossRate = (double)losses.Count / trades.Count;
                var avgWin = wins.Count > 0 ? wins.Average(t => t.RealizedPnL) : 0.0;
                var avgLoss = losses.Count > 0 ? Math.Abs(losses.Average(t => t.RealizedPnL)) : 0.0;

                return (winRate * avgWin) - (lossRate * avgLoss);
            }
            catch (Exception ex)
            {
                _logger.LogError("CalculateExpectancy failed: {0}", ex.Message);
                throw;
            }
        }

        /// <summary>
        /// Calculates Sharpe ratio.
        /// </summary>
        /// <param name="trades">Trade records.</param>
        /// <param name="riskFreeRate">Risk free return per trade period.</param>
        /// <returns>Sharpe ratio value.</returns>
        public double CalculateSharpeRatio(List<TradeRecord> trades, double riskFreeRate)
        {
            if (trades == null)
                throw new ArgumentNullException("trades");

            try
            {
                if (trades.Count < 2)
                    return 0.0;

                var returns = trades.Select(t => t.RealizedPnL).ToList();
                var mean = returns.Average();
                var variance = returns.Sum(r => (r - mean) * (r - mean)) / (returns.Count - 1);
                var stdDev = Math.Sqrt(variance);
                if (stdDev <= 0.0)
                    return 0.0;

                return (mean - riskFreeRate) / stdDev;
            }
            catch (Exception ex)
            {
                _logger.LogError("CalculateSharpeRatio failed: {0}", ex.Message);
                throw;
            }
        }

        /// <summary>
        /// Calculates Sortino ratio.
        /// </summary>
        /// <param name="trades">Trade records.</param>
        /// <param name="riskFreeRate">Risk free return per trade period.</param>
        /// <returns>Sortino ratio value.</returns>
        public double CalculateSortinoRatio(List<TradeRecord> trades, double riskFreeRate)
        {
            if (trades == null)
                throw new ArgumentNullException("trades");

            try
            {
                if (trades.Count < 2)
                    return 0.0;

                var returns = trades.Select(t => t.RealizedPnL).ToList();
                var mean = returns.Average();
                var downside = returns.Where(r => r < riskFreeRate).ToList();
                if (downside.Count == 0)
                    return 0.0;

                var downsideVariance = downside.Sum(r => (r - riskFreeRate) * (r - riskFreeRate)) / downside.Count;
                var downsideDev = Math.Sqrt(downsideVariance);
                if (downsideDev <= 0.0)
                    return 0.0;

                return (mean - riskFreeRate) / downsideDev;
            }
            catch (Exception ex)
            {
                _logger.LogError("CalculateSortinoRatio failed: {0}", ex.Message);
                throw;
            }
        }

        /// <summary>
        /// Calculates maximum drawdown percent from cumulative realized PnL.
        /// </summary>
        /// <param name="trades">Trade records.</param>
        /// <returns>Max drawdown in percent points.</returns>
        public double CalculateMaxDrawdown(List<TradeRecord> trades)
        {
            if (trades == null)
                throw new ArgumentNullException("trades");

            try
            {
                if (trades.Count == 0)
                    return 0.0;

                var ordered = trades.OrderBy(t => t.ExitTime.HasValue ? t.ExitTime.Value : t.EntryTime).ToList();
                var equity = 0.0;
                var peak = 0.0;
                var maxDrawdown = 0.0;

                foreach (var trade in ordered)
                {
                    equity += trade.RealizedPnL;
                    if (equity > peak)
                        peak = equity;

                    var drawdown = peak - equity;
                    if (drawdown > maxDrawdown)
                        maxDrawdown = drawdown;
                }

                if (peak <= 0.0)
                    return maxDrawdown;

                return (maxDrawdown / peak) * 100.0;
            }
            catch (Exception ex)
            {
                _logger.LogError("CalculateMaxDrawdown failed: {0}", ex.Message);
                throw;
            }
        }
    }
}
