using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Predictalytics.Application.Interfaces;
using Predictalytics.Domain.Entities;
using Predictalytics.Domain.Enums;
using Predictalytics.Infrastructure.Data;

namespace Predictalytics.Infrastructure.Services;

public class PositionPnLEngine : IPositionPnLEngine
{
    private readonly AppDbContext _db;
    private readonly ILogger<PositionPnLEngine> _logger;

    public PositionPnLEngine(AppDbContext db, ILogger<PositionPnLEngine> logger)
    {
        _db = db;
        _logger = logger;
    }

    public async Task RecalculateTraderPositionsAsync(int traderId, CancellationToken ct = default)
    {
        var trader = await _db.Traders
            .Include(t => t.Analytics)
            .FirstOrDefaultAsync(t => t.Id == traderId, ct);

        if (trader == null)
        {
            _logger.LogWarning("RecalculateTraderPositions: Trader {TraderId} not found.", traderId);
            return;
        }

        if (trader.IngestMode == IngestMode.SnapshotOnly)
        {
            _logger.LogInformation("Skipping tape replay/recalculation for SnapshotOnly trader {TraderId}", traderId);
            return;
        }

        // Fetch all trades for this trader, sorted chronologically, including outcomes and markets
        var trades = await _db.Trades
            .Include(t => t.MarketOutcome)
                .ThenInclude(o => o!.Market)
            .Where(t => t.TraderId == traderId)
            .OrderBy(t => t.ExecutedAt)
            .ToListAsync(ct);

        // Fetch existing positions for this trader to update or replace them
        var existingPositions = await _db.TraderPositions
            .Include(tp => tp.MarketOutcome)
                .ThenInclude(o => o!.Market)
            .Where(tp => tp.TraderId == traderId)
            .ToDictionaryAsync(tp => tp.MarketOutcomeId, ct);

        // Pre-flight check: Detect out-of-order unapplied trades (Bug 3) or manually reset checkpoints (Bug 2a)
        foreach (var pos in existingPositions.Values)
        {
            if (pos.IsHistoryPruned) continue; // Cannot reset if history is pruned

            var outcomeTrades = trades.Where(t => t.MarketOutcomeId == pos.MarketOutcomeId).ToList();
            bool needsReset = pos.LastAppliedTradeId == 0 || 
                outcomeTrades.Any(t => t.Id > pos.LastAppliedTradeId && pos.LastTradeExecutedAt.HasValue && t.ExecutedAt < pos.LastTradeExecutedAt.Value);

            if (needsReset)
            {
                if (pos.LastAppliedTradeId > 0)
                {
                    _logger.LogInformation("Out-of-order unapplied trade detected for Trader {TraderId} Outcome {OutcomeId}. Resetting position.", traderId, pos.MarketOutcomeId);
                }
                
                pos.SharesHeld = 0;
                pos.AvgCost = 0;
                pos.RealizedPnl = 0;
                pos.LastAppliedTradeId = 0;
                pos.LastTradeExecutedAt = null;
            }
        }

        var analytics = trader.Analytics;
        if (analytics == null)
        {
            analytics = new TraderAnalytics { TraderId = traderId };
            _db.TraderAnalytics.Add(analytics);
        }

        var tempPositions = new Dictionary<int, TraderPosition>();

        var cutoff30d = DateTime.UtcNow.AddDays(-30);
        var cutoff7d = DateTime.UtcNow.AddDays(-7);
        var cutoff24h = DateTime.UtcNow.AddHours(-24);
        
        decimal currentBalance = analytics.CurrentBalance;
        decimal estimatedBankroll = analytics.EstimatedBankroll;

        // Tracks outcomes traded within time frames
        var tradedOutcomes30d = new HashSet<int>();
        var tradedOutcomes7d = new HashSet<int>();
        var tradedOutcomes24h = new HashSet<int>();

        foreach (var trade in trades)
        {
            if (trade.MarketOutcomeId == null || trade.MarketOutcome == null)
                continue;

            var outcomeId = trade.MarketOutcomeId.Value;

            // Track if trade is within windows
            if (trade.ExecutedAt >= cutoff30d) tradedOutcomes30d.Add(outcomeId);
            if (trade.ExecutedAt >= cutoff7d) tradedOutcomes7d.Add(outcomeId);
            if (trade.ExecutedAt >= cutoff24h) tradedOutcomes24h.Add(outcomeId);

            if (!tempPositions.TryGetValue(outcomeId, out var pos))
            {
                if (existingPositions.TryGetValue(outcomeId, out var existing))
                {
                    pos = existing;
                }
                else
                {
                    pos = new TraderPosition
                    {
                        TraderId = traderId,
                        MarketOutcomeId = outcomeId,
                        MarketOutcome = trade.MarketOutcome, // Bug 8 Fix: Assign immediately so virtual payout runs
                        SharesHeld = 0,
                        AvgCost = 0,
                        RealizedPnl = 0,
                        LastAppliedTradeId = 0,
                        LastTradeExecutedAt = null,
                        IsHistoryPruned = false
                    };
                }
                pos.LastUpdatedAt = DateTime.UtcNow;
                tempPositions[outcomeId] = pos;
            }

            if (trade.Id <= pos.LastAppliedTradeId)
            {
                continue;
            }

            // Bug 3: Pruned positions must not re-apply orphaned trades that were skipped before the checkpoint
            if (pos.IsHistoryPruned && pos.LastTradeExecutedAt.HasValue && trade.ExecutedAt <= pos.LastTradeExecutedAt.Value)
            {
                continue;
            }

            var previousRealizedPnl = pos.RealizedPnl;

            // Apply trade side booking rules
            switch (trade.Side)
            {
                case TradeSide.Buy:
                    currentBalance -= trade.Amount;
                    if (pos.SharesHeld == 0)
                    {
                        pos.AvgCost = trade.Price;
                        pos.SharesHeld = trade.Size;
                    }
                    else
                    {
                        // Weighted average cost
                        var totalCost = (pos.SharesHeld * pos.AvgCost) + (trade.Size * trade.Price);
                        var totalShares = pos.SharesHeld + trade.Size;
                        pos.AvgCost = totalShares > 0 ? totalCost / totalShares : 0;
                        pos.SharesHeld = totalShares;
                    }
                    break;

                case TradeSide.Sell:
                    currentBalance += trade.Amount;
                    var sizeToSell = Math.Min(trade.Size, pos.SharesHeld);
                    pos.RealizedPnl += sizeToSell * (trade.Price - pos.AvgCost);
                    pos.SharesHeld -= trade.Size;
                    if (pos.SharesHeld < 0)
                    {
                        pos.SharesHeld = 0; // clamp to 0
                    }
                    break;

                case TradeSide.Redeem:
                    if (TryDeterminePayout(trade.MarketOutcome, out var redeemPayout))
                    {
                        currentBalance += (pos.SharesHeld * redeemPayout);
                        pos.RealizedPnl += pos.SharesHeld * (redeemPayout - pos.AvgCost);
                        pos.SharesHeld = 0;
                        pos.AvgCost = 0;
                    }
                    break;

                case TradeSide.Split:
                case TradeSide.Merge:
                    var cashEquivalent = trade.Size * trade.Price;
                    if (trade.Side == TradeSide.Split)
                    {
                        currentBalance -= cashEquivalent;
                        var totalCost = (pos.SharesHeld * pos.AvgCost) + cashEquivalent;
                        var totalShares = pos.SharesHeld + trade.Size;
                        pos.AvgCost = totalShares > 0 ? totalCost / totalShares : 0;
                        pos.SharesHeld = totalShares;
                    }
                    else if (trade.Side == TradeSide.Merge)
                    {
                        currentBalance += cashEquivalent;
                        var splitSizeToSell = Math.Min(trade.Size, pos.SharesHeld);
                        pos.RealizedPnl += splitSizeToSell * (trade.Price - pos.AvgCost);
                        pos.SharesHeld -= trade.Size;
                        if (pos.SharesHeld < 0) pos.SharesHeld = 0;
                    }
                    break;
                case TradeSide.AddLiquidity:
                case TradeSide.RemoveLiquidity:
                case TradeSide.Unknown:
                default:
                    // Ignored for PnL
                    break;
            }

            if (currentBalance < 0 && Math.Abs(currentBalance) > estimatedBankroll)
            {
                estimatedBankroll = Math.Abs(currentBalance);
            }

            pos.LastAppliedTradeId = Math.Max(pos.LastAppliedTradeId, trade.Id);
            if (pos.LastTradeExecutedAt == null || trade.ExecutedAt > pos.LastTradeExecutedAt.Value)
            {
                pos.LastTradeExecutedAt = trade.ExecutedAt;
            }

            var realizedPnlDelta = pos.RealizedPnl - previousRealizedPnl;
        }

        // Bug 1: Include positions without trades in this batch
        var allPositions = tempPositions.Values
            .Concat(existingPositions.Values.Where(ep => !tempPositions.ContainsKey(ep.MarketOutcomeId)))
            .ToList();

        // Bug 6: Virtual payout for unredeemed winning positions
        foreach (var pos in allPositions)
        {
            if (pos.SharesHeld > 0 && pos.MarketOutcome?.Market != null)
            {
                if (TryDeterminePayout(pos.MarketOutcome, out var virtualPayout))
                {
                    var virtualPnlDelta = pos.SharesHeld * (virtualPayout - pos.AvgCost);
                    pos.RealizedPnl += virtualPnlDelta;
                    pos.SharesHeld = 0;
                    pos.AvgCost = 0;
                }
            }
        }

        // Persist new / updated positions and calculate total values
        decimal totalRealizedPnl = 0;
        decimal totalUnrealizedPnl = 0;

        foreach (var pos in allPositions)
        {
            var outcome = trades.FirstOrDefault(t => t.MarketOutcomeId == pos.MarketOutcomeId)?.MarketOutcome ?? pos.MarketOutcome;
            if (pos.SharesHeld > 0 && outcome != null)
            {
                var unrealized = pos.SharesHeld * (outcome.CurrentPrice - pos.AvgCost);
                totalUnrealizedPnl += unrealized;
            }
            totalRealizedPnl += pos.RealizedPnl;

            // Only update DB for positions that were modified or newly created
            if (tempPositions.ContainsKey(pos.MarketOutcomeId))
            {
                if (pos.Id == 0)
                {
                    _db.TraderPositions.Add(pos);
                }
                else
                {
                    _db.TraderPositions.Update(pos);
                }
            }
        }

        // Bug 7: Positions are intentionally kept even if their trades are pruned by retention policies.

        // Update analytics record
        // (Analytics instance is already retrieved at the top of this method)
        
        var overallPnl = totalRealizedPnl + totalUnrealizedPnl;
        analytics.OverallPnL = overallPnl;
        analytics.CurrentBalance = currentBalance;
        analytics.EstimatedBankroll = estimatedBankroll;

        // Bug 1: Calculate time windows based on historical snapshots
        var today = DateTime.UtcNow.Date;
        
        // Save today's snapshot
        var todaySnapshot = await _db.TraderDailySnapshots
            .FirstOrDefaultAsync(s => s.TraderId == traderId && s.Date == today, ct);
            
        if (todaySnapshot == null)
        {
            todaySnapshot = new TraderDailySnapshot
            {
                TraderId = traderId,
                Date = today,
                TotalPnl = overallPnl,
                CurrentBalance = currentBalance
            };
            _db.TraderDailySnapshots.Add(todaySnapshot);
        }
        else
        {
            todaySnapshot.TotalPnl = overallPnl;
            todaySnapshot.CurrentBalance = currentBalance;
        }

        // Fetch historical snapshots
        var snapshot24h = await _db.TraderDailySnapshots
            .Where(s => s.TraderId == traderId && s.Date <= today.AddDays(-1))
            .OrderByDescending(s => s.Date)
            .FirstOrDefaultAsync(ct);
            
        var snapshot7d = await _db.TraderDailySnapshots
            .Where(s => s.TraderId == traderId && s.Date <= today.AddDays(-7))
            .OrderByDescending(s => s.Date)
            .FirstOrDefaultAsync(ct);
            
        var snapshot30d = await _db.TraderDailySnapshots
            .Where(s => s.TraderId == traderId && s.Date <= today.AddDays(-30))
            .OrderByDescending(s => s.Date)
            .FirstOrDefaultAsync(ct);

        var oldestSnapshot = await _db.TraderDailySnapshots
            .Where(s => s.TraderId == traderId)
            .OrderBy(s => s.Date)
            .FirstOrDefaultAsync(ct);

        var firstTrade = await _db.Trades
            .Where(t => t.TraderId == traderId)
            .OrderBy(t => t.ExecutedAt)
            .FirstOrDefaultAsync(ct);
            
        decimal GetFallback(DateTime cutoff)
        {
            if (firstTrade != null && firstTrade.ExecutedAt < cutoff)
            {
                // The trader traded before the window, but we have no snapshot.
                // Fallback to oldest snapshot if it exists, otherwise overallPnl (so that PnL24h=0)
                return oldestSnapshot?.TotalPnl ?? overallPnl;
            }
            // Trader started trading inside the window, so baseline is 0.
            return 0;
        }

        analytics.PnL24h = overallPnl - (snapshot24h?.TotalPnl ?? GetFallback(today.AddDays(-1)));
        analytics.PnL7d = overallPnl - (snapshot7d?.TotalPnl ?? GetFallback(today.AddDays(-7)));
        analytics.PnL30d = overallPnl - (snapshot30d?.TotalPnl ?? GetFallback(today.AddDays(-30)));

        // Count Trades30d
        analytics.Trades30d = trades.Where(t => t.ExecutedAt >= cutoff30d).Sum(t => t.AggregatedCount ?? 1);

        // Calculate Win Rate and Return Pcts on Market level
        var (winRateOverall, winRate30d, winRate7d, winRate24h, 
             medianWin, avgWin, medianLoss, avgLoss, profitFactor) = 
             CalculateMarketWinRates(trades, tempPositions, cutoff30d, cutoff7d, cutoff24h);

        analytics.OverallWinRate = winRateOverall;
        analytics.WinRate30d = winRate30d;
        analytics.WinRate7d = winRate7d;
        analytics.WinRate24h = winRate24h;
        
        analytics.MedianWinReturnPct = medianWin;
        analytics.AvgWinReturnPct = avgWin;
        analytics.MedianLossReturnPct = medianLoss;
        analytics.AvgLossReturnPct = avgLoss;
        analytics.ProfitFactor = profitFactor;
        analytics.LastCalculatedAt = DateTime.UtcNow;

        // Sync back to Trader record for quick sorting / UI display
        trader.TotalPnl = overallPnl;
        trader.WinRate = winRateOverall;
        // Row count is the single source of truth for TotalTrades — the worker-side
        // increment counters drift (INSERT IGNORE, deletions, historic imports) and
        // produced impossible states like Trades30d > TotalTrades. Compacted rows
        // count as 1 until an AggregatedCount column exists (FIXPLAN Teil D).
        trader.TotalTrades = trades.Sum(t => t.AggregatedCount ?? 1);
        if (trades.Count > 0 || trader.LastTradesUpdatedAt != null)
        {
            trader.LastAnalyzedAt = DateTime.UtcNow;
        }

        // Calculate Category Performance
        var existingCatPerf = await _db.TraderCategoryPerformances
            .Where(tcp => tcp.TraderId == traderId)
            .ToDictionaryAsync(tcp => (tcp.Category, tcp.Subcategory), ct);

        var newCatPerf = CalculateCategoryPerformances(trades, tempPositions);
        
        foreach (var kvp in newCatPerf)
        {
            if (existingCatPerf.TryGetValue(kvp.Key, out var existing))
            {
                existing.TotalVolume = kvp.Value.TotalVolume;
                existing.TotalPnL = kvp.Value.TotalPnL;
                existing.TotalInvested = kvp.Value.TotalInvested;
                existing.TotalTrades = kvp.Value.TotalTrades;
                existing.WinningTrades = kvp.Value.WinningTrades;
                _db.TraderCategoryPerformances.Update(existing);
            }
            else
            {
                var newEntity = kvp.Value;
                newEntity.TraderId = traderId;
                _db.TraderCategoryPerformances.Add(newEntity);
            }
        }

        // Save changes to database
        await _db.SaveChangesAsync(ct);

        _logger.LogInformation("Recalculated positions for trader {TraderName} (Id={TraderId}): RealizedPnL={Realized:F4}, UnrealizedPnL={Unrealized:F4}, Total={Total:F4}, WinRate={WinRate:F2}%",
            trader.DisplayName, traderId, totalRealizedPnl, totalUnrealizedPnl, overallPnl, winRateOverall);
    }

