using System.Text.Json;
using Predictalytics.Domain.Entities;
using Predictalytics.Domain.Enums;
using Predictalytics.Domain.Interfaces;
using Predictalytics.Infrastructure.Logging;
using Microsoft.Extensions.Logging;

namespace Predictalytics.Infrastructure.Providers.Polymarket;

/// <summary>
/// Full implementation of IPlatformProvider for Polymarket.
/// All log entries include Platform=Polymarket for log-file routing.
/// </summary>
public class PolymarketProvider : IPlatformProvider
{
    private readonly PolymarketApiClient _api;
    private readonly ILogger<PolymarketProvider> _logger;

    public PlatformType Platform => PlatformType.Polymarket;
    public string PlatformName => "Polymarket";
    public bool IsImplemented => true;

    public PolymarketProvider(PolymarketApiClient api, ILogger<PolymarketProvider> logger)
    { _api = api; _logger = logger; }

    public async Task<IReadOnlyList<Trade>> GetTraderTradesAsync(string platformUserId, int limit = 1000, CancellationToken ct = default)
    {
        using var _ = PlatformLogContext.Push(PlatformName);
        _logger.LogDebug("Fetching trades for {Wallet} (limit={Limit})", platformUserId, limit);
        var raw = await _api.GetTradesAsync(platformUserId, limit, ct);
        _logger.LogInformation("Fetched {Count} trades for {Wallet}", raw.Count, platformUserId);

        var mappedTrades = raw.Select(r =>
        {
            var wallet = !string.IsNullOrEmpty(r.User) ? r.User : 
                         !string.IsNullOrEmpty(r.ProxyWallet) ? r.ProxyWallet : 
                         platformUserId;
            var side = MapTradeSide(r);
            var sideStr = side.ToString().ToUpperInvariant();
            // Format: {txHash}_{wallet}_{assetId}_{side}
            // Wallet must be included to avoid cross-user collisions in the global IX_Trades_Platform_PlatformTradeId index.
            return new Trade
            {
                Platform = PlatformType.Polymarket,
                PlatformTradeId = string.IsNullOrEmpty(r.TransactionHash)
                    ? $"{r.Timestamp}_{wallet}_{r.Asset}_{sideStr}"
                    : $"{r.TransactionHash.ToLowerInvariant()}_{wallet}_{r.Asset}_{sideStr}",
                MarketId = r.ConditionId ?? "",
                AssetId = r.Asset ?? "",
                Outcome = r.Outcome ?? "",
                Side = side,
                Price = (decimal)r.Price,
                Size = (decimal)r.Size,
                Amount = (decimal)(r.Price * r.Size),
                ExecutedAt = DateTimeOffset.FromUnixTimeSeconds(r.Timestamp).UtcDateTime,
                TransactionHash = r.TransactionHash?.ToLowerInvariant(),
                TraderId = 0,
                TransientWallet = wallet,
                TransientDisplayName = !string.IsNullOrEmpty(r.Name) ? r.Name : r.Pseudonym,
                UsdcSize = (decimal)r.UsdcSize,
                OutcomeIndex = r.OutcomeIndex
            };
        }).ToList();

        return mappedTrades.GroupBy(t => t.PlatformTradeId, StringComparer.OrdinalIgnoreCase).Select(g => g.First()).ToList();
    }

