using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.EntityFrameworkCore;
using Predictalytics.Application.DTOs;
using Predictalytics.Application.Services;
using Predictalytics.Domain.Enums;
using Predictalytics.Infrastructure.Data;

namespace Predictalytics.Api.Endpoints;

public static class CoMovementEndpoints
{
    private const string InsiderTrait = "possible_insider";
    private const decimal InformedMovePts = 0.15m;      // price rise that counts as a "big move"
    private static readonly TimeSpan InformedHorizon = TimeSpan.FromDays(14);

    public static void MapCoMovementEndpoints(this IEndpointRouteBuilder routes)
    {
        var group = routes.MapGroup("/api/co-movement").WithTags("CoMovement");

        // Smart-money co-movement graph (#1 inc. 2): cluster copy-relevant + insider wallets by shared
        // timely co-entries; weight each wallet by how often its entries preceded a big price move.
        group.MapGet("/graph", async (double? windowHours, int? minEdge, decimal? minScore,
            AppDbContext db, CancellationToken ct) =>
        {
            var buy = TradeSide.Buy;
            var minScoreV = minScore ?? 40m;

            var candidates = await db.Traders
                .Include(t => t.Analytics)
                .Include(t => t.Traits)
                .Where(t => !t.IsSuspectedBot && t.Analytics != null
                    && (t.Analytics.CopytradingScore >= minScoreV || t.Traits.Any(tr => tr.Trait == InsiderTrait)))
                .OrderByDescending(t => t.Analytics!.CopytradingScore)
                .Take(80)
                .ToListAsync(ct);

            if (candidates.Count < 2)
                return Results.Ok(new CoMovementGraphDto(System.Array.Empty<CoMovementNodeDto>(), System.Array.Empty<CoMovementEdgeDto>(), 0));

            var ids = candidates.Select(t => t.Id).ToList();
            var byId = candidates.ToDictionary(t => t.Id);

            var raw = await db.Trades
                .Where(t => ids.Contains(t.TraderId) && t.Side == buy && t.MarketOutcomeId != null)
                .OrderByDescending(t => t.ExecutedAt)
                .Select(t => new { t.TraderId, OutcomeId = t.MarketOutcomeId!.Value, t.ExecutedAt })
                .Take(12000)
                .ToListAsync(ct);

            var entries = raw.Select(r => new CandidateEntry(r.TraderId, r.OutcomeId, r.ExecutedAt)).ToList();
            var graph = CoMovementGraphBuilder.Build(entries, windowHours ?? 48, minEdge ?? 3);

            var nodeIds = graph.ClusterByTrader.Keys.ToHashSet();
            var informed = await ComputeInformedShareAsync(db, raw.Where(r => nodeIds.Contains(r.TraderId))
                .Select(r => (r.TraderId, r.OutcomeId, r.ExecutedAt)).ToList(), ct);

            var nodes = nodeIds.Select(id =>
            {
                var t = byId[id];
                return new CoMovementNodeDto(
                    id, t.DisplayName, t.Platform.ToString(),
                    t.Analytics?.CopytradingScore ?? 0,
                    graph.ClusterByTrader[id],
                    informed.GetValueOrDefault(id),
                    t.Traits.Any(tr => tr.Trait == InsiderTrait));
            }).ToList();

            var edges = graph.Edges.Select(e => new CoMovementEdgeDto(e.TraderA, e.TraderB, e.Weight)).ToList();
            var clusterCount = graph.ClusterByTrader.Values.Distinct().Count();

            return Results.Ok(new CoMovementGraphDto(nodes, edges, clusterCount));
        });
    }

    /// <summary>
    /// Per wallet: fraction of its entries (on outcomes we have stored price history for) that preceded
    /// a big favorable price move — the "informed" signal. Uses only stored snapshots; degrades to 0.
    /// </summary>
    private static async Task<Dictionary<int, double>> ComputeInformedShareAsync(
        AppDbContext db, IReadOnlyList<(int TraderId, int OutcomeId, DateTime At)> entries, CancellationToken ct)
    {
        var result = new Dictionary<int, double>();
        var outcomeIds = entries.Select(e => e.OutcomeId).Distinct().ToList();
        if (outcomeIds.Count == 0 || outcomeIds.Count > 2000) return result; // guard: keep it bounded

        var snaps = await db.MarketOutcomePriceSnapshots
            .Where(s => outcomeIds.Contains(s.MarketOutcomeId))
            .Select(s => new { s.MarketOutcomeId, s.Price, s.Timestamp })
            .ToListAsync(ct);

        var byOutcome = snaps
            .GroupBy(s => s.MarketOutcomeId)
            .ToDictionary(g => g.Key, g => g.OrderBy(x => x.Timestamp).Select(x => (x.Timestamp, x.Price)).ToList());

        foreach (var byTrader in entries.GroupBy(e => e.TraderId))
        {
            int total = 0, informedCount = 0;
            foreach (var e in byTrader)
            {
                if (!byOutcome.TryGetValue(e.OutcomeId, out var series) || series.Count == 0) continue;

                var priceAt = PriceAt(series, e.At);
                var priceLater = PriceAt(series, e.At + InformedHorizon);
                if (priceAt is null || priceLater is null) continue;

                total++;
                if (priceLater.Value - priceAt.Value >= InformedMovePts) informedCount++;
            }
            result[byTrader.Key] = total > 0 ? (double)informedCount / total : 0;
        }

        return result;

        // Nearest snapshot price at or before t; falls back to the earliest snapshot.
        static decimal? PriceAt(List<(DateTime Ts, decimal Price)> series, DateTime t)
        {
            decimal? best = null;
            foreach (var (ts, price) in series)
            {
                if (ts <= t) best = price;
                else break;
            }
            return best ?? (series.Count > 0 ? series[0].Price : null);
        }
    }
}
