using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using Predictalytics.Infrastructure.Data;
using Predictalytics.Application.Interfaces;
using Predictalytics.Domain.Interfaces;

namespace Predictalytics.Worker.Services;

public class TraderAnalyticsWorker : BackgroundService
{
    private readonly IServiceProvider _services;
    private readonly ILogger<TraderAnalyticsWorker> _logger;

    public TraderAnalyticsWorker(IServiceProvider services, ILogger<TraderAnalyticsWorker> _logger)
    {
        _services = services;
        this._logger = _logger;
    }

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        _logger.LogInformation("TraderAnalyticsWorker starting...");

        while (!ct.IsCancellationRequested)
        {
            try
            {
                var processedCount = await RunAnalyticsAsync(ct);
                if (processedCount == 0)
                {
                    _logger.LogInformation("TraderAnalyticsWorker sleeping for 1 minute...");
                    await Task.Delay(TimeSpan.FromMinutes(1), ct);
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error in TraderAnalyticsWorker");
                await Task.Delay(TimeSpan.FromMinutes(1), ct);
            }
        }
    }

    private async Task<int> RunAnalyticsAsync(CancellationToken ct)
    {
        List<int> traderIds = new List<int>();
        Domain.Entities.BackgroundJob? activeJob = null;
        
        using (var scope = _services.CreateScope())
        {
            var jobRepo = scope.ServiceProvider.GetRequiredService<IJobRepository>();
            activeJob = await jobRepo.GetNextPendingJobAsync(Predictalytics.Domain.Enums.JobType.TraderAnalysis, ct);

            if (activeJob != null && activeJob.TraderId.HasValue)
            {
                traderIds.Add(activeJob.TraderId.Value);
                activeJob.Status = Predictalytics.Domain.Enums.JobStatus.InProgress;
                activeJob.StartedAt = DateTime.UtcNow;
                await jobRepo.UpdateAsync(activeJob, ct);
            }
            else
            {
                var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
                // Bug 5 Fix: Add 30-minute cooldown to prevent CPU looping.
                var cooldown = DateTime.UtcNow.AddMinutes(-30);
                
                // Find traders who have never been analyzed, or whose last analysis was before their latest trade.
                // Include Resolution-Trigger: Traders with open positions in resolved markets.
                traderIds = await db.Traders
                    .Where(t => 
                        t.LastAnalyzedAt == null || 
                        (t.LastAnalyzedAt < cooldown && t.LastTradesUpdatedAt != null && t.LastTradesUpdatedAt > t.LastAnalyzedAt) ||
                        (t.LastAnalyzedAt < cooldown && t.Positions.Any(p => p.SharesHeld > 0 && p.MarketOutcome != null && p.MarketOutcome.Market != null && p.MarketOutcome.Market.IsResolved))
                    )
                    .OrderBy(t => t.LastAnalyzedAt == null ? 0 : 1)
                    .ThenBy(t => t.LastAnalyzedAt)
                    .Select(t => t.Id)
                    .Take(500) // Limit batch size to prevent long-running loops without save
                    .ToListAsync(ct);
            }
        }

        if (traderIds.Count > 0)
        {
            _logger.LogInformation("Found {Count} active or unanalyzed traders to update", traderIds.Count);
        }

        async Task MarkJobFailedAsync(int traderId, Exception ex)
        {
            if (activeJob != null && activeJob.TraderId == traderId)
            {
                try
                {
                    using var jobScope = _services.CreateScope();
                    var updateJobRepo = jobScope.ServiceProvider.GetRequiredService<IJobRepository>();
                    activeJob.Status = Predictalytics.Domain.Enums.JobStatus.Failed;
                    activeJob.CompletedAt = DateTime.UtcNow;
                    activeJob.ErrorMessage = ex.Message;
                    await updateJobRepo.UpdateAsync(activeJob, CancellationToken.None);
                }
                catch { /* Ignore secondary errors */ }
            }
        }

        foreach (var id in traderIds)
        {
            if (ct.IsCancellationRequested) break;

            // Concurrent writers (reconciliation checkpoint resets, pollers) can touch the
            // same TraderPositions/Traders rows mid-save ("Record has changed since last
            // read"). A retry with a fresh context re-reads their changes and succeeds.
            const int maxAttempts = 2;
            for (int attempt = 1; attempt <= maxAttempts; attempt++)
            {
            try
            {
                using var traderScope = _services.CreateScope();
                var pnlEngine = traderScope.ServiceProvider.GetRequiredService<IPositionPnLEngine>();
                var traderRepo = traderScope.ServiceProvider.GetRequiredService<ITraderRepository>();
                
                var trader = await traderRepo.GetByIdAsync(id, ct);
                if (trader != null && trader.IngestMode != Predictalytics.Domain.Enums.IngestMode.SnapshotOnly)
                {
                    await pnlEngine.RecalculateTraderPositionsAsync(id, ct);
                }

                // Run CopytradingEstimator
                var db = traderScope.ServiceProvider.GetRequiredService<AppDbContext>();
                var estimator = traderScope.ServiceProvider.GetRequiredService<ICopytradingEstimator>();
                
                if (trader != null)
                {
                    int tradesCount = 0;
                    if (trader.IngestMode == Predictalytics.Domain.Enums.IngestMode.SnapshotOnly)
                    {
                        var analyticsObj = trader.Analytics ?? new Predictalytics.Domain.Entities.TraderAnalytics { TraderId = trader.Id };
                        analyticsObj.CopytradingScore = 0;
                        analyticsObj.CopytradingQualityScore = 0;
                        analyticsObj.CopytradingCopyabilityScore = 0;
                        trader.Analytics = analyticsObj;
                        
                        db.TraderTraits.RemoveRange(db.TraderTraits.Where(t => t.TraderId == id));
                        db.TraderTraits.Add(new Predictalytics.Domain.Entities.TraderTrait { TraderId = id, Trait = "not_copyable_hf", Value = 1 });
                        
                        // We still write daily snapshots below, so keep moving
                        await db.SaveChangesAsync(ct);
                    }
                    else
                    {
                        var trades = await db.Trades
                            .Include(t => t.MarketOutcome).ThenInclude(o => o.Market)
                            .Include(t => t.DbMarket)
                            .Include(t => t.Context)
                            .Where(t => t.TraderId == id && t.DbMarketId != null)
                            .OrderByDescending(t => t.ExecutedAt)
                            .Take(1000)
                            .ToListAsync(ct);

                        tradesCount = trades.Count;
                        if (trades.Count > 0)
                        {
                            var estScores = await estimator.CalculateScoresAsync(trader, trades, ct);
                            
                            var analyticsObj = trader.Analytics ?? new Predictalytics.Domain.Entities.TraderAnalytics { TraderId = trader.Id };
                            // Persist advanced copyability and quality scores derived from tape replay
                            analyticsObj.CopytradingScore = estScores.CombinedScore;
                            analyticsObj.CopytradingQualityScore = estScores.QualityScore;
                            analyticsObj.CopytradingCopyabilityScore = estScores.CopyabilityScore;
                            
                            trader.Analytics = analyticsObj;

                            var positions = await db.TraderPositions
                                .Include(p => p.MarketOutcome).ThenInclude(o => o.Market)
                                .Where(p => p.TraderId == id)
                                .ToListAsync(ct);

                            var computedTraits = Predictalytics.Application.Services.TraderTraitCalculator.Compute(trader, trades, positions);
                            
                            db.TraderTraits.RemoveRange(db.TraderTraits.Where(t => t.TraderId == id));
                            foreach (var (traitName, value) in computedTraits)
                            {
                                db.TraderTraits.Add(new Predictalytics.Domain.Entities.TraderTrait { TraderId = id, Trait = traitName, Value = value });
                            }

                            // Window Metrics
                            var window1End = DateTime.UtcNow;
                            var window1Start = window1End.AddDays(-60);
                            var window2End = window1Start;
                            var window2Start = window2End.AddDays(-120); // -180 to -60

                            db.TraderWindowMetrics.RemoveRange(db.TraderWindowMetrics.Where(w => w.TraderId == id));
                            
                            Predictalytics.Domain.Entities.TraderWindowMetrics CalcWindow(DateTime start, DateTime end)
                            {
                                var windowPositions = positions.Where(p => 
                                    p.MarketOutcome?.Market?.IsResolved == true &&
                                    p.MarketOutcome.Market.EndDate >= start &&
                                    p.MarketOutcome.Market.EndDate < end).ToList();
                                
                                int closedMarkets = windowPositions.Select(p => p.MarketOutcome!.MarketId).Distinct().Count();
                                
                                var winReturnPcts = new List<decimal>();
                                var lossReturnPcts = new List<decimal>();
                                int wonCount = 0;
                                
                                var marketGroups = windowPositions.GroupBy(p => p.MarketOutcome!.MarketId);
                                foreach(var mg in marketGroups)
                                {
                                    decimal realizedPnl = mg.Sum(p => p.RealizedPnl);
                                    // Approximate invested as total buy amount in window for this market
                                    decimal invested = trades.Where(t => t.DbMarketId == mg.Key && t.Side == Predictalytics.Domain.Enums.TradeSide.Buy && t.ExecutedAt >= start && t.ExecutedAt < end).Sum(t => t.Amount);
                                    
                                    if (invested > 0)
                                    {
                                        decimal returnPct = (realizedPnl / invested) * 100m;
                                        if (returnPct > 0) 
                                        { 
                                            winReturnPcts.Add(returnPct);
                                            wonCount++;
                                        }
                                        else if (returnPct < 0)
                                        {
                                            lossReturnPcts.Add(returnPct);
                                        }
                                    }
                                }

                                winReturnPcts.Sort();
                                lossReturnPcts.Sort();
                                
                                decimal medianWin = winReturnPcts.Count > 0 ? winReturnPcts[winReturnPcts.Count / 2] : 0;
                                decimal medianLoss = lossReturnPcts.Count > 0 ? lossReturnPcts[lossReturnPcts.Count / 2] : 0;
                                decimal avgReturn = winReturnPcts.Concat(lossReturnPcts).DefaultIfEmpty(0).Average();
                                decimal winRate = closedMarkets > 0 ? (decimal)wonCount / closedMarkets : 0;
                                decimal totalWins = winReturnPcts.Sum();
                                decimal totalLosses = Math.Abs(lossReturnPcts.Sum());
                                decimal? pf = totalLosses > 0 ? totalWins / totalLosses : (totalWins > 0 ? 999m : null);
                                
                                return new Predictalytics.Domain.Entities.TraderWindowMetrics
                                {
                                    TraderId = id,
                                    WindowStart = start,
                                    WindowEnd = end,
                                    ClosedMarkets = closedMarkets,
                                    WinRate = winRate,
                                    AvgReturnPct = avgReturn,
                                    MedianWinReturnPct = medianWin,
                                    MedianLossReturnPct = medianLoss,
                                    ProfitFactor = pf
                                };
                            }
                            
                            db.TraderWindowMetrics.Add(CalcWindow(window1Start, window1End));
                            db.TraderWindowMetrics.Add(CalcWindow(window2Start, window2End));

                            // Fingerprints
                            var buys = trades.Where(t => t.Side == Predictalytics.Domain.Enums.TradeSide.Buy).ToList();
                            
                            if (buys.Count > 0)
                            {
                                var bands = buys.GroupBy(b => Math.Floor(b.Price * 10m) / 10m)
                                    .Select(g => new { 
                                        Band = g.Key, 
                                        StakeShare = g.Sum(b => b.Amount) / buys.Sum(b => b.Amount),
                                        WinRate = g.Count(b => positions.Any(p => p.MarketOutcomeId == b.MarketOutcomeId && p.RealizedPnl > 0)) / (decimal)g.Count()
                                    }).ToList();
                                analyticsObj.PriceBandProfileJson = System.Text.Json.JsonSerializer.Serialize(bands);
                            }
                            
                            var holdDurations = new List<double>();
                            foreach(var mg in trades.GroupBy(t => t.DbMarketId))
                            {
                                var firstBuy = mg.Where(t => t.Side == Predictalytics.Domain.Enums.TradeSide.Buy).OrderBy(t => t.ExecutedAt).FirstOrDefault();
                                var lastExit = mg.Where(t => t.Side == Predictalytics.Domain.Enums.TradeSide.Sell || t.Side == Predictalytics.Domain.Enums.TradeSide.Redeem).OrderByDescending(t => t.ExecutedAt).FirstOrDefault();
                                if (firstBuy != null && lastExit != null && lastExit.ExecutedAt > firstBuy.ExecutedAt)
                                {
                                    holdDurations.Add((lastExit.ExecutedAt - firstBuy.ExecutedAt).TotalHours);
                                }
                            }
                            holdDurations.Sort();
                            analyticsObj.MedianHoldDurationHours = holdDurations.Count > 0 ? (decimal)holdDurations[holdDurations.Count / 2] : 0;
                            
                            var positionSizes = trades.GroupBy(t => t.DbMarketId).Select(g => g.Where(t => t.Side == Predictalytics.Domain.Enums.TradeSide.Buy).Sum(t => t.Amount)).Where(a => a > 0).ToList();
                            positionSizes.Sort();
                            analyticsObj.P50PositionSize = positionSizes.Count > 0 ? positionSizes[positionSizes.Count / 2] : 0;
                            analyticsObj.P90PositionSize = positionSizes.Count > 0 ? positionSizes[(int)(positionSizes.Count * 0.9)] : 0;
                            
                            var firstTradeObj = trades.OrderBy(t => t.ExecutedAt).FirstOrDefault();
                            var daysActiveVal = firstTradeObj != null ? (DateTime.UtcNow - firstTradeObj.ExecutedAt).TotalDays : 0;
                            analyticsObj.TradesPerWeek = daysActiveVal > 0 ? (decimal)(trades.Count / (daysActiveVal / 7.0)) : 0;

                            // Copyability Aggregates
                            var volumes = trades.Where(t => t.DbMarket != null && t.DbMarket.Volume > 0).Select(t => t.DbMarket!.Volume).ToList();
                            volumes.Sort();
                            analyticsObj.MedianMarketVolumeUsd = volumes.Count > 0 ? volumes[volumes.Count / 2] : 0;
                            
                            var drifts = trades.Where(t => t.Context != null && t.Context.PriceBefore1m > 0 && t.Context.PriceAfter1m.HasValue).Select(t => (t.Context!.PriceAfter1m!.Value - t.Context.PriceBefore1m!.Value) / t.Context.PriceBefore1m!.Value * 100m).ToList();
                            drifts.Sort();
                            analyticsObj.MedianPostFillDriftPct = drifts.Count > 0 ? drifts[drifts.Count / 2] : 0;
                            
                            analyticsObj.NetEdgeAfterFeesPct = analyticsObj.MedianWinReturnPct - 2.0m; // simple approximation

                            // Master Qualification Logic (E1/E2)
                            if (trader.IngestMode == Predictalytics.Domain.Enums.IngestMode.Full && trader.MasterStatus == Predictalytics.Domain.Enums.MasterStatus.None)
                            {
                                bool isMaster = true;
                                if (analyticsObj.OverallPnL < 500m) isMaster = false;
                                if (trades.Count(t => (DateTime.UtcNow - t.ExecutedAt).TotalDays <= 100) > 0)
                                {
                                    if (analyticsObj.OverallWinRate < 0.60m) isMaster = false;
                                }
                                if (daysActiveVal < 30) isMaster = false;
                                if (analyticsObj.Trades30d < 10) isMaster = false;
                                
                                if (isMaster)
                                {
                                    trader.MasterStatus = Predictalytics.Domain.Enums.MasterStatus.Candidate;
                                }
                            }

                            await db.SaveChangesAsync(ct);
                        }
                    }

                    // Only stamp if there were actually trades to analyze (or if SnapshotOnly where we skip trade fetch)
                    if (trader.IngestMode == Predictalytics.Domain.Enums.IngestMode.SnapshotOnly || tradesCount > 0)
                    {
                        trader.LastAnalyzedAt = DateTime.UtcNow;
                        await traderRepo.UpdateAsync(trader, ct);
                    }
                    else if (trader.LastAnalyzedAt == null)
                    {
                        // If it's a completely new trader with no trades, we still don't stamp LastAnalyzedAt
                        // so it remains null until the TradeHistoryWorker pulls trades.
                    }
                    
                    var statsService = traderScope.ServiceProvider.GetService<IPlatformStatisticsService>();
                    if (statsService != null)
                    {
                        statsService.TrackTradersAnalyzed(trader.Platform, 1);
                    }
                }
                
                if (activeJob != null && activeJob.TraderId == id)
                {
                    using var jobScope = _services.CreateScope();
                    var updateJobRepo = jobScope.ServiceProvider.GetRequiredService<IJobRepository>();
                    activeJob.Status = Predictalytics.Domain.Enums.JobStatus.Completed;
                    activeJob.CompletedAt = DateTime.UtcNow;
                    await updateJobRepo.UpdateAsync(activeJob, ct);
                }

                break; // trader done
            }
            catch (OperationCanceledException) when (ct.IsCancellationRequested)
            {
                _logger.LogInformation("Cancellation requested during analysis, stopping batch.");
                break;
            }
            catch (DbUpdateException ex) when (attempt < maxAttempts)
            {
                _logger.LogWarning("Concurrent modification while analyzing trader {TraderId} — retrying with a fresh context. ({Message})",
                    id, ex.InnerException?.Message ?? ex.Message);
                await Task.Delay(250, CancellationToken.None);
            }
            catch (DbUpdateException ex)
            {
                // Still colliding after the retry. LastAnalyzedAt stays unset, so this
                // trader is picked up again next cycle — a warning is enough.
                _logger.LogWarning("Trader {TraderId} skipped after repeated concurrent modifications — will retry next cycle. ({Message})",
                    id, ex.InnerException?.Message ?? ex.Message);
                await MarkJobFailedAsync(id, ex);
                break;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error recalculating positions/PnL for trader {TraderId}", id);
                await MarkJobFailedAsync(id, ex);
                break;
            }
            }

            if (ct.IsCancellationRequested) break;
        }

        if (traderIds.Count > 0)
        {
            _logger.LogInformation("Trader analytics update complete.");
        }
        
        return traderIds.Count;
    }
}
