using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Predictalytics.Application.Interfaces;
using Predictalytics.Application.Services;
using Predictalytics.Domain.Entities;
using Predictalytics.Infrastructure.Data;
using System.Text.Json;

namespace Predictalytics.Infrastructure.Services;

/// <summary>
/// Writes the strategy-fingerprint time series (<see cref="TraderFingerprintSnapshot"/>) from the
/// current persisted analytics, and reads drift off that history. Capture is scoped to copy-relevant
/// traders and rate-limited to about one row per trader per day so the table stays bounded.
/// </summary>
public class FingerprintSnapshotService : IFingerprintSnapshotService
{
    private readonly AppDbContext _db;
    private readonly ILogger<FingerprintSnapshotService> _logger;

    /// <summary>Only snapshot traders worth monitoring for drift (i.e. plausible copy candidates).</summary>
    private const decimal MinCopytradingScore = 40m;

    /// <summary>Don't capture more than one snapshot per trader within this window.</summary>
    private static readonly TimeSpan MinCaptureInterval = TimeSpan.FromHours(20);

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

    public async Task<int> CaptureDueAsync(CancellationToken ct = default)
    {
        var now = DateTime.UtcNow;
        var cutoff = now - MinCaptureInterval;

        // Traders already snapshotted within the window are skipped.
        var recentlyCaptured = await _db.TraderFingerprintSnapshots
            .Where(s => s.CapturedAt >= cutoff)
            .Select(s => s.TraderId)
            .Distinct()
            .ToListAsync(ct);
        var skip = recentlyCaptured.ToHashSet();

        var candidates = await _db.Traders
            .Include(t => t.Analytics)
            .Include(t => t.Traits)
            .Include(t => t.CategoryPerformances)
            .Where(t => t.Analytics != null && t.Analytics.CopytradingScore >= MinCopytradingScore)
            .ToListAsync(ct);

        int captured = 0;
        foreach (var trader in candidates)
        {
            if (skip.Contains(trader.Id)) continue;
            var a = trader.Analytics!;

            _db.TraderFingerprintSnapshots.Add(new TraderFingerprintSnapshot
            {
                TraderId = trader.Id,
                CapturedAt = now,
                CopytradingScore = a.CopytradingScore,
                CategoryConcentration = a.CategoryConcentration,
                ConvictionEdgePct = a.ConvictionEdgePct,
                P50PositionSize = a.P50PositionSize,
                P90PositionSize = a.P90PositionSize,
                MedianHoldDurationHours = a.MedianHoldDurationHours,
                TradesPerWeek = a.TradesPerWeek,
                CategoryMixJson = BuildCategoryMixJson(trader),
                TraitSetJson = JsonSerializer.Serialize(trader.Traits.Select(tr => tr.Trait).OrderBy(x => x).ToList())
            });
            captured++;
        }

        if (captured > 0)
        {
            await _db.SaveChangesAsync(ct);
            _logger.LogInformation("📸 Fingerprint snapshots captured for {Count} traders", captured);
        }

        return captured;
    }

    public async Task<FingerprintDriftResult?> GetDriftAsync(int traderId, int baselineDays = 14, CancellationToken ct = default)
    {
        var latest = await _db.TraderFingerprintSnapshots
            .Where(s => s.TraderId == traderId)
            .OrderByDescending(s => s.CapturedAt)
            .FirstOrDefaultAsync(ct);
        if (latest == null) return null;

        var baselineCutoff = latest.CapturedAt.AddDays(-baselineDays);
        var baseline = await _db.TraderFingerprintSnapshots
            .Where(s => s.TraderId == traderId && s.CapturedAt <= baselineCutoff)
            .OrderByDescending(s => s.CapturedAt)
            .FirstOrDefaultAsync(ct);
        if (baseline == null || baseline.Id == latest.Id) return null;

        return FingerprintDriftCalculator.Compare(baseline, latest);
    }

    public async Task<IReadOnlyList<(int TraderId, FingerprintDriftResult Drift)>> GetDriftedTradersAsync(int baselineDays = 14, CancellationToken ct = default)
    {
        // Candidates = traders that already have a snapshot old enough to be a baseline.
        var cutoff = DateTime.UtcNow.AddDays(-baselineDays);
        var candidateIds = await _db.TraderFingerprintSnapshots
            .Where(s => s.CapturedAt <= cutoff)
            .Select(s => s.TraderId)
            .Distinct()
            .ToListAsync(ct);

        var results = new List<(int, FingerprintDriftResult)>();
        foreach (var id in candidateIds)
        {
            var drift = await GetDriftAsync(id, baselineDays, ct);
            if (drift is { HasDrifted: true })
                results.Add((id, drift));
        }
        return results;
    }

    private static string? BuildCategoryMixJson(Trader trader)
    {
        var perfs = trader.CategoryPerformances;
        if (perfs == null || perfs.Count == 0) return null;

        var total = perfs.Sum(p => p.TotalVolume);
        if (total <= 0) return null;

        var mix = perfs.ToDictionary(p => p.Category.ToString(), p => p.TotalVolume / total);
        return JsonSerializer.Serialize(mix);
    }
}
