using Predictalytics.Application.DTOs;
using Predictalytics.Application.Interfaces;
using Predictalytics.Domain.Entities;
using Predictalytics.Domain.Enums;
using Predictalytics.Domain.Interfaces;
using Predictalytics.Domain.ValueObjects;
using Microsoft.Extensions.Logging;

namespace Predictalytics.Application.Services;

public class AnalyticsService : IAnalyticsService
{
    private readonly ITraderRepository _traderRepo;
    private readonly ITradeRepository _tradeRepo;
    private readonly IAlertRepository _alertRepo;
    private readonly IWatchlistRepository _watchlistRepo;
    private readonly IMarketRepository _marketRepo;
    private readonly IDiscoveryService _discovery;
    private readonly IEnumerable<IPlatformProvider> _providers;
    private readonly IPositionPnLEngine _pnlEngine;
    private readonly ILogger<AnalyticsService> _logger;

    public AnalyticsService(ITraderRepository traderRepo, ITradeRepository tradeRepo,
        IAlertRepository alertRepo, IWatchlistRepository watchlistRepo, IMarketRepository marketRepo, 
        IDiscoveryService discovery, IEnumerable<IPlatformProvider> providers, IPositionPnLEngine pnlEngine, ILogger<AnalyticsService> logger)
    {
        _traderRepo = traderRepo;
        _tradeRepo = tradeRepo;
        _alertRepo = alertRepo;
        _watchlistRepo = watchlistRepo;
        _marketRepo = marketRepo;
        _discovery = discovery;
        _providers = providers;
        _pnlEngine = pnlEngine;
        _logger = logger;
    }

    public async Task<DashboardDto> GetDashboardAsync(CancellationToken ct = default)
    {
        var totalTraders = await _traderRepo.GetCountAsync(ct: ct);
        var totalTrades = await _tradeRepo.GetCountAsync(ct: ct);
        var volume24h = await _tradeRepo.GetTotalVolumeAsync(DateTime.UtcNow.AddHours(-24), ct);
        var unreadAlerts = await _alertRepo.GetUnreadCountAsync(ct);
        var watchlist = await _watchlistRepo.GetAllAsync(ct);
        
        // Requirements: 
        // "Top Traders": successful 5 traders by PnL in the last 7 days.
        var topTraders = await _traderRepo.GetTopByPnLAsync(5, DateTime.UtcNow.AddDays(-7), ct);
        
        // "Recent Trades": 5 largest trades in the last 24h.
        var largestTrades = await _tradeRepo.GetLargestAsync(5, DateTime.UtcNow.AddHours(-24), ct);
        
        var recentTradesForActivity = await _tradeRepo.GetRecentAsync(500, ct: ct);
        var activeTraders24h = recentTradesForActivity.Where(t => t.ExecutedAt > DateTime.UtcNow.AddHours(-24))
            .Select(t => t.TraderId).Distinct().Count();
        
        var watchlistIds = watchlist.Select(w => w.TraderId).ToHashSet();
        var topTraderDtos = topTraders.Select(t => MapTraderDto(t, watchlistIds)).ToList();
        var largestTradeDtos = largestTrades.Select(MapTradeDto).ToList();
        
        var alerts = await _alertRepo.GetRecentAsync(10, ct: ct);
        var alertDtos = alerts.Select(a => new AlertDto(a.Id, a.Type.ToString(), a.Platform.ToString(),
            a.Title, a.Message, a.Severity, a.IsRead, a.CreatedAt, null)).ToList();
            
        var allTraders = await _traderRepo.GetAllAsync(take: 1000, ct: ct); // Reduced from 10000 for perf
        var traderCounts = allTraders.GroupBy(t => t.Platform.ToString()).ToDictionary(g => g.Key, g => g.Count());
        var volumeCounts = recentTradesForActivity.Where(t => t.ExecutedAt > DateTime.UtcNow.AddHours(-24))
            .GroupBy(t => t.Platform.ToString()).ToDictionary(g => g.Key, g => g.Sum(t => t.Amount));

        return new DashboardDto(totalTraders, activeTraders24h, totalTrades, volume24h,
            unreadAlerts, watchlist.Count, topTraderDtos, largestTradeDtos, alertDtos,
            new PlatformBreakdownDto(traderCounts, volumeCounts));
    }

