using Microsoft.Extensions.Logging;
using Predictalytics.Application.Interfaces;
using Predictalytics.Domain.Entities;
using Predictalytics.Infrastructure.Providers.Polymarket;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Predictalytics.Infrastructure.Services;

public class CopytradingEstimator : ICopytradingEstimator
{
    private readonly ILogger<CopytradingEstimator> _logger;
    private readonly PolymarketApiClient _polymarketClient;
    private readonly IRateLimiter _rateLimiter;

    public CopytradingEstimator(ILogger<CopytradingEstimator> logger, PolymarketApiClient polymarketClient, IRateLimiter rateLimiter)
    {
        _logger = logger;
        _polymarketClient = polymarketClient;
        _rateLimiter = rateLimiter;
    }

    public async Task<(decimal QualityScore, decimal CopyabilityScore, decimal CombinedScore)> CalculateScoresAsync(
        Trader trader, 
        IReadOnlyList<Trade> trades, 
        CancellationToken ct = default)
    {
        if (trades.Count == 0) return (0, 0, 0);

        // 1. Event Clustering
        var events = ClusterTradesIntoEvents(trades, TimeSpan.FromHours(24));

        decimal qualityScore = CalculateQualityScore(trader, events);
        decimal copyabilityScore = await CalculateCopyabilityScoreAsync(trader, events, ct);

        // Minimum Quality Gate: if Quality is below 30, it is not highly copyable regardless of copyability.
        // We do not hard-gate to 0, but we scale it down heavily.
        
        // Multiplicative Formula: Both in [0, 100], combined in [0, 100]
        decimal normalizedQuality = qualityScore / 100m;
        decimal normalizedCopyability = copyabilityScore / 100m;
        
        decimal combinedScore = (normalizedQuality * normalizedCopyability) * 100m;
        
        // Hard-gate: If quality is very low, combined score is strictly 0.
        if (qualityScore < 20)
        {
            combinedScore = 0;
        }

        return (Math.Clamp(Math.Round(qualityScore, 2), 0, 100), 
                Math.Clamp(Math.Round(copyabilityScore, 2), 0, 100), 
                Math.Clamp(Math.Round(combinedScore, 2), 0, 100));
    }

    private decimal CalculateQualityScore(Trader trader, IReadOnlyList<TradeEvent> events)
    {
        var buyEvents = events.Where(e => e.Side == Predictalytics.Domain.Enums.TradeSide.Buy).ToList();
        if (buyEvents.Count == 0) return 0;

        var edges = new List<decimal>();

        foreach (var e in buyEvents)
        {
            var sampleTrade = e.ConstituentTrades.First();
            var market = sampleTrade.DbMarket;
            var outcome = sampleTrade.MarketOutcome;

            decimal resolutionPrice = 0;

            // Get the final outcome resolution value using the robust matcher
            var isWinner = sampleTrade.MarketOutcome != null && Predictalytics.Domain.Helpers.MarketOutcomeHelper.IsWinningOutcome(sampleTrade.MarketOutcome, market?.ResolutionOutcome);
            resolutionPrice = isWinner ? 1.0m : 0.0m;
            
            // If not resolved or unknown, assume neutral or average price
            if (!market?.IsResolved ?? true)
            {
                resolutionPrice = outcome?.CurrentPrice ?? e.AveragePrice; // fallback to entry if unknown
            }

            decimal entryPrice = e.AveragePrice;
            decimal size = e.TotalSize;
            decimal risked = entryPrice * size;
            
            if (risked > 0)
            {
                decimal eventPnl = (resolutionPrice - entryPrice) * size;
                edges.Add(eventPnl / risked); // Edge for this event (Return on Investment)
            }
        }

        if (edges.Count == 0) return 0;

        decimal meanEdge = edges.Average();
        
        decimal stdDev = 0;
        if (edges.Count > 1)
        {
            decimal sumOfSquares = edges.Sum(e => (e - meanEdge) * (e - meanEdge));
            stdDev = (decimal)Math.Sqrt((double)(sumOfSquares / (edges.Count - 1)));
        }

        decimal standardError = stdDev / (decimal)Math.Sqrt(edges.Count);
        decimal zScore = 1.645m; // 90% confidence
        
        decimal lcbEdge = meanEdge - (zScore * standardError);

        // Convert LCB Edge to a 0-100 score. 
        // 0% edge = 50. +20% edge = 100. -20% edge = 0.
        decimal score = 50 + (lcbEdge * 100m * 2.5m); 
        return Math.Clamp(Math.Round(score, 2), 0, 100);
    }