    public async Task<IReadOnlyList<Trade>> GetTradesPagedAsync(string platformUserId, int limitPerRequest = 500, CancellationToken ct = default)
    {
        using var _ = PlatformLogContext.Push(PlatformName);
        _logger.LogDebug("Fetching all paged trades for {Wallet}", platformUserId);
        var raw = await _api.GetTradesPagedAsync(platformUserId, limitPerRequest, ct);
        _logger.LogInformation("Fetched {Count} trades total for {Wallet}", raw.Count, platformUserId);

        var mappedTrades = raw.Select(r =>
        {
            var wallet = !string.IsNullOrEmpty(r.User) ? r.User : 
                         !string.IsNullOrEmpty(r.ProxyWallet) ? r.ProxyWallet : 
                         platformUserId;
            var side = MapTradeSide(r);
            var sideStr = side.ToString().ToUpperInvariant();
            return new Trade
            {
                Platform = PlatformType.Polymarket,
                PlatformTradeId = string.IsNullOrEmpty(r.TransactionHash)
                    ? $"{r.Timestamp}_{wallet}_{r.Asset}_{sideStr}"
                    : $"{r.TransactionHash.ToLowerInvariant()}_{wallet}_{r.Asset}_{sideStr}",
                MarketId = r.ConditionId ?? "",
                AssetId = r.Asset ?? "",
                Outcome = r.Outcome ?? "",
                Side = side,
                Price = (decimal)r.Price,
                Size = (decimal)r.Size,
                Amount = (decimal)(r.Price * r.Size),
                ExecutedAt = DateTimeOffset.FromUnixTimeSeconds(r.Timestamp).UtcDateTime,
                TransactionHash = r.TransactionHash?.ToLowerInvariant(),
                TraderId = 0,
                TransientWallet = wallet,
                TransientDisplayName = !string.IsNullOrEmpty(r.Name) ? r.Name : r.Pseudonym,
                UsdcSize = (decimal)r.UsdcSize,
                OutcomeIndex = r.OutcomeIndex
            };
        }).ToList();

        return mappedTrades.GroupBy(t => t.PlatformTradeId, StringComparer.OrdinalIgnoreCase).Select(g => g.First()).ToList();
    }
    public async Task<IReadOnlyList<Trade>> GetMarketTradesAsync(string platformMarketId, int limit = 1000, CancellationToken ct = default)
    {
        var raw = await _api.GetMarketTradesAsync(platformMarketId, limit, 0, ct);
        _logger.LogInformation("Fetched {Count} trades for Market {Market} (limit={Limit})", raw.Count, platformMarketId, limit);

        var mappedTrades = raw.Select(r =>
        {
            var wallet = !string.IsNullOrEmpty(r.User) ? r.User : (r.ProxyWallet ?? "");
            var side = MapTradeSide(r);
            var sideStr = side.ToString().ToUpperInvariant();
            return new Trade
            {
                Platform = PlatformType.Polymarket,
                PlatformTradeId = string.IsNullOrEmpty(r.TransactionHash)
                    ? $"{r.Timestamp}_{wallet}_{r.Asset}_{sideStr}"
                    : $"{r.TransactionHash.ToLowerInvariant()}_{wallet}_{r.Asset}_{sideStr}",
                MarketId = r.ConditionId ?? "",
                AssetId = r.Asset ?? "",
                Outcome = r.Outcome ?? "",
                Side = side,
                Price = (decimal)r.Price,
                Size = (decimal)r.Size,
                Amount = (decimal)(r.Price * r.Size),
                ExecutedAt = DateTimeOffset.FromUnixTimeSeconds(r.Timestamp).UtcDateTime,
                TransactionHash = r.TransactionHash,
                TraderId = 0,
                TransientWallet = wallet,
                TransientDisplayName = !string.IsNullOrEmpty(r.Name) ? r.Name : r.Pseudonym,
                UsdcSize = (decimal)r.UsdcSize,
                OutcomeIndex = r.OutcomeIndex
            };
        }).ToList();

        return mappedTrades.GroupBy(t => t.PlatformTradeId).Select(g => g.First()).ToList();
    }

    public async Task<IReadOnlyList<TraderPositionInfo>> GetTraderPositionsAsync(string platformUserId, CancellationToken ct = default)
    {
        using var _ = PlatformLogContext.Push(PlatformName);
        _logger.LogDebug("Fetching positions for {Wallet}", platformUserId);
        var raw = await _api.GetPositionsAsync(platformUserId, ct);
        _logger.LogInformation("Fetched {Count} positions for {Wallet}", raw.Count, platformUserId);

        return raw.Select(r => new TraderPositionInfo(
            platformUserId, r.Market, r.Question, r.Outcome,
            (decimal)r.Size, (decimal)r.AvgPrice,
            (decimal)r.CurrentValue, (decimal)r.PercentPnl,
            r.AssetId, (decimal)r.CashPnl
        )).ToList();
    }

    public async Task<IReadOnlyList<DiscoveredTrader>> DiscoverTradersAsync(int limit = 50, CancellationToken ct = default)
    {
        using var _ = PlatformLogContext.Push(PlatformName);
        _logger.LogInformation("Running trader discovery via v1/leaderboard (limit={Limit})...", limit);
        var leaderboard = await _api.GetLeaderboardAsync(limit, ct: ct);
        _logger.LogInformation("Discovery returned {Count} traders from leaderboard", leaderboard.Count);

        return leaderboard.Select(e => new DiscoveredTrader(
            e.ProxyWallet,
            string.IsNullOrEmpty(e.UserName) ? e.ProxyWallet[..10] + "..." : e.UserName,
            (decimal)e.Vol,
            0, // trade count not in leaderboard API
            0  // win rate computed later from trades
        )).ToList();
    }