    public async Task<TraderDeepDiveDto?> GetTraderDeepDiveAsync(int traderId, CancellationToken ct = default)
    {
        var trader = await _traderRepo.GetByIdAsync(traderId, ct);
        if (trader == null) return null;
        var trades = await _tradeRepo.GetByTraderIdAsync(traderId, 0, 500, ct);

        // Fetch or load price snapshots for outcomes
        var outcomeIds = trades
            .Where(t => t.MarketOutcomeId.HasValue)
            .Select(t => t.MarketOutcomeId!.Value)
            .Distinct()
            .ToList();

        var snapshotsByOutcome = new Dictionary<int, List<MarketOutcomePriceSnapshot>>();

        foreach (var outcomeId in outcomeIds)
        {
            var existing = await _marketRepo.GetPriceSnapshotsAsync(outcomeId, ct);
            
            var needsFetch = existing.Count == 0;
            if (existing.Count > 0)
            {
                var latestSnapshot = existing.MaxBy(ps => ps.Timestamp);
                var firstTradeWithOutcome = trades.FirstOrDefault(t => t.MarketOutcomeId == outcomeId && t.MarketOutcome != null);
                var isResolved = firstTradeWithOutcome?.MarketOutcome?.Market?.IsResolved ?? false;
                if (!isResolved && (DateTime.UtcNow - (latestSnapshot?.Timestamp ?? DateTime.MinValue)).TotalHours >= 24)
                {
                    needsFetch = true;
                }
            }

            if (needsFetch)
            {
                var firstTradeWithOutcome = trades.FirstOrDefault(t => t.MarketOutcomeId == outcomeId && t.MarketOutcome != null);
                if (firstTradeWithOutcome?.MarketOutcome != null)
                {
                    var outcome = firstTradeWithOutcome.MarketOutcome;
                    var provider = _providers.FirstOrDefault(p => p.Platform == firstTradeWithOutcome.Platform);
                    if (provider != null && !string.IsNullOrEmpty(outcome.TokenId))
                    {
                        try
                        {
                            var fetched = await provider.GetPriceHistoryAsync(outcome.TokenId, ct);
                            if (fetched.Count > 0)
                            {
                                foreach (var f in fetched)
                                {
                                    f.MarketOutcomeId = outcomeId;
                                }
                                await _marketRepo.SavePriceSnapshotsAsync(outcomeId, fetched, ct);
                                existing = fetched;
                            }
                        }
                        catch (Exception ex)
                        {
                            _logger.LogError(ex, "Failed to fetch price history for outcome {OutcomeId}", outcomeId);
                        }
                    }
                }
            }

            snapshotsByOutcome[outcomeId] = existing.ToList();
        }

        var analysis = PerformDeepDive(trader, trades, snapshotsByOutcome);
        var tradeDtos = trades.Take(100).Select(MapTradeDto).ToList();
        return new TraderDeepDiveDto(traderId, trader.DisplayName, trader.Platform,
            analysis.ClassifiedStrategy, analysis.IsSuspectedBot, analysis.AvgHoldDurationHours,
            analysis.AvgPositionSizeUsd, analysis.MarketsTraded, analysis.HedgingFrequency,
            analysis.TimingAccuracy, analysis.EntryQuality, analysis.ExitQuality,
            analysis.BotIndicators, analysis.Summary, tradeDtos);
    }

    public async Task<IReadOnlyList<TraderPositionDto>> GetTraderPositionsAsync(int traderId, CancellationToken ct = default)
    {
        var positions = await _traderRepo.GetPositionsAsync(traderId, ct);
        return positions.Select(p => new TraderPositionDto(
            p.MarketOutcome?.Market?.ConditionId ?? p.MarketOutcomeId.ToString(),
            p.MarketOutcome?.Market?.Question,
            p.MarketOutcome?.Market?.Category.ToString(),
            p.MarketOutcome?.TokenId,
            p.SharesHeld,
            p.AvgCost,
            p.RealizedPnl,
            p.SharesHeld > 0 && p.MarketOutcome != null ? p.SharesHeld * (p.MarketOutcome.CurrentPrice - p.AvgCost) : 0,
            p.MarketOutcome?.CurrentPrice ?? 0,
            p.LastTradeExecutedAt
        )).ToList();
    }