    private Task<decimal> CalculateCopyabilityScoreAsync(Trader trader, IReadOnlyList<TradeEvent> events, CancellationToken ct)
    {
        if (events.Count == 0) return Task.FromResult(0m);

        var alphaDecays = new List<decimal>();
        var sizes = new List<decimal>();

        foreach (var e in events)
        {
            decimal eventDecay = 0;
            decimal totalTradeSize = 0;

            foreach (var t in e.ConstituentTrades)
            {
                sizes.Add(t.Amount); // USD equivalent sizing

                if (t.Context?.FollowerFillPrice10s.HasValue == true && t.Price > 0)
                {
                    decimal slippage = 0;
                    if (t.Side == Predictalytics.Domain.Enums.TradeSide.Buy)
                    {
                        slippage = t.Context.FollowerFillPrice10s.Value - t.Price; // Positive slippage = price went up (we buy higher)
                    }
                    else
                    {
                        slippage = t.Price - t.Context.FollowerFillPrice10s.Value; // Positive slippage = price went down (we sell lower)
                    }
                    
                    // slippage as a percentage of price
                    decimal decayPct = slippage / t.Price;
                    eventDecay += decayPct * t.Size;
                    totalTradeSize += t.Size;
                }
            }

            if (totalTradeSize > 0)
            {
                alphaDecays.Add(eventDecay / totalTradeSize);
            }
        }

        // 1. Alpha Decay Score
        decimal averageDecay = alphaDecays.Count > 0 ? alphaDecays.Average() : 0;
        
        // Convert averageDecay to score.
        // If average decay is <= 0%, score is 100.
        // If average decay is 10% (0.10), score is 0.
        decimal decayScore = 100m - (averageDecay * 1000m); // 0.10 * 1000 = 100
        decayScore = Math.Clamp(decayScore, 0, 100);

        // 2. Sizing Consistency Score
        decimal sizingScore = 50;
        if (sizes.Count > 1)
        {
            decimal meanSize = sizes.Average();
            decimal sumOfSquares = sizes.Sum(s => (s - meanSize) * (s - meanSize));
            decimal stdDev = (decimal)Math.Sqrt((double)(sumOfSquares / (sizes.Count - 1)));
            
            decimal cv = stdDev / (meanSize == 0 ? 1 : meanSize); // Coefficient of Variation
            
            // CV of 0 = 100 score. CV of 1.0 = 50 score. CV of 2.0 = 0 score.
            sizingScore = 100m - (cv * 50m);
            sizingScore = Math.Clamp(sizingScore, 0, 100);
        }

        decimal combined = (decayScore * 0.7m) + (sizingScore * 0.3m);
        return Task.FromResult(Math.Clamp(Math.Round(combined, 2), 0, 100));
    }

    public async Task<decimal?> EstimateFollowerFillPriceAsync(
        Trade traderTrade, 
        int latencySeconds = 10, 
        CancellationToken ct = default)
    {
        if (string.IsNullOrEmpty(traderTrade.MarketId)) return null;

        await _rateLimiter.WaitAsync(Predictalytics.Domain.Enums.PlatformType.Polymarket, ct);
        
        long tradeUnix = ((DateTimeOffset)traderTrade.ExecutedAt).ToUnixTimeSeconds();
        long windowStart = tradeUnix + latencySeconds;
        long windowEnd = windowStart + 120; // 2 minutes window to get filled

        var tape = new List<PolymarketTradeResponse>();
        int limit = 1000;
        int offset = 0;
        
        while (offset <= 1000)
        {
            try
            {
                var page = await _polymarketClient.GetMarketTradesAsync(traderTrade.MarketId, limit, offset, ct);
                if (page.Count == 0) break;
                
                var filtered = page.Where(p => 
                    !string.Equals(p.ProxyWallet, traderTrade.Trader.PlatformUserId, StringComparison.OrdinalIgnoreCase) && 
                    !string.Equals(p.User, traderTrade.Trader.PlatformUserId, StringComparison.OrdinalIgnoreCase))
                    .ToList();

                tape.AddRange(filtered);
                
                var oldestInPage = page.Last().Timestamp;
                if (oldestInPage < tradeUnix) 
                {
                    break; // We have gone back far enough
                }
                offset += limit;
            }
            catch (Exception)
            {
                break; // Offset exceeded limit or API error
            }
        }

        var relevantTrades = tape
            .Where(t => t.Asset == traderTrade.AssetId && t.Timestamp >= windowStart && t.Timestamp <= windowEnd)
            .OrderBy(t => t.Timestamp)
            .ToList();

        if (relevantTrades.Count == 0) return null;

        decimal remainingSize = traderTrade.Size;
        decimal totalValue = 0;
        decimal totalSizeFilled = 0;

        foreach (var t in relevantTrades)
        {
            decimal sizeAvailable = (decimal)t.Size;
            decimal fillSize = Math.Min(remainingSize, sizeAvailable);
            
            totalValue += fillSize * (decimal)t.Price;
            totalSizeFilled += fillSize;
            remainingSize -= fillSize;

            if (remainingSize <= 0) break;
        }

        if (totalSizeFilled == 0) return null;

        return totalValue / totalSizeFilled;
    }

