using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Domain.Accounts;
using Managing.Domain.Bots;
using Managing.Domain.Indicators;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers;
using Managing.Domain.Statistics;
using Managing.Domain.Trades;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;

namespace Managing.Application.Trading;

public class StatisticService : IStatisticService
{
    private readonly IStatisticRepository _statisticRepository;
    private readonly IExchangeService _exchangeService;
    private readonly IAccountService _accountService;
    private readonly IEvmManager _evmManager;
    private readonly ITradingService _tradingService;
    private readonly IBacktester _backtester;
    private readonly ITradaoService _tradaoService;
    private readonly IMessengerService _messengerService;
    private readonly ICacheService _cacheService;

    private readonly ILogger<StatisticService> _logger;

    public StatisticService(
        IExchangeService exchangeService,
        IAccountService accountService,
        ILogger<StatisticService> logger,
        IStatisticRepository statisticRepository,
        IEvmManager evmManager,
        ITradingService tradingService,
        IBacktester backtester,
        ITradaoService tradaoService,
        IMessengerService messengerService,
        ICacheService cacheService)
    {
        _exchangeService = exchangeService;
        _accountService = accountService;
        _logger = logger;
        _statisticRepository = statisticRepository;
        _evmManager = evmManager;
        _tradingService = tradingService;
        _backtester = backtester;
        _tradaoService = tradaoService;
        _messengerService = messengerService;
        _cacheService = cacheService;
    }

    public async Task UpdateTopVolumeTicker(TradingExchanges exchange, int top)
    {
        var account = (await _accountService.GetAccounts(false, false)).FirstOrDefault(a => a.Exchange == exchange);
        var date = DateTime.UtcNow;

        if (account == null)
            throw new Exception($"Enable to found account for exchange {exchange}");

        var lastTop = GetLastTopVolumeTicker();

        if (lastTop != null && lastTop.Count > 0)
        {
            _logger.LogInformation($"A top of {lastTop.Count} already exist for the current rage");
            return;
        }

        var volumeTickers = new Dictionary<Ticker, decimal>();
        foreach (var ticker in (Ticker[])Enum.GetValues(typeof(Ticker)))
        {
            var volume = _exchangeService.GetVolume(account, ticker);
            var price = await _exchangeService.GetPrice(account, ticker, date);
            volumeTickers.Add(ticker, volume * price);
        }

        var currentTop = volumeTickers.OrderByDescending(v => v.Value).Take(top).ToList();

        for (int rank = 0; rank < currentTop.Count; rank++)
        {
            var dto = new TopVolumeTicker()
            {
                Date = date,
                Rank = rank + 1,
                Ticker = currentTop[rank].Key,
                Volume = currentTop[rank].Value,
                Exchange = exchange
            };

            await _statisticRepository.InsertTopVolumeTicker(dto);
        }
    }

    public async Task UpdateFundingRates()
    {
        // Get fundingRate from database
        var previousFundingRate = await GetFundingRates();
        var newFundingRates = await _evmManager.GetFundingRates();

        var topRates = newFundingRates
            .Where(fr => fr.Direction == TradeDirection.Short && fr.Rate > 0)
            .OrderByDescending(fr => fr.Rate)
            .Take(3)
            .ToList();

        topRates.AddRange(newFundingRates
            .Where(fr => fr.Direction == TradeDirection.Long && fr.Rate > 0)
            .OrderBy(fr => fr.Rate)
            .TakeLast(3)
            .ToList());

        // Old position not in the new top
        foreach (var oldRate in previousFundingRate)
        {
            if (topRates.All(tr => !SameFundingRate(tr, oldRate)))
            {
                // Close position
                await _messengerService.SendDowngradedFundingRate(oldRate);
                await _statisticRepository.RemoveFundingRate(oldRate);
            }
        }

        // New position not in the old top
        foreach (var newRate in topRates)
        {
            if (previousFundingRate.All(tr => !SameFundingRate(tr, newRate)))
            {
                // Open position
                await _messengerService.SendNewTopFundingRate(newRate);
                await _statisticRepository.InsertFundingRate(newRate);
            }
            else if (previousFundingRate.Any(tr => SameFundingRate(tr, newRate)))
            {
                var oldRate = previousFundingRate.FirstOrDefault(tr => SameFundingRate(tr, newRate));
                if (oldRate != null && Math.Abs(oldRate.Rate - newRate.Rate) > 5m)
                {
                    await _messengerService.SendFundingRateUpdate(oldRate, newRate);
                    await _statisticRepository.UpdateFundingRateAsync(oldRate, newRate);
                }
            }
        }
    }