    public async Task<Market?> GetMarketAsync(string platformMarketId, CancellationToken ct = default)
    {
        using var _ = PlatformLogContext.Push(PlatformName);
        var raw = await _api.GetMarketAsync(platformMarketId, ct);
        if (raw == null || string.IsNullOrEmpty(raw.ConditionId)) return null;

        var parentTags = "";
        if (raw.Events != null && raw.Events.Count > 0)
        {
            var ev = raw.Events[0];
            parentTags = ev.Tags != null && ev.Tags.Count > 0 ? string.Join(", ", ev.Tags.Select(t => t.Label)) : "";

            // F3: If parentTags is empty, try to fetch the event from Gamma API to load tags
            if (string.IsNullOrEmpty(parentTags) && long.TryParse(ev.Id, out var eventId))
            {
                try
                {
                    var fetchedEv = await _api.GetEventAsync(eventId, ct);
                    if (fetchedEv != null && fetchedEv.Tags != null && fetchedEv.Tags.Count > 0)
                    {
                        parentTags = string.Join(", ", fetchedEv.Tags.Select(t => t.Label));
                        ev.Tags = fetchedEv.Tags;
                    }
                }
                catch (Exception ex)
                {
                    _logger.LogWarning(ex, "Failed to fetch event {EventId} for tags", eventId);
                }
            }
        }
        var market = MapGammaMarket(raw, parentTags);

        // Map the parent Event if available in the Market response
        if (raw.Events != null && raw.Events.Count > 0)
        {
            var rawEv = raw.Events[0];
            long.TryParse(rawEv.Id, out var numericEventId);
            market.Event = new Event
            {
                Platform = PlatformType.Polymarket,
                PlatformEventId = numericEventId,
                Slug = rawEv.Slug,
                Title = rawEv.Title,
                Description = rawEv.Description,
                ImageUrl = rawEv.Image,
                StartDate = DateTime.TryParse(rawEv.StartDate, out var esd) ? esd : null,
                EndDate = DateTime.TryParse(rawEv.EndDate, out var eed) ? eed : null,
                CreatedAt = DateTime.TryParse(rawEv.CreatedAt, out var ecd) ? ecd : DateTime.UtcNow,
                DbCreatedAt = DateTime.UtcNow,
                IsActive = rawEv.Active,
                IsClosed = rawEv.Closed,
                Tags = rawEv.Tags != null && rawEv.Tags.Count > 0 ? string.Join(", ", rawEv.Tags.Select(t => t.Label)) : string.Empty,
                LastUpdatedAt = DateTime.UtcNow
            };
        }
        else
        {
            // Fallback empty event if missing (should rarely happen for valid Polymarket markets)
            market.Event = new Event
            {
                Platform = PlatformType.Polymarket,
                Slug = "unknown-" + market.ConditionId,
                Title = "Unknown Event",
                DbCreatedAt = DateTime.UtcNow,
                LastUpdatedAt = DateTime.UtcNow
            };
        }

        _logger.LogInformation("Fetched market: {Question}", raw.Question);
        return market;
    }

    public async Task<IReadOnlyList<Event>> GetEventsAsync(int limit = 100, string? cursor = null, bool includeClosed = false, CancellationToken ct = default)
    {
        using var _ = PlatformLogContext.Push(PlatformName);
        int offset = 0;
        if (!string.IsNullOrEmpty(cursor) && int.TryParse(cursor, out var parsed))
            offset = parsed;

        _logger.LogInformation("Fetching events batch (limit={Limit}, offset={Offset}, includeClosed={Closed})", limit, offset, includeClosed);
        var rawEvents = await _api.GetEventsAsync(limit, offset, includeClosed, ct);
        _logger.LogInformation("Fetched {Count} events from Gamma API", rawEvents.Count);

        var events = new List<Event>();
        foreach (var rawEvent in rawEvents)
        {
            var ev = MapGammaEvent(rawEvent);
            events.Add(ev);
        }

        return events;
    }

