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

namespace NT8.Core.Analytics
{
    /// <summary>
    /// Analyzes drawdown behavior from trade history.
    /// </summary>
    public class DrawdownAnalyzer
    {
        private readonly ILogger _logger;

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

            _logger = logger;
        }

        /// <summary>
        /// Runs full drawdown analysis.
        /// </summary>
        /// <param name="trades">Trade records.</param>
        /// <returns>Drawdown report.</returns>
        public DrawdownReport Analyze(List<TradeRecord> trades)
        {
            if (trades == null)
                throw new ArgumentNullException("trades");

            try
            {
                var periods = IdentifyDrawdowns(trades);
                var report = new DrawdownReport();
                report.DrawdownPeriods = periods;
                report.NumberOfDrawdowns = periods.Count;
                report.MaxDrawdownAmount = periods.Count > 0 ? periods.Max(p => p.DrawdownAmount) : 0.0;
                report.MaxDrawdownPercent = periods.Count > 0 ? periods.Max(p => p.DrawdownPercent) : 0.0;
                report.CurrentDrawdownAmount = periods.Count > 0 && !periods[periods.Count - 1].RecoveryTime.HasValue
                    ? periods[periods.Count - 1].DrawdownAmount
                    : 0.0;
                report.AverageDrawdownPercent = periods.Count > 0 ? periods.Average(p => p.DrawdownPercent) : 0.0;
                report.LongestDuration = periods.Count > 0 ? periods.Max(p => p.DurationToTrough) : TimeSpan.Zero;

                var recovered = periods.Where(p => p.DurationToRecovery.HasValue).Select(p => p.DurationToRecovery.Value).ToList();
                if (recovered.Count > 0)
                {
                    report.AverageRecoveryTime = TimeSpan.FromTicks((long)recovered.Average(t => t.Ticks));
                }

                return report;
            }
            catch (Exception ex)
            {
                _logger.LogError("Drawdown Analyze failed: {0}", ex.Message);
                throw;
            }
        }

        /// <summary>
        /// Identifies drawdown periods from ordered trades.
        /// </summary>
        /// <param name="trades">Trade records.</param>
        /// <returns>Drawdown periods.</returns>
        public List<DrawdownPeriod> IdentifyDrawdowns(List<TradeRecord> trades)
        {
            if (trades == null)
                throw new ArgumentNullException("trades");

            try
            {
                var ordered = trades
                    .OrderBy(t => t.ExitTime.HasValue ? t.ExitTime.Value : t.EntryTime)
                    .ToList();

                var periods = new List<DrawdownPeriod>();
                var equity = 0.0;
                var peak = 0.0;
                DateTime peakTime = DateTime.MinValue;
                DrawdownPeriod active = null;

                foreach (var trade in ordered)
                {
                    var eventTime = trade.ExitTime.HasValue ? trade.ExitTime.Value : trade.EntryTime;
                    equity += trade.RealizedPnL;

                    if (equity >= peak)
                    {
                        peak = equity;
                        peakTime = eventTime;

                        if (active != null)
                        {
                            active.RecoveryTime = eventTime;
                            active.DurationToRecovery = active.RecoveryTime.Value - active.StartTime;
                            periods.Add(active);
                            active = null;
                        }

                        continue;
                    }

                    var drawdownAmount = peak - equity;
                    var drawdownPercent = peak > 0.0 ? (drawdownAmount / peak) * 100.0 : drawdownAmount;

                    if (active == null)
                    {
                        active = new DrawdownPeriod();
                        active.StartTime = peakTime == DateTime.MinValue ? eventTime : peakTime;
                        active.PeakEquity = peak;
                        active.TroughTime = eventTime;
                        active.TroughEquity = equity;
                        active.DrawdownAmount = drawdownAmount;
                        active.DrawdownPercent = drawdownPercent;
                        active.DurationToTrough = eventTime - active.StartTime;
                    }
                    else if (equity <= active.TroughEquity)
                    {
                        active.TroughTime = eventTime;
                        active.TroughEquity = equity;
                        active.DrawdownAmount = drawdownAmount;
                        active.DrawdownPercent = drawdownPercent;
                        active.DurationToTrough = eventTime - active.StartTime;
                    }
                }

                if (active != null)
                {
                    periods.Add(active);
                }

                return periods;
            }
            catch (Exception ex)
            {
                _logger.LogError("IdentifyDrawdowns failed: {0}", ex.Message);
                throw;
            }
        }

        /// <summary>
        /// Attributes one drawdown period to likely causes.
        /// </summary>
        /// <param name="period">Drawdown period.</param>
        /// <returns>Attribution details.</returns>
        public DrawdownAttribution AttributeDrawdown(DrawdownPeriod period)
        {
            if (period == null)
                throw new ArgumentNullException("period");

            try
            {
                var attribution = new DrawdownAttribution();

                if (period.DrawdownPercent >= 20.0)
                    attribution.PrimaryCause = "SevereLossCluster";
                else if (period.DrawdownPercent >= 10.0)
                    attribution.PrimaryCause = "ModerateLossCluster";
                else
                    attribution.PrimaryCause = "NormalVariance";

                attribution.TradeCount = 0;
                attribution.WorstSymbol = string.Empty;
                attribution.WorstStrategy = string.Empty;
                attribution.GradeContributions.Add("Unknown", period.DrawdownAmount);

                return attribution;
            }
            catch (Exception ex)
            {
                _logger.LogError("AttributeDrawdown failed: {0}", ex.Message);
                throw;
            }
        }

        /// <summary>
        /// Calculates recovery time in days for a drawdown period.
        /// </summary>
        /// <param name="period">Drawdown period.</param>
        /// <returns>Recovery time in days, -1 if unrecovered.</returns>
        public double CalculateRecoveryTime(DrawdownPeriod period)
        {
            if (period == null)
                throw new ArgumentNullException("period");

            try
            {
                if (!period.RecoveryTime.HasValue)
                    return -1.0;

                return (period.RecoveryTime.Value - period.StartTime).TotalDays;
            }
            catch (Exception ex)
            {
                _logger.LogError("CalculateRecoveryTime failed: {0}", ex.Message);
                throw;
            }
        }
    }
}
