namespace Predictalytics.Application.Services;

/// <summary>One market entry (a Buy) by the seed trader.</summary>
public sealed record CoEntry(int OutcomeId, DateTime At);

/// <summary>One market entry (a Buy) by a candidate wallet.</summary>
public sealed record CandidateEntry(int TraderId, int OutcomeId, DateTime At);

/// <summary>A wallet that repeatedly enters the same outcomes as the seed, around the same time.</summary>
public sealed record CoMovingWallet(int TraderId, int SharedMarkets, double AvgLeadHours);

/// <summary>
/// Smart-money co-movement (#1): given the seed trader's entries and other wallets' entries into the
/// same outcomes, rank the wallets that repeatedly co-enter within a time window. A positive
/// <see cref="CoMovingWallet.AvgLeadHours"/> means the wallet tends to move BEFORE the seed — the
/// signal that surfaces informed traders. Pure — no DB access, fully unit-tested.
/// </summary>
public static class CoMovementCalculator
{
    public static IReadOnlyList<CoMovingWallet> Rank(
        IReadOnlyList<CoEntry> seedEntries,
        IReadOnlyList<CandidateEntry> candidateEntries,
        double windowHours = 48,
        int minSharedMarkets = 3)
    {
        // Seed entry times per outcome (a trader may enter the same outcome more than once).
        var seedByOutcome = seedEntries
            .GroupBy(e => e.OutcomeId)
            .ToDictionary(g => g.Key, g => g.Select(e => e.At).OrderBy(t => t).ToList());

        var window = TimeSpan.FromHours(windowHours);
        var perTrader = new Dictionary<int, (HashSet<int> Markets, double LeadSumHours, int Matches)>();

        foreach (var cand in candidateEntries)
        {
            if (!seedByOutcome.TryGetValue(cand.OutcomeId, out var seedTimes)) continue;

            // Nearest seed entry on the same outcome; count only if within the window.
            DateTime? nearest = null;
            var bestGap = window;
            foreach (var st in seedTimes)
            {
                var gap = (st - cand.At).Duration();
                if (gap <= bestGap)
                {
                    bestGap = gap;
                    nearest = st;
                }
            }
            if (nearest is null) continue;

            if (!perTrader.TryGetValue(cand.TraderId, out var acc))
                acc = (new HashSet<int>(), 0d, 0);

            acc.Markets.Add(cand.OutcomeId);
            // Positive lead = candidate entered before the seed.
            acc.LeadSumHours += (nearest.Value - cand.At).TotalHours;
            acc.Matches += 1;
            perTrader[cand.TraderId] = acc;
        }

        return perTrader
            .Where(kv => kv.Value.Markets.Count >= minSharedMarkets)
            .Select(kv => new CoMovingWallet(
                kv.Key,
                kv.Value.Markets.Count,
                kv.Value.Matches > 0 ? Math.Round(kv.Value.LeadSumHours / kv.Value.Matches, 1) : 0))
            .OrderByDescending(w => w.SharedMarkets)
            .ThenByDescending(w => w.AvgLeadHours)
            .ToList();
    }
}