    public async Task<IReadOnlyList<DiscoveredTrader>> GetTopHoldersAsync(string platformMarketId, int limit = 20, CancellationToken ct = default)
    {
        using var _ = PlatformLogContext.Push(PlatformName);
        _logger.LogInformation("Fetching top holders for market {MarketId}", platformMarketId[..12] + "...");
        var holdersGroups = await _api.GetHoldersAsync(platformMarketId, limit, ct);

        // Flatten all holders across token groups, deduplicate by wallet
        var uniqueHolders = holdersGroups
            .SelectMany(g => g.Holders)
            .GroupBy(h => h.ProxyWallet)
            .Select(g =>
            {
                var first = g.First();
                var totalAmount = g.Sum(h => h.Amount);
                var displayName = !string.IsNullOrEmpty(first.Name) ? first.Name
                    : !string.IsNullOrEmpty(first.Pseudonym) ? first.Pseudonym
                    : first.ProxyWallet[..10] + "...";

                return new DiscoveredTrader(
                    first.ProxyWallet,
                    displayName,
                    (decimal)totalAmount,
                    0, 0
                );
            })
            .OrderByDescending(d => d.Volume24h)
            .ToList();

        _logger.LogInformation("Discovered {Count} unique holders from market {MarketId}",
            uniqueHolders.Count, platformMarketId[..12] + "...");

        return uniqueHolders;
    }

    // ── Private helpers ──────────────────────────────────────────

    private Event MapGammaEvent(GammaEventResponse rawEvent)
    {
        long.TryParse(rawEvent.Id, out var numericId);
        
        var ev = new Event
        {
            Platform = PlatformType.Polymarket,
            PlatformEventId = numericId,
            Slug = rawEvent.Slug,
            Title = rawEvent.Title,
            Description = rawEvent.Description,
            ImageUrl = rawEvent.Image,
            StartDate = DateTime.TryParse(rawEvent.StartDate, out var sd) ? sd : null,
            EndDate = DateTime.TryParse(rawEvent.EndDate, out var ed) ? ed : null,
            CreatedAt = DateTime.TryParse(rawEvent.CreatedAt, out var cd) ? cd : DateTime.UtcNow,
            DbCreatedAt = DateTime.UtcNow,
            IsActive = rawEvent.Active,
            IsClosed = rawEvent.Closed,
            Tags = rawEvent.Tags != null && rawEvent.Tags.Count > 0 
                ? string.Join(", ", rawEvent.Tags.Select(t => t.Label)) 
                : string.Empty,
            LastUpdatedAt = DateTime.UtcNow
        };

        if (rawEvent.Markets != null)
        {
            foreach (var rawMarket in rawEvent.Markets)
            {
                if (string.IsNullOrEmpty(rawMarket.ConditionId) || string.IsNullOrEmpty(rawMarket.ClobTokenIds)) continue;

                var market = MapGammaMarket(rawMarket, ev.Tags ?? "");
                ev.Markets.Add(market);
            }
        }

        return ev;
    }

    private Market MapGammaMarket(GammaMarketResponse raw, string parentTags = "")
    {
        long.TryParse(raw.Id, out var marketNumericId);

        var catMap = Predictalytics.Infrastructure.Helpers.MarketCategoryMapper.Map(raw.Category ?? "", parentTags, raw.Question ?? "");

        var market = new Market
        {
            Platform = PlatformType.Polymarket,
            PlatformMarketId = marketNumericId,
            ConditionId = raw.ConditionId,
            QuestionId = raw.QuestionId,
            MarketSlug = raw.Slug,
            Description = raw.Description,
            ImageUrl = raw.Image,
            Question = raw.Question,
            Category = catMap.Category,
            Subcategory = catMap.Subcategory,
            Volume = (decimal)raw.Volume,
            Volume24h = (decimal)raw.Volume24hr,
            Liquidity = (decimal)raw.Liquidity,
            StartDate = DateTime.TryParse(raw.StartDate, out var msd) ? msd : null,
            EndDate = DateTime.TryParse(raw.EndDate ?? raw.EndDateIso, out var med) ? med : null,
            CreatedAt = DateTime.TryParse(raw.CreatedAt, out var mcd) ? mcd : DateTime.UtcNow,
            DbCreatedAt = DateTime.UtcNow,
            LastUpdatedAt = DateTime.UtcNow,
            FeeRateBps = (decimal)(raw.TakerFee * 10000),
            IsNegRisk = raw.NegRisk,
            ClosedAt = DateTime.TryParse(raw.ClosedTime, out var mct) ? mct : null
        };

        // Parse outcomes, prices, and token IDs from JSON strings
        var outcomeLabels = ParseJsonStringArray(raw.Outcomes);
        var outcomePrices = ParseJsonStringArray(raw.OutcomePrices);
        var tokenIds = ParseJsonStringArray(raw.ClobTokenIds);

        bool pricesSnapped = false;
        bool hasHighPrice = false;
        bool allSnapped = outcomePrices.Count > 0;
        string? snappedWinnerLabel = null;

        for (int i = 0; i < outcomeLabels.Count; i++)
        {
            decimal price = 0;
            if (i < outcomePrices.Count)
                decimal.TryParse(outcomePrices[i], System.Globalization.NumberStyles.Any,
                    System.Globalization.CultureInfo.InvariantCulture, out price);

            var label = outcomeLabels[i];
            if ((label.Equals("Yes", StringComparison.OrdinalIgnoreCase) || label.Equals("No", StringComparison.OrdinalIgnoreCase))
                && !string.IsNullOrEmpty(raw.GroupItemTitle))
            {
                label = $"{raw.GroupItemTitle} - {label}";
            }

            if (price <= 0.02m || price >= 0.98m)
            {
                if (price >= 0.98m)
                {
                    hasHighPrice = true;
                    snappedWinnerLabel = label;
                }
            }
            else
            {
                allSnapped = false;
            }

            string tokenId = i < tokenIds.Count ? tokenIds[i] : "";

            market.Outcomes.Add(new MarketOutcome
            {
                Label = label,
                OutcomeIndex = i,
                TokenId = tokenId,
                CurrentPrice = price
            });
        }

        pricesSnapped = allSnapped && hasHighPrice;

        market.IsResolved = raw.UmaResolutionStatus == "resolved" || (raw.Closed && pricesSnapped);
        market.ResolutionOutcome = market.IsResolved ? snappedWinnerLabel : null;

        return market;
    }