    public async Task<IReadOnlyList<TraderDto>> GetTradersAsync(int skip = 0, int take = 50, string? platform = null, bool highlyCopyable = false, string? traitFilter = null, CancellationToken ct = default)
    {
        PlatformType? pType = null;
        if (!string.IsNullOrEmpty(platform) && platform != "All" && Enum.TryParse<PlatformType>(platform, true, out var pt))
            pType = pt;

        var traders = await _traderRepo.GetAllAsync(platform: pType, skip: 0, take: 5000, ct: ct); // Get more to filter in memory
        
        if (highlyCopyable)
        {
            traders = traders.Where(t => t.Analytics != null && t.Analytics.CopytradingScore >= 60).ToList();
        }

        if (!string.IsNullOrEmpty(traitFilter))
        {
            traders = traders.Where(t => t.Traits.Any(tr => tr.Trait == traitFilter)).ToList();
        }

        traders = traders.Skip(skip).Take(take).ToList();

        var watchlist = await _watchlistRepo.GetAllAsync(ct);
        var wIds = watchlist.Select(w => w.TraderId).ToHashSet();
        return traders.Select(t => MapTraderDto(t, wIds)).ToList();
    }

    public Task<IReadOnlyList<string>> GetTraitsAsync(CancellationToken ct = default)
    {
        // Actually this is unused since we mapped it straight to db context in the endpoint, 
        // but required by interface.
        return Task.FromResult<IReadOnlyList<string>>(new List<string>());
    }

    public async Task<IReadOnlyList<MarketDto>> GetMarketsAsync(int skip = 0, int take = 50, string? platform = null, string? category = null, string? query = null, CancellationToken ct = default)
    {
        // NOTE: Currently IMarketRepository.GetActiveAsync doesn't support pagination/filtering. 
        // We will fetch all and filter in memory for now, or you can update repository.
        // Let's use GetActiveAsync and map it.
        var markets = await _marketRepo.GetActiveAsync(1000, ct);
        
        PlatformType? pType = null;
        if (!string.IsNullOrEmpty(platform) && platform != "All" && Enum.TryParse<PlatformType>(platform, true, out var pt))
            pType = pt;

        var q = markets.AsEnumerable();
        if (pType.HasValue)
            q = q.Where(m => m.Platform == pType.Value);
            
        if (!string.IsNullOrEmpty(category) && category != "All" && Enum.TryParse<MarketCategory>(category, true, out var cat))
            q = q.Where(m => m.Category == cat);
            
        if (!string.IsNullOrEmpty(query))
            q = q.Where(m => m.Question.Contains(query, StringComparison.OrdinalIgnoreCase) || m.ConditionId.Contains(query));

        var result = q.Skip(skip).Take(take).Select(m => new MarketDto
        {
            Id = m.Id,
            Platform = m.Platform.ToString(),
            Question = m.Question,
            Volume = (double)m.Volume,
            Liquidity = (double)m.Liquidity,
            EndDate = m.EndDate,
            IsResolved = m.IsResolved
        }).ToList();

        return result;
    }

    public async Task<TraderDetailDto?> GetTraderDetailAsync(int traderId, CancellationToken ct = default)
    {
        var trader = await _traderRepo.GetByIdAsync(traderId, ct);
        if (trader == null) return null;
        var trades = await _tradeRepo.GetByTraderIdAsync(traderId, 0, 50, ct);
        var wl = await _watchlistRepo.GetByTraderIdAsync(traderId, ct);
        var s = trader.CurrentScore;
        var a = trader.Analytics;
        var perfs = trader.CategoryPerformances.Select(p => new TraderCategoryPerformanceDto(
            p.Category.ToString(),
            p.TotalVolume,
            p.TotalPnL,
            p.TotalTrades,
            p.WinningTrades,
            p.WinRate,
            p.AvgReturnPct)).ToList();

        return new TraderDetailDto(trader.Id, trader.Platform.ToString(), trader.PlatformUserId, trader.DisplayName,
            trader.Notes, trader.Tier.ToString(), trader.Strategy.ToString(), trader.IsSuspectedBot, trader.ManualPriorityOverride,
            trader.WinRate, trader.TotalPnl, trader.TotalTrades,
            a?.Trades30d ?? 0, a?.PnL30d ?? 0, a?.WinRate30d ?? 0,
            a?.PnL7d ?? 0, a?.WinRate7d ?? 0, a?.PnL24h ?? 0, a?.WinRate24h ?? 0,
            a?.EstimatedBankroll ?? 0, a?.CurrentBalance ?? 0,
            s?.ActivityScore ?? 0, s?.QualityScore ?? 0, s?.VolumeScore ?? 0, s?.TimingScore ?? 0,
            s?.CombinedScore ?? 0, a?.CopytradingScore ?? 0, a?.CopytradingQualityScore ?? 0, a?.CopytradingCopyabilityScore ?? 0, 
            a?.MedianWinReturnPct ?? 0, a?.AvgWinReturnPct ?? 0, a?.MedianLossReturnPct ?? 0, a?.AvgLossReturnPct ?? 0, a?.ProfitFactor,
            s?.Rank ?? 0, wl != null, trader.CreatedAt, trader.LastPolledAt,
            trader.AiStrategySummary,
            trades.Select(MapTradeDto).ToList(),
            perfs,
            trader.Traits.Select(t => new TraderTraitDto(t.Trait, t.Value)).ToList());
    }