    public IReadOnlyList<TradeEvent> ClusterTradesIntoEvents(IReadOnlyList<Trade> trades, TimeSpan window)
    {
        var events = new List<TradeEvent>();
        
        var grouped = trades.GroupBy(t => new { t.MarketId, t.AssetId, t.Side });

        foreach (var group in grouped)
        {
            var ordered = group.OrderBy(t => t.ExecutedAt).ToList();
            TradeEvent? currentEvent = null;

            foreach (var trade in ordered)
            {
                if (currentEvent == null || trade.ExecutedAt - currentEvent.EventStartTime > window)
                {
                    if (currentEvent != null) events.Add(currentEvent);
                    
                    currentEvent = new TradeEvent
                    {
                        MarketId = trade.MarketId,
                        AssetId = trade.AssetId ?? "",
                        Side = trade.Side,
                        EventStartTime = trade.ExecutedAt,
                        EventEndTime = trade.ExecutedAt,
                        ConstituentTrades = new List<Trade> { trade },
                        AveragePrice = trade.Price,
                        TotalSize = trade.Size
                    };
                }
                else
                {
                    currentEvent.ConstituentTrades.Add(trade);
                    currentEvent.EventEndTime = trade.ExecutedAt;
                    
                    // Update VWAP
                    decimal newTotalSize = currentEvent.TotalSize + trade.Size;
                    decimal totalValue = (currentEvent.AveragePrice * currentEvent.TotalSize) + (trade.Price * trade.Size);
                    currentEvent.AveragePrice = totalValue / newTotalSize;
                    currentEvent.TotalSize = newTotalSize;
                }
            }

            if (currentEvent != null)
            {
                events.Add(currentEvent);
            }
        }

        return events.OrderBy(e => e.EventStartTime).ToList();
    }

    public async Task<decimal?> CalculateMaeAsync(TradeEvent tradeEvent, CancellationToken ct = default)
    {
        if (string.IsNullOrEmpty(tradeEvent.MarketId)) return null;

        await _rateLimiter.WaitAsync(Predictalytics.Domain.Enums.PlatformType.Polymarket, ct);
        
        long startUnix = tradeEvent.EventStartTime.ToUnixTimeSeconds();
        // Determine end of holding period. Since we don't have sell trades linked natively, 
        // we'll just check MAE over the next 7 days or until now, whichever is shorter. 
        // (A more accurate implementation would need to match buys with sells to find the holding period)
        long endUnix = Math.Min(DateTimeOffset.UtcNow.ToUnixTimeSeconds(), startUnix + (7 * 24 * 3600));

        var tape = new List<PolymarketTradeResponse>();
        int limit = 1000;
        int offset = 0;
        
        while (offset <= 1000)
        {
            try
            {
                var page = await _polymarketClient.GetMarketTradesAsync(tradeEvent.MarketId, limit, offset, ct);
                if (page.Count == 0) break;
                
                tape.AddRange(page);
                
                var oldestInPage = page.Last().Timestamp;
                if (oldestInPage < startUnix) 
                {
                    break;
                }
                offset += limit;
            }
            catch (Exception)
            {
                break; // Offset exceeded limit or API error
            }
        }

        var relevantTrades = tape
            .Where(t => t.Asset == tradeEvent.AssetId && t.Timestamp >= startUnix && t.Timestamp <= endUnix)
            .ToList();

        if (relevantTrades.Count == 0) return null;

        decimal mae = 0;

        // VWAP of the event is the entry price
        decimal entryPrice = tradeEvent.AveragePrice;

        foreach (var t in relevantTrades)
        {
            decimal price = (decimal)t.Price;
            decimal unrealizedLoss = 0;

            if (tradeEvent.Side == Predictalytics.Domain.Enums.TradeSide.Buy)
            {
                // Buy -> loss if price drops
                if (price < entryPrice)
                {
                    unrealizedLoss = entryPrice - price;
                }
            }
            else
            {
                // Sell -> loss if price rises
                if (price > entryPrice)
                {
                    unrealizedLoss = price - entryPrice;
                }
            }

            if (unrealizedLoss > mae)
            {
                mae = unrealizedLoss;
            }
        }

        // Return MAE as a percentage of entry price or absolute? Absolute is often easier for 0-1 bounded prices.
        return mae;
    }
}
