using Predictalytics.Domain.Entities;
using Predictalytics.Domain.Enums;

namespace Predictalytics.Application.Services;

public static class TraderTraitCalculator
{
    public static List<(string Trait, decimal Value)> Compute(
        Trader trader, 
        IReadOnlyCollection<Trade> trades, 
        IReadOnlyCollection<TraderPosition> positions)
    {
        var traits = new List<(string, decimal)>();
        if (trader.IngestMode == IngestMode.SnapshotOnly)
        {
            traits.Add(("not_copyable_hf", 1.0m));
        }

        if (trades.Count == 0) return traits;

        var now = DateTime.UtcNow;
        var tradesList = trades.OrderBy(t => t.ExecutedAt).ToList();

        // 1. sub_second_cadence
        if (tradesList.Count >= 50)
        {
            var intervals = new List<double>();
            for (int i = 1; i < tradesList.Count; i++)
            {
                intervals.Add((tradesList[i].ExecutedAt - tradesList[i - 1].ExecutedAt).TotalSeconds);
            }
            
            intervals.Sort();
            var medianInterval = intervals[intervals.Count / 2];

            if (medianInterval < 2.0)
            {
                traits.Add(("sub_second_cadence", (decimal)medianInterval));
            }
        }

        // 2. always_on_24_7
        var last7dTrades = tradesList.Where(t => t.ExecutedAt >= now.AddDays(-7)).ToList();
        if (last7dTrades.Count >= 200)
        {
            double maxGapHours = 0;
            for (int i = 1; i < last7dTrades.Count; i++)
            {
                var gap = (last7dTrades[i].ExecutedAt - last7dTrades[i - 1].ExecutedAt).TotalHours;
                if (gap > maxGapHours) maxGapHours = gap;
            }
            if (maxGapHours < 4.0)
            {
                traits.Add(("always_on_24_7", (decimal)maxGapHours));
            }
        }

        // 3. uniform_sizes
        var last200 = tradesList.TakeLast(200).ToList();
        if (last200.Count >= 10)
        {
            var sizes = last200.Select(t => t.Size).Where(s => s > 0).ToList();
            if (sizes.Count > 0)
            {
                var mean = sizes.Average();
                var stdDev = (decimal)Math.Sqrt((double)sizes.Sum(s => (s - mean) * (s - mean)) / sizes.Count);
                if (mean > 0 && stdDev / mean < 0.1m)
                {
                    traits.Add(("uniform_sizes", stdDev / mean));
                }
            }
        }

        // 4. round_amounts
        var amounts = tradesList.Select(t => t.Amount).ToList();
        if (amounts.Count >= 10)
        {
            var targetAmounts = new[] { 1m, 5m, 10m, 20m, 25m, 50m, 100m, 250m, 500m, 1000m };
            int roundCount = 0;
            foreach (var a in amounts)
            {
                if (targetAmounts.Any(ta => Math.Abs(a - ta) <= ta * 0.01m))
                {
                    roundCount++;
                }
            }
            if ((decimal)roundCount / amounts.Count > 0.6m)
            {
                traits.Add(("round_amounts", (decimal)roundCount / amounts.Count));
            }
        }

        // 5. uses_split_merge
        int splitMergeCount = tradesList.Count(t => t.Side == TradeSide.Split || t.Side == TradeSide.Merge);
        if (tradesList.Count > 0 && (decimal)splitMergeCount / tradesList.Count > 0.1m)
        {
            traits.Add(("uses_split_merge", (decimal)splitMergeCount / tradesList.Count));
        }

        // 6. both_sides_same_market
        var marketsWithBothSides = tradesList
            .GroupBy(t => t.MarketId)
            .Count(g => g.Any(t => t.Side == TradeSide.Buy) && g.Any(t => t.Side == TradeSide.Sell));
        var totalMarkets = tradesList.Select(t => t.MarketId).Distinct().Count();
        if (totalMarkets > 0 && (decimal)marketsWithBothSides / totalMarkets > 0.2m)
        {
            traits.Add(("both_sides_same_market", (decimal)marketsWithBothSides / totalMarkets));
        }

        // 7. resolution_farming
        var buys = tradesList.Where(t => t.Side == TradeSide.Buy).ToList();
        if (buys.Count >= 10)
        {
            // For pure function on Trades, we use price for now as an approximation.
            int farmingBuys = buys.Count(b => b.Price >= 0.93m);
            if ((decimal)farmingBuys / buys.Count > 0.3m)
            {
                traits.Add(("resolution_farming", (decimal)farmingBuys / buys.Count));
            }
        }

        // 8. longshot_buyer
        if (buys.Count >= 10)
        {
            int longshotBuys = buys.Count(b => b.Price <= 0.10m);
            if ((decimal)longshotBuys / buys.Count > 0.3m)
            {
                traits.Add(("longshot_buyer", (decimal)longshotBuys / buys.Count));
            }
        }

        // 9. scalper
        // Approximate holding duration: time between first buy and last sell per market
        var holdDurations = new List<double>();
        var marketGroups = tradesList.GroupBy(t => t.MarketId);
        foreach (var mg in marketGroups)
        {
            var firstBuy = mg.Where(t => t.Side == TradeSide.Buy).OrderBy(t => t.ExecutedAt).FirstOrDefault();
            var lastSell = mg.Where(t => t.Side == TradeSide.Sell).OrderByDescending(t => t.ExecutedAt).FirstOrDefault();
            if (firstBuy != null && lastSell != null && lastSell.ExecutedAt > firstBuy.ExecutedAt)
            {
                holdDurations.Add((lastSell.ExecutedAt - firstBuy.ExecutedAt).TotalHours);
            }
        }
        if (holdDurations.Count > 0)
        {
            holdDurations.Sort();
            var medianHold = holdDurations[holdDurations.Count / 2];
            if (medianHold < 1.0)
            {
                traits.Add(("scalper", (decimal)medianHold));
            }
        }

        // 10. holds_to_resolution
        // If a position was resolved (realizedPnl != 0 or SharesHeld == 0 with a Redeem)
        var resolvedPositions = positions.Where(p => p.MarketOutcome?.Market?.IsResolved == true).ToList();
        if (resolvedPositions.Count > 0)
        {
            int heldToRes = resolvedPositions.Count(p => p.AvgCost > 0 && p.RealizedPnl != 0 && p.SharesHeld == 0);
            if ((decimal)heldToRes / resolvedPositions.Count > 0.7m)
            {
                traits.Add(("holds_to_resolution", (decimal)heldToRes / resolvedPositions.Count));
            }
        }

        // 11. fresh_wallet
        var firstTrade = tradesList.First();
        var ageDays = (now - firstTrade.ExecutedAt).TotalDays;
        if (ageDays < 30)
        {
            traits.Add(("fresh_wallet", (decimal)ageDays));
        }

        // 12. stable_stake_fraction
        if (trader.Analytics?.EstimatedBankroll > 0 && amounts.Count >= 10)
        {
            var bankroll = trader.Analytics.EstimatedBankroll;
            var fractions = amounts.Select(a => a / bankroll).ToList();
            var meanF = fractions.Average();
            var stdDevF = (decimal)Math.Sqrt((double)fractions.Sum(f => (f - meanF) * (f - meanF)) / fractions.Count);
            if (meanF > 0 && stdDevF / meanF < 0.5m)
            {
                traits.Add(("stable_stake_fraction", stdDevF / meanF));
            }
        }

        // 13. possible_insider
        bool isResolutionFarmer = traits.Any(t => t.Item1 == "resolution_farming");
        var resolvedMarkets = positions
            .Where(p => p.MarketOutcome?.Market?.IsResolved == true)
            .GroupBy(p => p.MarketOutcome!.MarketId)
            .ToList();

        if (resolvedMarkets.Count >= 5 && resolvedMarkets.Count <= 100 && !isResolutionFarmer)
        {
            var bankroll = trader.Analytics?.EstimatedBankroll ?? 0;
            var avgAmount = tradesList.Select(t => t.Amount).DefaultIfEmpty(0).Average();
            var tradesPerDay = tradesList.Count / Math.Max(1, (now - firstTrade.ExecutedAt).TotalDays);

            if ((avgAmount >= 500 || (bankroll > 0 && avgAmount >= bankroll * 0.1m)) && tradesPerDay < 5)
            {
                // Core metric "Market Surprise": L = Product(p(won)) * Product((1-p)(lost))
                // Value = -log10(L)
                double logL = 0;
                int wonCount = 0;
                var wonVwaps = new List<decimal>();

                foreach (var rm in resolvedMarkets)
                {
                    // p = entry VWAP. Approximate with AvgCost.
                    var pos = rm.FirstOrDefault();
                    if (pos == null || pos.AvgCost <= 0) continue;
                    
                    double p = (double)pos.AvgCost;
                    bool won = pos.RealizedPnl > 0 || (pos.MarketOutcome.Market.ResolutionOutcome == pos.MarketOutcome.Label);
                    
                    if (won)
                    {
                        logL += Math.Log10(p);
                        wonCount++;
                        wonVwaps.Add(pos.AvgCost);
                    }
                    else
                    {
                        logL += Math.Log10(Math.Max(1e-6, 1.0 - p));
                    }
                }

                decimal winRate = resolvedMarkets.Count > 0 ? (decimal)wonCount / resolvedMarkets.Count : 0;
                decimal avgWonVwap = wonVwaps.Count > 0 ? wonVwaps.Average() : 0;
                decimal surpriseValue = (decimal)(-logL);

                if (winRate >= 0.85m && avgWonVwap <= 0.70m && surpriseValue >= 3m)
                {
                    traits.Add(("possible_insider", surpriseValue));
                }
            }
        }

        // 14. Market Return Profile Traits (D2c)
        if (trader.Analytics != null && trader.Analytics.MedianWinReturnPct != 0)
        {
            int wonMarkets = resolvedMarkets.Count(rm => rm.Sum(p => p.RealizedPnl) > 0);
            
            if (trader.Analytics.MedianWinReturnPct < 10m && wonMarkets >= 20)
            {
                traits.Add(("thin_margin_wins", trader.Analytics.MedianWinReturnPct));
            }
            
            if (trader.Analytics.MedianWinReturnPct > 100m && wonMarkets >= 5)
            {
                traits.Add(("high_payoff_wins", trader.Analytics.MedianWinReturnPct));
            }
        }

        // sells_at_loss (stop_loss_ratio)
        var sells = tradesList.Where(t => t.Side == TradeSide.Sell && t.Price > 0).ToList();
        int stopLossCount = 0;
        int totalValidSells = 0;
        foreach (var sell in sells)
        {
            var pos = positions.FirstOrDefault(p => p.MarketOutcomeId == sell.MarketOutcomeId);
            if (pos != null && pos.AvgCost > 0)
            {
                totalValidSells++;
                if (sell.Price <= pos.AvgCost * 0.9m)
                {
                    stopLossCount++;
                }
            }
        }
        if (totalValidSells > 0)
        {
            decimal stopLossRatio = (decimal)stopLossCount / totalValidSells;
            if (stopLossRatio > 0.15m)
            {
                traits.Add(("sells_at_loss", stopLossRatio));
            }
        }

        // days_active
        var daysActive = (now - firstTrade.ExecutedAt).TotalDays;
        traits.Add(("days_active", (decimal)daysActive));

        // trades_last_30_days
        int tradesLast30d = tradesList.Count(t => (now - t.ExecutedAt).TotalDays <= 30);
        traits.Add(("trades_last_30_days", (decimal)tradesLast30d));

        // martingale_pattern
        var chronologicalMarkets = resolvedMarkets
            .Select(g => new {
                MarketId = g.Key,
                Won = g.Sum(p => p.RealizedPnl) > 0 || g.Any(p => p.MarketOutcome?.Market?.ResolutionOutcome == p.MarketOutcome?.Label),
                Invested = tradesList.Where(t => t.DbMarketId == g.Key && t.Side == TradeSide.Buy).Sum(t => t.Amount),
                ResolvedTime = g.First().MarketOutcome?.Market?.EndDate ?? DateTime.MinValue
            })
            .Where(x => x.Invested > 0)
            .OrderBy(x => x.ResolvedTime)
            .ToList();

        if (chronologicalMarkets.Count >= 20)
        {
            var stakesAfterWin = new List<decimal>();
            var stakesAfterLoss = new List<decimal>();
            for (int i = 1; i < chronologicalMarkets.Count; i++)
            {
                if (chronologicalMarkets[i-1].Won)
                {
                    stakesAfterWin.Add(chronologicalMarkets[i].Invested);
                }
                else
                {
                    stakesAfterLoss.Add(chronologicalMarkets[i].Invested);
                }
            }
            if (stakesAfterWin.Count > 0 && stakesAfterLoss.Count > 0)
            {
                decimal avgAfterWin = stakesAfterWin.Average();
                decimal avgAfterLoss = stakesAfterLoss.Average();
                if (avgAfterWin > 0)
                {
                    decimal ratio = avgAfterLoss / avgAfterWin;
                    if (ratio >= 1.5m)
                    {
                        traits.Add(("martingale_pattern", ratio));
                    }
                }
            }
        }

        return traits;
    }
}