    public async Task<MarketDetailDto?> GetMarketDetailAsync(int marketId, CancellationToken ct = default)
    {
        var market = await _marketRepo.GetByIdAsync(marketId, ct);
        if (market == null) return null;

        // Prefer fast INT FK lookup; fall back to string scan for legacy unlinked trades
        var recentTrades = await _tradeRepo.GetByDbMarketIdAsync(marketId, 0, 50, ct);
        if (recentTrades.Count == 0)
        {
            // Fallback: trades ingested before DbMarketId backfill
            recentTrades = await _tradeRepo.GetByMarketIdAsync(market.ConditionId, 0, 50, ct);
        }

        decimal botScore = 0;
        int uniqueTraders = 0;
        decimal avgTradeSize = 0;

        if (recentTrades.Count > 0)
        {
            uniqueTraders = recentTrades.Select(t => t.TraderId).Distinct().Count();
            avgTradeSize = recentTrades.Average(t => t.Size);
            var botTrades = recentTrades.Count(t => t.Trader != null && t.Trader.IsSuspectedBot);
            botScore = (decimal)botTrades / recentTrades.Count * 100;
        }

        return new MarketDetailDto
        {
            Id = market.Id,
            Platform = market.Platform.ToString(),
            PlatformMarketId = market.ConditionId,
            Question = market.Question,
            Description = market.Description,
            Category = market.Category.ToString(),
            Subcategory = market.Subcategory,
            Volume = (double)market.Volume,
            Liquidity = (double)market.Liquidity,
            EndDate = market.EndDate,
            IsResolved = market.IsResolved,
            ResolutionOutcome = market.ResolutionOutcome,
            ImageUrl = market.ImageUrl,
            BotActivityScore = market.Analytics?.BotActivityScore ?? botScore,
            UniqueTradersCount = market.Analytics?.UniqueTradersCount ?? uniqueTraders,
            AverageTradeSize = market.Analytics?.AverageTradeSize ?? avgTradeSize,
            Outcomes = market.Outcomes.Select(o => new MarketOutcomeDto { Name = o.Label, Price = (double)o.CurrentPrice }).ToList(),
            RecentTrades = recentTrades.Select(MapTradeDto).ToList()
        };
    }

    public async Task<SearchResultsDto> SearchAsync(string query, CancellationToken ct = default)
    {
        var traders = await _traderRepo.SearchAsync(query, 20, ct);
        var markets = await _marketRepo.SearchAsync(query, 20, ct);

        var watchlist = await _watchlistRepo.GetAllAsync(ct);
        var wIds = watchlist.Select(w => w.TraderId).ToHashSet();

        return new SearchResultsDto
        {
            Traders = traders.Select(t => MapTraderDto(t, wIds)).ToList(),
            Markets = markets.Select(m => new MarketDto
            {
                Id = m.Id,
                Platform = m.Platform.ToString(),
                Question = m.Question,
                Volume = (double)m.Volume,
                Liquidity = (double)m.Liquidity,
                EndDate = m.EndDate,
                IsResolved = m.IsResolved
            }).ToList()
        };
    }

