namespace Predictalytics.Application.Services;

/// <summary>An undirected co-movement edge: two wallets that co-entered <see cref="Weight"/> shared outcomes.</summary>
public sealed record CoMovementEdge(int TraderA, int TraderB, int Weight);

/// <summary>The co-movement graph: edges plus the cluster each connected wallet belongs to.</summary>
public sealed record CoMovementGraph(
    IReadOnlyList<CoMovementEdge> Edges,
    IReadOnlyDictionary<int, int> ClusterByTrader);

/// <summary>
/// Builds the smart-money co-movement graph (#1, increment 2): wallets are linked when they repeatedly
/// enter the SAME outcomes within a time window, and the connected components of that graph are the
/// "smart-money clusters". Pure — no DB access — and fully unit-tested. Clustering is union-find over
/// edges that meet the minimum weight.
/// </summary>
public static class CoMovementGraphBuilder
{
    public static CoMovementGraph Build(
        IReadOnlyList<CandidateEntry> entries,
        double windowHours = 48,
        int minEdgeWeight = 3)
    {
        var window = TimeSpan.FromHours(windowHours);

        // Accumulate, per unordered wallet pair, the DISTINCT outcomes they co-entered within the window.
        var pairOutcomes = new Dictionary<(int, int), HashSet<int>>();

        foreach (var group in entries.GroupBy(e => e.OutcomeId))
        {
            var list = group.ToList();
            for (int i = 0; i < list.Count; i++)
            {
                for (int j = i + 1; j < list.Count; j++)
                {
                    if (list[i].TraderId == list[j].TraderId) continue;
                    if ((list[i].At - list[j].At).Duration() > window) continue;

                    var a = Math.Min(list[i].TraderId, list[j].TraderId);
                    var b = Math.Max(list[i].TraderId, list[j].TraderId);
                    if (!pairOutcomes.TryGetValue((a, b), out var set))
                    {
                        set = new HashSet<int>();
                        pairOutcomes[(a, b)] = set;
                    }
                    set.Add(group.Key);
                }
            }
        }

        var edges = pairOutcomes
            .Where(kv => kv.Value.Count >= minEdgeWeight)
            .Select(kv => new CoMovementEdge(kv.Key.Item1, kv.Key.Item2, kv.Value.Count))
            .OrderByDescending(e => e.Weight)
            .ToList();

        var clusterByTrader = ClusterConnectedComponents(edges);
        return new CoMovementGraph(edges, clusterByTrader);
    }

    /// <summary>Union-find: assign each wallet in an edge a cluster id (the smallest trader id in its component).</summary>
    private static Dictionary<int, int> ClusterConnectedComponents(IReadOnlyList<CoMovementEdge> edges)
    {
        var parent = new Dictionary<int, int>();

        int Find(int x)
        {
            parent.TryAdd(x, x);
            while (parent[x] != x)
            {
                parent[x] = parent[parent[x]]; // path halving
                x = parent[x];
            }
            return x;
        }

        void Union(int a, int b)
        {
            var ra = Find(a);
            var rb = Find(b);
            if (ra == rb) return;
            // Keep the smaller id as the root so cluster ids are stable/deterministic.
            if (ra < rb) parent[rb] = ra; else parent[ra] = rb;
        }

        foreach (var e in edges)
            Union(e.TraderA, e.TraderB);

        return parent.Keys.ToDictionary(t => t, Find);
    }
}