    private static (decimal Overall, decimal WinRate30d, decimal WinRate7d, decimal WinRate24h,
        decimal MedianWinReturnPct, decimal AvgWinReturnPct,
        decimal MedianLossReturnPct, decimal AvgLossReturnPct,
        decimal? ProfitFactor) CalculateMarketWinRates(
        List<Trade> trades,
        Dictionary<int, TraderPosition> finalPositions,
        DateTime cutoff30d,
        DateTime cutoff7d,
        DateTime cutoff24h)
    {
        // Group trades by Market
        var tradesByMarket = trades
            .Where(t => t.DbMarketId.HasValue || !string.IsNullOrEmpty(t.MarketId))
            .GroupBy(t => t.DbMarketId.HasValue ? t.DbMarketId.Value.ToString() : t.MarketId);

        int closedMarketsOverall = 0, winsOverall = 0;
        int closedMarkets30d = 0, wins30d = 0;
        int closedMarkets7d = 0, wins7d = 0;
        int closedMarkets24h = 0, wins24h = 0;

        var winReturns = new List<decimal>();
        var lossReturns = new List<decimal>();
        decimal totalGrossWins = 0;
        decimal totalGrossLosses = 0;

        foreach (var marketGroup in tradesByMarket)
        {
            var outcomeIds = marketGroup
                .Where(t => t.MarketOutcomeId.HasValue)
                .Select(t => t.MarketOutcomeId!.Value)
                .Distinct()
                .ToList();

            var isClosed = outcomeIds.All(oid => !finalPositions.TryGetValue(oid, out var pos) || pos.SharesHeld == 0);
            if (!isClosed)
            {
                var firstTradeWithMarket = marketGroup.FirstOrDefault(t => t.MarketOutcome?.Market != null);
                if (firstTradeWithMarket?.MarketOutcome?.Market?.IsResolved == true)
                {
                    isClosed = true;
                }
            }

            if (isClosed)
            {
                decimal marketPnl = 0;
                foreach (var oid in outcomeIds)
                {
                    if (finalPositions.TryGetValue(oid, out var pos))
                    {
                        marketPnl += pos.RealizedPnl;
                    }
                }

                var invested = marketGroup.Where(t => t.Side == TradeSide.Buy).Sum(t => t.Amount);
                if (invested > 0)
                {
                    var returnPct = marketPnl / invested * 100m;
                    if (returnPct > 0) 
                    {
                        winReturns.Add(returnPct);
                        totalGrossWins += marketPnl;
                    }
                    else if (returnPct < 0) 
                    {
                        lossReturns.Add(returnPct);
                        totalGrossLosses += Math.Abs(marketPnl);
                    }
                }

                var lastTradeTime = marketGroup.Max(t => t.ExecutedAt);
                var isWin = marketPnl > 0;

                closedMarketsOverall++;
                if (isWin) winsOverall++;

                if (lastTradeTime >= cutoff30d)
                {
                    closedMarkets30d++;
                    if (isWin) wins30d++;
                }
                if (lastTradeTime >= cutoff7d)
                {
                    closedMarkets7d++;
                    if (isWin) wins7d++;
                }
                if (lastTradeTime >= cutoff24h)
                {
                    closedMarkets24h++;
                    if (isWin) wins24h++;
                }
            }
        }

        var winRateOverall = closedMarketsOverall > 0 ? (decimal)winsOverall / closedMarketsOverall * 100m : 0m;
        var winRate30d = closedMarkets30d > 0 ? (decimal)wins30d / closedMarkets30d * 100m : 0m;
        var winRate7d = closedMarkets7d > 0 ? (decimal)wins7d / closedMarkets7d * 100m : 0m;
        var winRate24h = closedMarkets24h > 0 ? (decimal)wins24h / closedMarkets24h * 100m : 0m;

        decimal medianWin = 0, avgWin = 0, medianLoss = 0, avgLoss = 0;
        decimal? profitFactor = null;

        if (winReturns.Count > 0)
        {
            winReturns.Sort();
            avgWin = winReturns.Average();
            medianWin = winReturns.Count % 2 != 0 
                ? winReturns[winReturns.Count / 2] 
                : (winReturns[(winReturns.Count - 1) / 2] + winReturns[winReturns.Count / 2]) / 2m;
        }

        if (lossReturns.Count > 0)
        {
            lossReturns.Sort();
            avgLoss = lossReturns.Average();
            medianLoss = lossReturns.Count % 2 != 0 
                ? lossReturns[lossReturns.Count / 2] 
                : (lossReturns[(lossReturns.Count - 1) / 2] + lossReturns[lossReturns.Count / 2]) / 2m;
        }

        if (totalGrossLosses > 0)
        {
            profitFactor = totalGrossWins / totalGrossLosses;
        }
        else if (totalGrossWins > 0)
        {
            // Edge case: trader only wins and has no losses
            profitFactor = null; // or theoretically infinity, but null is safer and standard
        }

        return (winRateOverall, winRate30d, winRate7d, winRate24h, medianWin, avgWin, medianLoss, avgLoss, profitFactor);
    }