    private TraderAnalysis PerformDeepDive(
        Trader trader, 
        IReadOnlyList<Trade> trades, 
        Dictionary<int, List<MarketOutcomePriceSnapshot>> snapshotsByOutcome)
    {
        if (trades.Count == 0)
            return new TraderAnalysis(trader.Id, StrategyType.Unknown, false, 0, 0, 0, 0, 50, 50, 50,
                Array.Empty<string>(), "Insufficient data.");

        // Use DbMarketId when available, fall back to MarketId string for older trades
        var marketKeys = trades
            .Select(t => t.DbMarketId.HasValue ? t.DbMarketId.Value.ToString() : t.MarketId)
            .Where(k => !string.IsNullOrEmpty(k))
            .ToList();

        var marketsTraded = marketKeys.Distinct().Count();
        var avgSize = trades.Average(t => t.Amount);
        var botIndicators = new List<string>();
        var times = trades.Select(t => t.ExecutedAt).OrderBy(t => t).ToList();
        if (times.Count > 10)
        {
            var intervals = times.Zip(times.Skip(1), (a, b) => (b - a).TotalSeconds).ToList();
            if (intervals.Average() < 10) botIndicators.Add("Sub-10s trade frequency");
        }

        // Group by unified market key to find hedged markets (both Yes and No held)
        var hedgeGroups = trades
            .GroupBy(t => t.DbMarketId.HasValue ? t.DbMarketId.Value.ToString() : t.MarketId)
            .Where(g => !string.IsNullOrEmpty(g.Key) && g.Select(t => t.Outcome).Distinct().Count() > 1);

        var hedgingRate = marketsTraded > 0 ? (decimal)hedgeGroups.Count() / marketsTraded * 100 : 0;
        var strategy = avgSize > 10000 ? StrategyType.Whale : hedgingRate > 30 ? StrategyType.Hedger :
            botIndicators.Count > 0 ? StrategyType.Bot : StrategyType.Unknown;

        // Calculate holding duration
        var holdDuration = (decimal)CalculateAvgHoldDuration(trades.OrderBy(t => t.ExecutedAt).ToList());

        // Calculate Entry and Exit qualities
        var entryQualities = new List<decimal>();
        var exitQualities = new List<decimal>();

        foreach (var trade in trades)
        {
            if (trade.MarketOutcomeId == null) continue;
            var outcomeId = trade.MarketOutcomeId.Value;

            if (snapshotsByOutcome.TryGetValue(outcomeId, out var snapshots) && snapshots.Count > 0)
            {
                var subsequent = snapshots
                    .Where(ps => ps.Timestamp > trade.ExecutedAt && ps.Timestamp <= trade.ExecutedAt.AddDays(7))
                    .ToList();

                if (subsequent.Count > 0)
                {
                    var avgSubsequentPrice = subsequent.Average(ps => ps.Price);
                    if (trade.Side == TradeSide.Buy)
                    {
                        var entryQuality = 50m + ((avgSubsequentPrice - trade.Price) / Math.Max(trade.Price, 0.01m)) * 100m;
                        entryQualities.Add(Math.Clamp(entryQuality, 0m, 100m));
                    }
                    else if (trade.Side == TradeSide.Sell)
                    {
                        var exitQuality = 50m + ((trade.Price - avgSubsequentPrice) / Math.Max(trade.Price, 0.01m)) * 100m;
                        exitQualities.Add(Math.Clamp(exitQuality, 0m, 100m));
                    }
                }
            }
        }

        var finalEntryQuality = entryQualities.Count > 0 ? Math.Round(entryQualities.Average(), 2) : 50m;
        var finalExitQuality = exitQualities.Count > 0 ? Math.Round(exitQualities.Average(), 2) : 50m;
        
        decimal finalTimingAccuracy;
        if (entryQualities.Count > 0 && exitQualities.Count > 0)
        {
            finalTimingAccuracy = Math.Round((entryQualities.Average() + exitQualities.Average()) / 2m, 2);
        }
        else if (entryQualities.Count > 0)
        {
            finalTimingAccuracy = Math.Round(entryQualities.Average(), 2);
        }
        else if (exitQualities.Count > 0)
        {
            finalTimingAccuracy = Math.Round(exitQualities.Average(), 2);
        }
        else
        {
            finalTimingAccuracy = 50m;
        }

        return new TraderAnalysis(trader.Id, strategy, botIndicators.Count > 1, holdDuration, avgSize, marketsTraded,
            hedgingRate, finalTimingAccuracy, finalEntryQuality, finalExitQuality, botIndicators.ToArray(),
            $"{trader.DisplayName}: {strategy}, {marketsTraded} markets, avg ${avgSize:N0}");
    }

