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

namespace Managing.Application.Workers;

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 IAgentBalanceRepository _agentBalanceRepository;
    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,
        IAgentBalanceRepository agentBalanceRepository)
    {
        _exchangeService = exchangeService;
        _accountService = accountService;
        _logger = logger;
        _statisticRepository = statisticRepository;
        _evmManager = evmManager;
        _tradingService = tradingService;
        _backtester = backtester;
        _tradaoService = tradaoService;
        _messengerService = messengerService;
        _cacheService = cacheService;
        _agentBalanceRepository = agentBalanceRepository;
    }

    public async Task UpdateTopVolumeTicker(TradingExchanges exchange, int top)
    {
        var account = _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 = _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);
                    _statisticRepository.UpdateFundingRate(oldRate, newRate);
                }
            }
        }
    }

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

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

    public IList<TopVolumeTicker> GetLastTopVolumeTicker()
    {
        var from = DateTime.UtcNow.AddDays(-1);
        return _statisticRepository.GetTopVolumeTickers(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 = _tradingService.GetScenarios();
        var account = _accountService.GetAccounts(false, false).FirstOrDefault(a => a.Exchange == TradingExchanges.Evm);

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

        var overview = GetLastSpotlight(DateTime.Now.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.Now,
                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
            };

            var options = new ParallelOptions()
            {
                MaxDegreeOfParallelism = 2
            };

            _ = Parallel.ForEach(tickers, options, async ticker =>
            {
                spotlight.TickerSignals.Add(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)
                });
            });

            overview.Spotlights.Add(spotlight);
            _statisticRepository.UpdateSpotlightOverview(overview);
        }

        overview.DateTime = DateTime.Now;
        _statisticRepository.UpdateSpotlightOverview(overview);
    }

    private async Task<List<Signal>> 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,
                BotType = BotType.ScalpingBot,
                IsForBacktest = true,
                CooldownPeriod = 1,
                MaxLossStreak = 0,
                FlipPosition = false,
                Name = "StatisticsBacktest",
                FlipOnlyWhenInProfit = true,
                MaxPositionTimeHours = null,
                CloseEarlyWhenProfitable = false
            };

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

            return backtest.Signals;
        }
        catch (Exception ex)
        {
            _logger.LogError("Backtest cannot be run {message}", ex.Message);
        }

        return null;
    }

    public SpotlightOverview GetLastSpotlight(DateTime dateTime)
    {
        var overviews = _statisticRepository.GetSpotlightOverviews(dateTime);

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

        return null;
    }

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

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

    public async Task<List<Trade>> GetLeadboardPositons()
    {
        var customWatchAccount = _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 = _statisticRepository.GetBestTraders();
        var lastBestTrader = (await _tradaoService.GetBestTrader()).FindGoodTrader();

        // Update / Insert best trader
        foreach (var trader in lastBestTrader)
        {
            if (previousBestTraders.Exists((p) => p.Address == trader.Address))
            {
                _statisticRepository.UpdateBestTrader(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 = _statisticRepository.GetBadTraders();
        var lastBadTrader = (await _tradaoService.GetBadTrader()).FindBadTrader();

        // Update / Insert best trader
        foreach (var trader in lastBadTrader)
        {
            if (previousBadTraders.Exists((p) => p.Address == trader.Address))
            {
                _statisticRepository.UpdateBadTrader(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);
    }

    public async Task<AgentBalanceHistory> GetAgentBalances(string agentName, DateTime start,
        DateTime? end = null)
    {
        var effectiveEnd = end ?? DateTime.UtcNow;
        string cacheKey = $"AgentBalances_{agentName}_{start:yyyyMMdd}_{effectiveEnd:yyyyMMdd}";

        // Check if the balances are already cached
        var cachedBalances = _cacheService.GetValue<AgentBalanceHistory>(cacheKey);

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

        var balances = await _agentBalanceRepository.GetAgentBalances(agentName, start, end);

        // Create a single AgentBalanceHistory with all balances
        var result = new AgentBalanceHistory
        {
            AgentName = agentName,
            AgentBalances = balances.OrderBy(b => b.Time).ToList()
        };

        // Cache the results for 5 minutes
        _cacheService.SaveValue(cacheKey, result, TimeSpan.FromMinutes(5));

        return result;
    }

    public async Task<(IList<AgentBalanceHistory> Agents, int TotalCount)> GetBestAgents(
        DateTime start,
        DateTime? end = null,
        int page = 1,
        int pageSize = 10)
    {
        var effectiveEnd = end ?? DateTime.UtcNow;
        string cacheKey = $"BestAgents_{start:yyyyMMdd}_{effectiveEnd:yyyyMMdd}";

        // Check if the results are already cached
        var cachedResult = _cacheService.GetValue<(IList<AgentBalanceHistory>, int)>(cacheKey);

        if (cachedResult != default)
        {
            // Apply pagination to cached results
            var (cachedAgents, cachedTotalCount) = cachedResult;
            var paginatedAgents = cachedAgents
                .Skip((page - 1) * pageSize)
                .Take(pageSize)
                .ToList();
            return (paginatedAgents, cachedTotalCount);
        }

        // Get all agents with their balance history
        var (fetchedAgents, fetchedTotalCount) =
            await _agentBalanceRepository.GetAllAgentBalancesWithHistory(start, end);

        // Cache all results for 5 minutes
        _cacheService.SaveValue(cacheKey, (fetchedAgents, fetchedTotalCount), TimeSpan.FromMinutes(5));

        // Apply pagination
        var result = fetchedAgents
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .ToList();

        return (result, fetchedTotalCount);
    }
}