    private static Dictionary<(MarketCategory, string), TraderCategoryPerformance> CalculateCategoryPerformances(
        List<Trade> trades,
        Dictionary<int, TraderPosition> finalPositions)
    {
        var result = new Dictionary<(MarketCategory, string), TraderCategoryPerformance>();

        var tradesByMarket = trades
            .Where(t => t.MarketOutcome?.Market != null)
            .GroupBy(t => t.MarketOutcome!.Market!);

        foreach (var marketGroup in tradesByMarket)
        {
            var market = marketGroup.Key;
            var category = market.Category;
            var subcat = market.Subcategory ?? "";

            var key = (category, subcat);

            if (!result.TryGetValue(key, out var perf))
            {
                perf = new TraderCategoryPerformance { Category = category, Subcategory = subcat };
                result[key] = perf;
            }

            // Add volume
            perf.TotalVolume += marketGroup.Sum(t => t.Amount);

            // Determine if market is closed for this trader
            var outcomeIds = marketGroup
                .Where(t => t.MarketOutcomeId.HasValue)
                .Select(t => t.MarketOutcomeId!.Value)
                .Distinct()
                .ToList();

            var isClosed = outcomeIds.All(oid => !finalPositions.TryGetValue(oid, out var pos) || pos.SharesHeld == 0);
            if (!isClosed && market.IsResolved)
            {
                isClosed = true;
            }

            if (isClosed)
            {
                decimal marketPnl = 0;
                foreach (var oid in outcomeIds)
                {
                    if (finalPositions.TryGetValue(oid, out var pos))
                    {
                        marketPnl += pos.RealizedPnl;
                    }
                }

                decimal marketBuyAmount = marketGroup
                    .Where(t => t.Side == TradeSide.Buy)
                    .Sum(t => t.Amount);

                perf.TotalInvested += marketBuyAmount;
                perf.TotalPnL += marketPnl;
                perf.TotalTrades += 1;
                if (marketPnl > 0)
                {
                    perf.WinningTrades += 1;
                }
            }
        }

        return result;
    }

    private bool TryDeterminePayout(MarketOutcome outcome, out decimal payout)
    {
        payout = 0m;
        var market = outcome.Market;
        if (market == null || !market.IsResolved) return false;

        if (!string.IsNullOrWhiteSpace(market.ResolutionOutcome))
        {
            payout = Predictalytics.Domain.Helpers.MarketOutcomeHelper.IsWinningOutcome(outcome, market.ResolutionOutcome) ? 1.00m : 0.00m;
            return true;
        }

        if (market.Outcomes != null && market.Outcomes.Any(o => o.CurrentPrice >= 0.98m))
        {
            payout = outcome.CurrentPrice >= 0.98m ? 1.00m : 0.00m;
            return true;
        }

        return false;
    }
}