    private static double CalculateAvgHoldDuration(List<Trade> trades)
    {
        var outcomeBuys = new Dictionary<int, List<(DateTime ExecutedAt, decimal Size)>>();
        double totalWeightedHours = 0;
        decimal totalMatchedSize = 0;

        foreach (var t in trades)
        {
            if (t.MarketOutcomeId == null) continue;
            var oid = t.MarketOutcomeId.Value;

            if (t.Side == TradeSide.Buy)
            {
                if (!outcomeBuys.TryGetValue(oid, out var list))
                {
                    list = new List<(DateTime, decimal)>();
                    outcomeBuys[oid] = list;
                }
                list.Add((t.ExecutedAt, t.Size));
            }
            else if (t.Side == TradeSide.Sell || t.Side == TradeSide.Redeem)
            {
                if (outcomeBuys.TryGetValue(oid, out var list) && list.Count > 0)
                {
                    var sellSizeRemaining = t.Size;
                    while (sellSizeRemaining > 0 && list.Count > 0)
                    {
                        var buy = list[0];
                        var matchedSize = Math.Min(sellSizeRemaining, buy.Size);
                        var hours = (t.ExecutedAt - buy.ExecutedAt).TotalHours;
                        if (hours < 0) hours = 0;

                        totalWeightedHours += hours * (double)matchedSize;
                        totalMatchedSize += matchedSize;

                        sellSizeRemaining -= matchedSize;
                        if (matchedSize >= buy.Size)
                        {
                            list.RemoveAt(0);
                        }
                        else
                        {
                            list[0] = (buy.ExecutedAt, buy.Size - matchedSize);
                        }
                    }
                }
            }
        }

        return totalMatchedSize > 0 ? totalWeightedHours / (double)totalMatchedSize : 0;
    }

    public async Task TriggerTradeSyncAsync(int traderId, CancellationToken ct = default)
    {
        var trader = await _traderRepo.GetByIdAsync(traderId, ct);
        if (trader != null)
        {
            trader.LastTradesUpdatedAt = null; // Force worker to pick it up
            await _traderRepo.UpdateAsync(trader, ct);
            _logger.LogInformation("Manually triggered trade sync for trader {TraderId} ({Name})", traderId, trader.DisplayName);
        }
    }

    public async Task ForceAnalyzeTraderAsync(int traderId, CancellationToken ct = default)
    {
        _logger.LogInformation("Manually forcing deep analysis for trader {TraderId}", traderId);
        await _pnlEngine.RecalculateTraderPositionsAsync(traderId, ct);
    }

    public async Task<int> AddTraderAsync(string platform, string walletAddress, CancellationToken ct = default)
    {
        if (!Enum.TryParse<PlatformType>(platform, true, out var pType))
            throw new ArgumentException($"Invalid platform: {platform}");

        _logger.LogInformation("Manually adding trader {Wallet} for platform {Platform}", walletAddress, platform);
        return await _discovery.ImportTraderAsync(pType, walletAddress, walletAddress[..Math.Min(10, walletAddress.Length)] + "...", false, ct);
    }

    private static TraderDto MapTraderDto(Trader t, HashSet<int> wIds) => new(
        t.Id, t.Platform.ToString(), t.PlatformUserId, t.DisplayName, t.Tier.ToString(), t.Strategy.ToString(),
        t.CurrentScore?.CombinedScore ?? 0, t.Analytics?.CopytradingScore ?? 0, 
        t.Analytics?.CopytradingQualityScore ?? 0, t.Analytics?.CopytradingCopyabilityScore ?? 0,
        t.WinRate, t.TotalPnl, t.TotalTrades,
        t.Analytics?.Trades30d ?? 0, t.Analytics?.PnL30d ?? 0, t.Analytics?.WinRate30d ?? 0, t.Analytics?.EstimatedBankroll ?? 0,
        wIds.Contains(t.Id), t.IsSuspectedBot, t.LastPolledAt, t.Traits?.Select(tr => tr.Trait).ToList());

    private static TradeDto MapTradeDto(Trade t) => new(
        t.Id, t.TraderId, t.Trader?.DisplayName ?? "?", t.Platform.ToString(),
        t.DbMarketId, t.MarketId, t.DbMarket?.Question ?? t.MarketId, t.Outcome, t.Side.ToString(), t.Price, t.Size, t.Amount, t.ExecutedAt);
}