    private bool SameFundingRate(FundingRate oldRate, FundingRate newRate)
    {
        return oldRate.Ticker == newRate.Ticker &&
               oldRate.Exchange == newRate.Exchange &&
               oldRate.Direction == newRate.Direction;
    }

    public async Task<List<FundingRate>> GetFundingRates()
    {
        var previousFundingRate = await _statisticRepository.GetFundingRatesAsync();
        return previousFundingRate;
    }

    public IList<TopVolumeTicker> GetLastTopVolumeTicker()
    {
        return GetLastTopVolumeTickerAsync().Result;
    }

    public async Task<IList<TopVolumeTicker>> GetLastTopVolumeTickerAsync()
    {
        var from = DateTime.UtcNow.AddDays(-1);
        return await _statisticRepository.GetTopVolumeTickersAsync(from);
    }

    public async Task<IList<Ticker>> GetTickers()
    {
        var cachedTickers = _cacheService.GetValue<List<Ticker>>("tickers");

        if (cachedTickers != null)
        {
            return cachedTickers;
        }

        var tickers = await _evmManager.GetAvailableTicker();
        _cacheService.SaveValue("tickers", tickers, TimeSpan.FromDays(1));
        return tickers;
    }

    public async Task UpdateSpotlight()
    {
        var scenarios = await _tradingService.GetScenariosAsync();
        var account =
            (await _accountService.GetAccounts(false, false)).FirstOrDefault(a => a.Exchange == TradingExchanges.Evm);

        if (account == null)
            throw new Exception($"Enable to found default account");

        var overview = await GetLastSpotlightAsync(DateTime.UtcNow.AddMinutes(-20));

        if (overview != null)
        {
            if (overview.Spotlights.Count < overview.ScenarioCount)
            {
                _logger.LogInformation(
                    $"Spotlights not up to date. {overview.Spotlights.Count}/{overview.ScenarioCount}");
            }
            else
            {
                _logger.LogInformation("No need to update spotlights");
                return;
            }
        }
        else
        {
            overview = new SpotlightOverview
            {
                Spotlights = new List<Spotlight>(),
                DateTime = DateTime.UtcNow,
                Identifier = Guid.NewGuid(),
                ScenarioCount = scenarios.Count(),
            };
            await _statisticRepository.SaveSpotligthtOverview(overview);
        }

        var tickers = await GetTickers();

        foreach (var scenario in scenarios)
        {
            if (overview.Spotlights.Any(s => s.Scenario.Name == scenario.Name))
                continue;

            var spotlight = new Spotlight
            {
                TickerSignals = new List<TickerSignal>(),
                Scenario = scenario
            };

            // Use SemaphoreSlim to limit concurrency to 2 operations at a time
            using var semaphore = new SemaphoreSlim(2, 2);

            var tickerTasks = tickers.Select(async ticker =>
            {
                await semaphore.WaitAsync();
                try
                {
                    var tickerSignal = new TickerSignal
                    {
                        Ticker = ticker,
                        FiveMinutes = await GetSignals(account, scenario, ticker, Timeframe.FiveMinutes),
                        FifteenMinutes = await GetSignals(account, scenario, ticker, Timeframe.FifteenMinutes),
                        OneHour = await GetSignals(account, scenario, ticker, Timeframe.OneHour),
                        FourHour = await GetSignals(account, scenario, ticker, Timeframe.FourHour),
                        OneDay = await GetSignals(account, scenario, ticker, Timeframe.OneDay)
                    };
                    return tickerSignal;
                }
                finally
                {
                    semaphore.Release();
                }
            });

            var tickerSignals = await Task.WhenAll(tickerTasks);
            spotlight.TickerSignals.AddRange(tickerSignals.Where(ts => ts != null));

            overview.Spotlights.Add(spotlight);
            await _statisticRepository.UpdateSpotlightOverviewAsync(overview);
        }

        overview.DateTime = DateTime.UtcNow;
        await _statisticRepository.UpdateSpotlightOverviewAsync(overview);
    }