    private static List<string> ParseJsonStringArray(string? json)
    {
        if (string.IsNullOrEmpty(json)) return [];

        try
        {
            return JsonSerializer.Deserialize<List<string>>(json) ?? [];
        }
        catch
        {
            return [];
        }
    }

    private static TradeSide MapTradeSide(PolymarketTradeResponse r)
    {
        // Check Action/Type field first for special operations
        var typeOrAction = !string.IsNullOrEmpty(r.Action) ? r.Action
            : !string.IsNullOrEmpty(r.Type) ? r.Type : "";

        if (!string.IsNullOrEmpty(typeOrAction))
        {
            if (typeOrAction.Equals("SPLIT", StringComparison.OrdinalIgnoreCase)) return TradeSide.Split;
            if (typeOrAction.Equals("MERGE", StringComparison.OrdinalIgnoreCase)) return TradeSide.Merge;
            if (typeOrAction.Equals("REDEEM", StringComparison.OrdinalIgnoreCase)) return TradeSide.Redeem;
            if (typeOrAction.Equals("ADD_LIQUIDITY", StringComparison.OrdinalIgnoreCase)) return TradeSide.AddLiquidity;
            if (typeOrAction.Equals("REMOVE_LIQUIDITY", StringComparison.OrdinalIgnoreCase)) return TradeSide.RemoveLiquidity;
            // Type field can also contain BUY/SELL directly
            if (typeOrAction.Equals("BUY", StringComparison.OrdinalIgnoreCase)) return TradeSide.Buy;
            if (typeOrAction.Equals("SELL", StringComparison.OrdinalIgnoreCase)) return TradeSide.Sell;
        }

        // Side field (explicit buy/sell direction)
        if (!string.IsNullOrEmpty(r.Side))
        {
            if (r.Side.Equals("BUY", StringComparison.OrdinalIgnoreCase)) return TradeSide.Buy;
            if (r.Side.Equals("SELL", StringComparison.OrdinalIgnoreCase)) return TradeSide.Sell;
        }

        return TradeSide.Unknown;
    }

    public async Task<IReadOnlyList<MarketOutcomePriceSnapshot>> GetPriceHistoryAsync(string tokenId, CancellationToken ct = default)
    {
        using var _ = PlatformLogContext.Push(PlatformName);
        _logger.LogDebug("Fetching price history for CLOB Token {TokenId}", tokenId);
        
        var rawHistory = await _api.GetPricesHistoryAsync(tokenId, "6h", ct);
        _logger.LogInformation("Fetched {Count} price history entries for {TokenId}", rawHistory.Count, tokenId);

        return rawHistory.Select(r => new MarketOutcomePriceSnapshot
        {
            Price = (decimal)r.Price,
            Timestamp = DateTimeOffset.FromUnixTimeSeconds(r.Timestamp).UtcDateTime,
            MarketOutcomeId = 0
        }).ToList();
    }
}
