using System;
using System.Collections.Generic;
using System.Linq;
using QuantEngine.Core.Interfaces;

namespace QuantEngine.Core.Domain
{
    public record FactorOutputs(
        double Momentum20D,
        double Momentum60D,
        double Momentum120D,
        double Atr20Pct,
        double StDev20D,
        double Beta60D,
        double Rs20D
    );

    public static class FactorCalculator
    {
        public static FactorOutputs CalculateFactors(
            List<PriceHistoryDailyRecord> stockBars,
            List<PriceHistoryDailyRecord> indexBars)
        {
            if (stockBars == null || stockBars.Count < 2)
            {
                return new FactorOutputs(0, 0, 0, 0, 0, 1.0, 0);
            }

            var sortedStock = NormalizeBars(stockBars);
            var sortedIndex = NormalizeBars(indexBars);
            return new FactorOutputs(
                CalculateMomentum(sortedStock, 20),
                CalculateMomentum(sortedStock, 60),
                CalculateMomentum(sortedStock, 120),
                CalculateAtr20Pct(sortedStock),
                CalculatePriceStDev20D(sortedStock),
                CalculateBeta60D(sortedStock, sortedIndex),
                CalculateRs20D(sortedStock, sortedIndex));
        }

        private static List<PriceHistoryDailyRecord> NormalizeBars(List<PriceHistoryDailyRecord>? bars)
        {
            if (bars == null || bars.Count == 0)
            {
                return new List<PriceHistoryDailyRecord>();
            }

            return bars
                .OrderBy(b => b.TradeDate)
                .GroupBy(b => b.TradeDate)
                .Select(g => g.Last())
                .ToList();
        }

        private static double CalculateMomentum(List<PriceHistoryDailyRecord> bars, int period)
        {
            if (bars.Count <= period) return 0.0;
            double current = (double)bars[^1].Close;
            double prev = (double)bars[^(period + 1)].Close;
            if (prev <= 0.0 || double.IsNaN(prev) || double.IsInfinity(prev)) return 0.0;
            return ((current - prev) / prev) * 100.0;
        }

        private static double CalculateAtr20Pct(List<PriceHistoryDailyRecord> bars)
        {
            if (bars.Count < 21) return 0.0;

            var trList = new List<double>();
            for (int i = bars.Count - 20; i < bars.Count; i++)
            {
                double high = (double)bars[i].High;
                double low = (double)bars[i].Low;
                double prevClose = (double)bars[i - 1].Close;

                double tr = Math.Max(high - low, Math.Max(Math.Abs(high - prevClose), Math.Abs(low - prevClose)));
                trList.Add(tr);
            }

            double atr = trList.Average();
            double closeToday = (double)bars[^1].Close;
            if (closeToday <= 0.0 || double.IsNaN(closeToday) || double.IsInfinity(closeToday)) return 0.0;
            return (atr / closeToday) * 100.0;
        }

        private static double CalculatePriceStDev20D(List<PriceHistoryDailyRecord> bars)
        {
            if (bars.Count < 20) return 0.0;

            var subset = bars.Skip(bars.Count - 20).Select(b => (double)b.Close).ToList();
            double avg = subset.Average();
            double sumOfSquares = subset.Sum(val => Math.Pow(val - avg, 2));
            return Math.Sqrt(sumOfSquares / (subset.Count - 1));
        }

        private static double CalculateBeta60D(List<PriceHistoryDailyRecord> stock, List<PriceHistoryDailyRecord> index)
        {
            if (stock.Count < 61 || index.Count < 61) return 1.0;

            var stockMap = stock.ToDictionary(b => b.TradeDate);
            var indexMap = index.ToDictionary(b => b.TradeDate);

            var overlappingDates = stockMap.Keys.Intersect(indexMap.Keys).OrderBy(d => d).ToList();
            if (overlappingDates.Count < 61) return 1.0;

            var alignedStock = overlappingDates.Select(d => stockMap[d]).ToList();
            var alignedIndex = overlappingDates.Select(d => indexMap[d]).ToList();

            var stockReturns = new List<double>();
            var indexReturns = new List<double>();

            int startIdx = Math.Max(1, alignedStock.Count - 60);
            for (int i = startIdx; i < alignedStock.Count; i++)
            {
                double sPrev = (double)alignedStock[i - 1].Close;
                double sCurr = (double)alignedStock[i].Close;
                double iPrev = (double)alignedIndex[i - 1].Close;
                double iCurr = (double)alignedIndex[i].Close;

                if (sPrev > 0 && iPrev > 0)
                {
                    stockReturns.Add((sCurr - sPrev) / sPrev);
                    indexReturns.Add((iCurr - iPrev) / iPrev);
                }
            }

            if (stockReturns.Count < 10) return 1.0;

            double avgStock = stockReturns.Average();
            double avgIndex = indexReturns.Average();

            double covariance = 0.0;
            double varianceIndex = 0.0;

            for (int i = 0; i < stockReturns.Count; i++)
            {
                double diffStock = stockReturns[i] - avgStock;
                double diffIndex = indexReturns[i] - avgIndex;

                covariance += diffStock * diffIndex;
                varianceIndex += diffIndex * diffIndex;
            }

            if (varianceIndex <= 0.0) return 1.0;
            return covariance / varianceIndex;
        }

        private static double CalculateRs20D(List<PriceHistoryDailyRecord> stock, List<PriceHistoryDailyRecord> index)
        {
            if (stock.Count < 21 || index.Count < 21) return 0.0;

            var stockMap = stock.ToDictionary(b => b.TradeDate);
            var indexMap = index.ToDictionary(b => b.TradeDate);

            var overlappingDates = stockMap.Keys.Intersect(indexMap.Keys).OrderBy(d => d).ToList();
            if (overlappingDates.Count < 21) return 0.0;

            var alignedStock = overlappingDates.Select(d => stockMap[d]).ToList();
            var alignedIndex = overlappingDates.Select(d => indexMap[d]).ToList();

            double sCurr = (double)alignedStock[^1].Close;
            double sPrev = (double)alignedStock[^(20 + 1)].Close;
            double iCurr = (double)alignedIndex[^1].Close;
            double iPrev = (double)alignedIndex[^(20 + 1)].Close;

            if (sPrev <= 0.0 || iPrev <= 0.0) return 0.0;

            double stockReturn = (sCurr - sPrev) / sPrev * 100.0;
            double indexReturn = (iCurr - iPrev) / iPrev * 100.0;

            return stockReturn - indexReturn;
        }
    }
}