    private async Task<List<LightSignal>> GetSignals(Account account, Scenario scenario, Ticker ticker,
        Timeframe timeframe)
    {
        try
        {
            var moneyManagement = new MoneyManagement()
            {
                Leverage = 1,
                Timeframe = timeframe,
                StopLoss = 0.008m,
                TakeProfit = 0.02m
            };

            var config = new TradingBotConfig
            {
                AccountName = account.Name,
                MoneyManagement = moneyManagement,
                Ticker = ticker,
                ScenarioName = scenario.Name,
                Timeframe = timeframe,
                IsForWatchingOnly = true,
                BotTradingBalance = 1000,
                IsForBacktest = true,
                CooldownPeriod = 1,
                MaxLossStreak = 0,
                FlipPosition = false,
                Name = "StatisticsBacktest",
                FlipOnlyWhenInProfit = true,
                MaxPositionTimeHours = null,
                CloseEarlyWhenProfitable = false
            };

            var backtest = await _backtester.RunTradingBotBacktest(
                config,
                DateTime.UtcNow.AddDays(-7),
                DateTime.UtcNow,
                null,
                false,
                false);

            // Note: LightBacktest doesn't contain signals data, so we return an empty list
            // The full signals data would need to be retrieved from the database using the backtest ID
            _logger.LogWarning(
                "GetSignals called but LightBacktest doesn't contain signals data. Returning empty list.");
            return new List<LightSignal>();
        }
        catch (Exception ex)
        {
            _logger.LogError("Backtest cannot be run {message}", ex.Message);
        }

        return null;
    }

    public async Task<SpotlightOverview> GetLastSpotlight(DateTime dateTime)
    {
        return await GetLastSpotlightAsync(dateTime);
    }

    public async Task<SpotlightOverview> GetLastSpotlightAsync(DateTime dateTime)
    {
        var overviews = await _statisticRepository.GetSpotlightOverviewsAsync(dateTime);

        if (overviews.Any())
        {
            return overviews.OrderBy(o => o.DateTime).Last();
        }

        return null;
    }

    public List<Trader> GetBestTraders()
    {
        return GetBestTradersAsync().Result;
    }

    public async Task<List<Trader>> GetBestTradersAsync()
    {
        return await _statisticRepository.GetBestTradersAsync();
    }

    public List<Trader> GetBadTraders()
    {
        return GetBadTradersAsync().Result;
    }

    public async Task<List<Trader>> GetBadTradersAsync()
    {
        return await _statisticRepository.GetBadTradersAsync();
    }

    public async Task<List<Trade>> GetLeadboardPositons()
    {
        var customWatchAccount = await _tradingService.GetTradersWatch();
        var trades = new List<Trade>();

        foreach (var trader in customWatchAccount)
        {
            trades.AddRange(await _tradaoService.GetTrades(trader.Address));
        }

        return trades;
    }

    public async Task UpdateLeaderboard()
    {
        var previousBestTraders = await _statisticRepository.GetBestTradersAsync();
        var lastBestTrader = (await _tradaoService.GetBestTrader()).FindGoodTrader();

        // Update / Insert best trader
        foreach (var trader in lastBestTrader)
        {
            if (previousBestTraders.Exists((p) => p.Address == trader.Address))
            {
                await _statisticRepository.UpdateBestTraderAsync(trader);
            }
            else
            {
                await _statisticRepository.InsertBestTrader(trader);
            }
        }

        // Remove trader that wasnt good enough
        foreach (var trader in previousBestTraders)
        {
            if (!lastBestTrader.Exists((t) => t.Address == trader.Address))
            {
                await _statisticRepository.RemoveBestTrader(trader);
            }
        }

        await _messengerService.SendBestTraders(lastBestTrader);
    }

    public async Task UpdateNoobiesboard()
    {
        var previousBadTraders = await _statisticRepository.GetBadTradersAsync();
        var lastBadTrader = (await _tradaoService.GetBadTrader()).FindBadTrader();

        // Update / Insert best trader
        foreach (var trader in lastBadTrader)
        {
            if (previousBadTraders.Exists((p) => p.Address == trader.Address))
            {
                await _statisticRepository.UpdateBadTraderAsync(trader);
            }
            else
            {
                await _statisticRepository.InsertBadTrader(trader);
            }
        }

        // Remove trader that wasnt good enough
        foreach (var trader in previousBadTraders)
        {
            if (!lastBadTrader.Exists((t) => t.Address == trader.Address))
            {
                await _statisticRepository.RemoveBadTrader(trader);
            }
        }

        await _messengerService.SendBadTraders(lastBadTrader);
    }
}