using Discord;
using Discord.Commands;
using Discord.Net;
using Discord.WebSocket;
using Managing.Application.Abstractions.Services;
using Managing.Application.Trading.Commands;
using Managing.Application.Trading.Handlers;
using Managing.Common;
using Managing.Core;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Statistics;
using Managing.Domain.Trades;
using Managing.Domain.Users;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using static Managing.Common.Enums;

namespace Managing.Infrastructure.Messengers.Discord
{
    public class DiscordService : IHostedService, IDisposable, IDiscordService
    {
        private const string _separator = "|";
        private readonly DiscordSocketClient _client;
        private readonly CommandService _commandService;
        private readonly IServiceProvider _services;
        private readonly IExchangeService _exchangeService;
        private readonly IMoneyManagementService _moneyManagementService;
        private readonly IAccountService _accountService;
        private readonly ITradingService _tradingService;
        private readonly IStatisticService _statisticService;

        private readonly DiscordSettings _settings;
        private readonly ILogger<DiscordService> _logger;

        private readonly string _explorerUrl = "";

        public DiscordService(DiscordSocketClient client,
            CommandService commandService,
            IServiceProvider services,
            DiscordSettings settings, ILogger<DiscordService> logger)
        {
            _client = client;
            _commandService = commandService;
            _services = services;
            _settings = settings;
            _logger = logger;
        }

        #region Setup

        // The hosted service has started
        public async Task StartAsync(CancellationToken cancellationToken)
        {
            if (_settings.HandleUserAction)
            {
                _client.ButtonExecuted += ButtonHandler;
                _commandService.CommandExecuted += CommandExecuted;
                _client.SlashCommandExecuted += SlashCommandHandler;
            }

            _client.Ready += ClientReady;
            _client.Log += Log;
            _commandService.Log += Log;
            // look for classes implementing ModuleBase to load commands from
            await _commandService.AddModulesAsync(GetType().Assembly, _services);
            // log in to Discord, using the provided token
            await _client.LoginAsync(TokenType.Bot, _settings.Token);
            // start bot
            await _client.StartAsync();
        }

        private async Task SlashCommandHandler(SocketSlashCommand command)
        {
            await command.DeferAsync();

            // Let's add a switch statement for the command name so we can handle multiple commands in one event.
            switch (command.Data.Name)
            {
                case Constants.DiscordSlashCommand.Leaderboard:
                    await SlashCommands.HandleLeaderboardCommand(_services, command);
                    break;
                case Constants.DiscordSlashCommand.Noobiesboard:
                    await SlashCommands.HandleNoobiesboardCommand(_services, command);
                    break;
                case Constants.DiscordSlashCommand.LeaderboardPosition:
                    await SlashCommands.HandleLeadboardPositionCommand(_services, command);
                    break;
                case Constants.DiscordSlashCommand.FundingRates:
                    await SlashCommands.HandleFundingRateCommand(_services, command);
                    break;
            }
        }

        private async Task ClientReady()
        {
            // set status to online
            await _client.SetStatusAsync(UserStatus.Online);
            // Discord started as a game chat service, so it has the option to show what games you are playing
            // Here the bot will display "Playing dead" while listening
            await _client.SetGameAsync(_settings.BotActivity, "https://moon.com", ActivityType.Playing);
            if (!_settings.HandleUserAction) return;

            List<ApplicationCommandProperties> applicationCommandProperties = new();
            try
            {
                var leaderBoardCommand = new SlashCommandBuilder();
                leaderBoardCommand.WithName(Constants.DiscordSlashCommand.Leaderboard);
                leaderBoardCommand.WithDescription("Shows the last leaderboard");
                applicationCommandProperties.Add(leaderBoardCommand.Build());

                var leadboardPositionsCommand = new SlashCommandBuilder();
                leadboardPositionsCommand.WithName(Constants.DiscordSlashCommand.LeaderboardPosition);
                leadboardPositionsCommand.WithDescription("Shows the opened position of the leaderboard");
                applicationCommandProperties.Add(leadboardPositionsCommand.Build());

                var noobiesboardCommand = new SlashCommandBuilder();
                noobiesboardCommand.WithName(Constants.DiscordSlashCommand.Noobiesboard);
                noobiesboardCommand.WithDescription("Shows the last Noobies board");
                applicationCommandProperties.Add(noobiesboardCommand.Build());

                var fundingRatesCommand = new SlashCommandBuilder();
                fundingRatesCommand.WithName(Constants.DiscordSlashCommand.FundingRates);
                fundingRatesCommand.WithDescription("Shows the last funding rates");
                applicationCommandProperties.Add(fundingRatesCommand.Build());

                await _client.BulkOverwriteGlobalApplicationCommandsAsync(applicationCommandProperties.ToArray());
            }
            catch (ApplicationCommandException exception)
            {
                var json = JsonConvert.SerializeObject(exception, Formatting.Indented);
                _logger.LogError(exception, json);
            }
        }

        public static List<ApplicationCommandProperties> GetSlashCommands()
        {
            List<ApplicationCommandProperties> commands = new();
            return commands;
        }

        // logging
        private async Task Log(LogMessage arg)
        {
            await Task.Run(() => { _logger.LogInformation(arg.ToString()); });
        }

        private async Task CommandExecuted(Optional<CommandInfo> command, ICommandContext context, IResult result)
        {
            // if a command isn't found
            if (!command.IsSpecified)
            {
                await context.Message.AddReactionAsync(new Emoji("🤨")); // eyebrow raised emoji
                return;
            }

            // log failure to the console 
            if (!result.IsSuccess)
            {
                await Log(new LogMessage(LogSeverity.Error, nameof(CommandExecuted), $"Error: {result.ErrorReason}"));
                return;
            }

            // react to message
            await context.Message.AddReactionAsync(new Emoji("🤖")); // robot emoji
        }

        // the hosted service is stopping
        public async Task StopAsync(CancellationToken cancellationToken)
        {
            await _client.SetGameAsync(null);
            await _client.SetStatusAsync(UserStatus.Offline);
            await _client.StopAsync();
            _client.Log -= Log;
            _client.Ready -= ClientReady;
            _commandService.Log -= Log;
            _commandService.CommandExecuted -= CommandExecuted;
            _client.ButtonExecuted -= ButtonHandler;
            _client.SlashCommandExecuted -= SlashCommandHandler;
        }

        public void Dispose()
        {
            _client?.Dispose();
        }

        #endregion

        #region In

        public async Task ButtonHandler(SocketMessageComponent component)
        {
            var parameters = component.Data.CustomId.Split(new[] { _separator }, StringSplitOptions.None);
            var command = parameters[0];

            if (component.User.GlobalName != "crypto_saitama")
            {
                await component.Channel.SendMessageAsync(
                    "Sorry bro, this feature is not accessible for you.. Do not hesitate to send me approx. 456 121 $ and i give you full access");
            }
            else
            {
                switch (command)
                {
                    case Constants.DiscordButtonAction.OpenPosition:
                        await OpenPosition(component, parameters);
                        break;
                    case Constants.DiscordButtonAction.ClosePosition:
                        await ClosePosition(component, parameters);
                        break;
                    case Constants.DiscordButtonAction.CopyPosition:
                        await CopyPosition(component, parameters);
                        break;
                    default:
                        break;
                }
            }
        }

        private async Task CopyPosition(SocketMessageComponent component, string[] parameters)
        {
            await component.Channel.SendMessageAsync("Let met few seconds to copy this position");
            await component.Channel.TriggerTypingAsync();

            var json = MiscExtensions.Base64Decode(parameters[1]);
            var trade = JsonConvert.DeserializeObject<CopyTradeData>(json);
            await OpenPosition(component, trade.AccountName, trade.MoneyManagementName, PositionInitiator.CopyTrading,
                trade.Ticker, trade.Direction, Timeframe.FifteenMinutes,
                DateTime.Now.AddMinutes(trade.ExpirationMinute), true, trade.Leverage);
        }

        public async Task OpenPosition(SocketMessageComponent component, string[] parameters)
        {
            await component.Channel.SendMessageAsync("Let met few seconds to open a position");
            await component.Channel.TriggerTypingAsync();

            var accountName = MiscExtensions.ParseEnum<string>(parameters[1]);
            var ticker = MiscExtensions.ParseEnum<Ticker>(parameters[2]);
            var direction = MiscExtensions.ParseEnum<TradeDirection>(parameters[3]);
            var timeframe = MiscExtensions.ParseEnum<Timeframe>(parameters[4]);
            var moneyManagementName = parameters[5];
            var expiration = DateTime.Parse(parameters[6]);

            await OpenPosition(component, accountName, moneyManagementName, PositionInitiator.User, ticker, direction,
                timeframe, expiration, false);
        }

        private async Task OpenPosition(SocketMessageComponent component, string accountName, string moneyManagement,
            PositionInitiator initiator, Ticker ticker, TradeDirection direction, Timeframe timeframe,
            DateTime expiration, bool ignoreSLTP, decimal? leverage = null)
        {
            if (DateTime.Now > expiration)
            {
                await component.Channel.SendMessageAsync(
                    "Sorry I can't open position because you tried to click on a expired button.");
            }
            else
            {
                var exchangeService = (IExchangeService)_services.GetService(typeof(IExchangeService));
                var moneyManagementService =
                    (IMoneyManagementService)_services.GetService(typeof(IMoneyManagementService));
                var accountService = (IAccountService)_services.GetService(typeof(IAccountService));
                var tradingService = (ITradingService)_services.GetService(typeof(ITradingService));

                // Create default user for Discord bot operations
                var defaultUser = new User { Name = "DiscordBot" };

                var tradeCommand = new OpenPositionRequest(
                    accountName,
                    await moneyManagementService.GetMoneyMangement(defaultUser, moneyManagement),
                    direction,
                    ticker,
                    initiator,
                    DateTime.UtcNow,
                    defaultUser,
                    100m);
                var position = await new OpenPositionCommandHandler(exchangeService, accountService, tradingService)
                    .Handle(tradeCommand);

                var builder = new ComponentBuilder().WithButton("Close Position",
                    $"{Constants.DiscordButtonAction.ClosePosition}{_separator}{position.Identifier}");

                await component.Channel.SendMessageAsync(MessengerHelpers.GetPositionMessage(position),
                    components: builder.Build());
            }
        }


        private string GetClosingPositionMessage(Position position)
        {
            return $"Closing : {position.OriginDirection} {position.Open.Ticker} \n" +
                   $"Open Price : {position.Open.Price} \n" +
                   $"Closing Price : {position.Open.Price} \n" +
                   $"Quantity :{position.Open.Quantity} \n" +
                   $"PNL : {position.ProfitAndLoss.Realized} $";
        }

        private async Task ClosePosition(SocketMessageComponent component, string[] parameters)
        {
            var exchangeService = (IExchangeService)_services.GetService(typeof(IExchangeService));
            var accountService = (IAccountService)_services.GetService(typeof(IAccountService));
            var tradingService = (ITradingService)_services.GetService(typeof(ITradingService));
            var scopeFactory = (IServiceScopeFactory)_services.GetService(typeof(IServiceScopeFactory));

            await component.RespondAsync("Alright, let met few seconds to close this position");
            var position = await tradingService.GetPositionByIdentifierAsync(Guid.Parse(parameters[1]));
            var command = new ClosePositionCommand(position);
            var result =
                await new ClosePositionCommandHandler(exchangeService, accountService, tradingService, scopeFactory)
                    .Handle(command);
            var fields = new List<EmbedFieldBuilder>()
            {
                new EmbedFieldBuilder
                {
                    Name = "Direction",
                    Value = position.OriginDirection,
                    IsInline = true
                },
                new EmbedFieldBuilder
                {
                    Name = "Open Price",
                    Value = $"{position.Open.Price:#.##}",
                    IsInline = true
                },
                new EmbedFieldBuilder
                {
                    Name = "Quantity",
                    Value = $"{position.Open.Quantity:#.##}",
                    IsInline = true
                },
                new EmbedFieldBuilder
                {
                    Name = "Pnl",
                    Value = $"{position.ProfitAndLoss.Net:#.##}",
                    IsInline = true
                },
            };
            var embed = DiscordHelpers.GetEmbed(position.AccountName, $"Position status is now {result.Status}", fields,
                position.ProfitAndLoss.Net > 0 ? Color.Green : Color.Red);
            await component.Channel.SendMessageAsync("", embed: embed);
        }

        #endregion


        #region Out

        public async Task SendSignal(string message)
        {
            var channel = _client.GetChannel(_settings.SignalChannelId) as IMessageChannel;
            var builder = new ComponentBuilder().WithButton("Open Position", $"openposition{_separator}");
            await channel.SendMessageAsync(message, components: builder.Build());
        }

        public async Task SendSignal(string message, TradingExchanges exchange, Ticker ticker, TradeDirection direction,
            Timeframe timeframe)
        {
            var expirationDate = DateTime.Now.AddMinutes(_settings.ButtonExpirationMinutes).ToString("G");
            var channel = _client.GetChannel(_settings.SignalChannelId) as IMessageChannel;
            var builder = new ComponentBuilder().WithButton("Open Position",
                $"{Constants.DiscordButtonAction.OpenPosition}{_separator}{exchange}{_separator}{ticker}{_separator}{direction}{_separator}{timeframe}{_separator}{expirationDate}");
            await channel.SendMessageAsync(message, components: builder.Build());
        }

        public async Task SendIncreasePosition(string address, Trade trade, string copyAccountName,
            Trade? oldTrade = null)
        {
            var channel = _client.GetChannel(_settings.CopyTradingChannelId) as IMessageChannel;


            var fields = new List<EmbedFieldBuilder>()
            {
                new EmbedFieldBuilder
                {
                    Name = "Last size update",
                    Value = $"{trade.Date:s}",
                    IsInline = true
                },
                new EmbedFieldBuilder
                {
                    Name = "Entry Price",
                    Value = $"{trade.Price:#.##} $",
                    IsInline = true
                },
                new EmbedFieldBuilder
                {
                    Name = "Quantity",
                    Value = $"{trade.Quantity / trade.Leverage:#.##}",
                    IsInline = true
                },
                new EmbedFieldBuilder
                {
                    Name = "Leverage",
                    Value = $"x{trade.Leverage:#.##}",
                    IsInline = true
                }
            };

            if (oldTrade != null)
            {
                fields.Add(new EmbedFieldBuilder
                {
                    Name = "Increasy by", Value = $"{(trade.Quantity - oldTrade.Quantity) / trade.Leverage:#.##} $"
                });
            }

            var titlePrefix = oldTrade != null ? "Increase " : "";

            var builder = new ComponentBuilder();

            var moneyManagementService = (IMoneyManagementService)_services.GetService(typeof(IMoneyManagementService));
            // var moneyManagements = moneyManagementService.GetMoneyMangements();

            // TODO Update this to get the money management from the account
            var moneyManagements = new List<MoneyManagement>
            {
                new MoneyManagement
                {
                    Name = "MediumRisk",
                    Leverage = 1,
                    StopLoss = 0.021m,
                    TakeProfit = 0.042m,
                    Timeframe = Timeframe.FifteenMinutes
                }
            };
            foreach (var mm in moneyManagements)
            {
                var data = new CopyTradeData
                {
                    Direction = trade.Direction,
                    Ticker = trade.Ticker,
                    AccountName = copyAccountName,
                    ExpirationMinute = 10,
                    Leverage = trade.Leverage,
                };
                data.MoneyManagementName = mm.Name;
                var encodedData = MiscExtensions.Base64Encode(JsonConvert.SerializeObject(data));

                if (oldTrade == null)
                {
                    builder.WithButton($"Copy with {mm.Name}",
                        $"{Constants.DiscordButtonAction.CopyPosition}{_separator}{encodedData}");
                }
                else
                {
                    builder.WithButton($"Increase with {mm.Name}",
                        $"{Constants.DiscordButtonAction.CopyPosition}{_separator}{encodedData}");
                }
            }


            var embed = DiscordHelpers.GetEmbed(address, $"{titlePrefix}{trade.Direction} {trade.Ticker}", fields,
                trade.Direction == TradeDirection.Long ? Color.Green : Color.Red);
            await channel.SendMessageAsync("", components: builder.Build(), embed: embed);
        }

        public async Task SendPosition(string message, TradingExchanges exchange, Ticker ticker,
            TradeDirection direction, Timeframe timeframe)
        {
            var expirationDate = DateTime.Now.AddMinutes(_settings.ButtonExpirationMinutes).ToString("G");
            var channel = _client.GetChannel(_settings.SignalChannelId) as IMessageChannel;
            var builder = new ComponentBuilder().WithButton("Open Position",
                $"{Constants.DiscordButtonAction.OpenPosition}{_separator}{exchange}{_separator}{ticker}{_separator}{direction}{_separator}{timeframe}{_separator}{expirationDate}");
            await channel.SendMessageAsync(message, components: builder.Build());
        }

        public async Task SendMessage(string message)
        {
            var channel = _client.GetChannel(_settings.TradesChannelId) as IMessageChannel;
            await channel.SendMessageAsync(message);
        }

        public async Task SendClosingPosition(Position position)
        {
            var channel = _client.GetChannel(_settings.TradesChannelId) as IMessageChannel;
            await channel.SendMessageAsync(GetClosingPositionMessage(position));
        }

        public async Task SendTradeMessage(string message, bool isBadBehavior = false)
        {
            try
            {
                var channel =
                    _client.GetChannel(isBadBehavior ? _settings.TroublesChannelId : _settings.TradesChannelId) as
                        IMessageChannel;

                if (channel == null)
                {
                    _logger.LogError("Discord channel not found: {ChannelId}",
                        isBadBehavior ? _settings.TroublesChannelId : _settings.TradesChannelId);
                    return;
                }
                else
                {
                    await channel?.SendMessageAsync(message)!;
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
        }

        public async Task SendClosedPosition(string address, Trade oldTrade)
        {
            var fields = new List<EmbedFieldBuilder>()
            {
                new EmbedFieldBuilder
                {
                    Name = "Last size update",
                    Value = $"{oldTrade.Date:s}",
                    IsInline = true
                },
                new EmbedFieldBuilder
                {
                    Name = "Entry Price",
                    Value = $"{oldTrade.Price:#.##} $",
                    IsInline = true
                },
                new EmbedFieldBuilder
                {
                    Name = "Quantity",
                    Value = $"{oldTrade.Quantity / oldTrade.Leverage:#.##}",
                    IsInline = true
                },
                new EmbedFieldBuilder
                {
                    Name = "Leverage",
                    Value = $"x{oldTrade.Leverage:#.##}",
                    IsInline = true
                }
            };

            var embed = DiscordHelpers.GetEmbed(address, $"Closed {oldTrade.Direction} {oldTrade.Ticker}", fields,
                oldTrade.Direction == TradeDirection.Long ? Color.DarkGreen : Color.DarkRed);
            var channel = _client.GetChannel(_settings.CopyTradingChannelId) as IMessageChannel;
            await channel.SendMessageAsync("", embed: embed);
        }


        public async Task SendDecreasePosition(string address, Trade trade, decimal decreaseAmount)
        {
            var fields = new List<EmbedFieldBuilder>()
            {
                new EmbedFieldBuilder
                {
                    Name = "Last size update",
                    Value = $"{trade.Date:s}",
                    IsInline = true
                },
                new EmbedFieldBuilder
                {
                    Name = "Entry Price",
                    Value = $"{trade.Price:#.##}  $",
                    IsInline = true
                },
                new EmbedFieldBuilder
                {
                    Name = "Quantity",
                    Value = $"{trade.Quantity / trade.Leverage:#.##}",
                    IsInline = true
                },
                new EmbedFieldBuilder
                {
                    Name = "Leverage",
                    Value = $"x{trade.Leverage:#.##}",
                    IsInline = true
                },
                new EmbedFieldBuilder
                {
                    Name = "Decrease amount",
                    Value = $"{decreaseAmount:#.##} $"
                }
            };

            var embed = DiscordHelpers.GetEmbed(address, $"Decrease {trade.Direction} {trade.Ticker}", fields,
                Color.Blue);
            var channel = _client.GetChannel(_settings.CopyTradingChannelId) as IMessageChannel;
            await channel.SendMessageAsync("", embed: embed);
        }


        public async Task SendPosition(Position position)
        {
            var channel = _client.GetChannel(_settings.TradesChannelId) as IMessageChannel;
            var builder = new ComponentBuilder().WithButton("Close Position",
                $"{Constants.DiscordButtonAction.ClosePosition}{_separator}{position.Open.ExchangeOrderId}");
            await channel.SendMessageAsync(MessengerHelpers.GetPositionMessage(position), components: builder.Build());
        }

        public async Task SendBestTraders(List<Trader> traders)
        {
            var channel = _client.GetChannel(_settings.LeaderboardChannelId) as IMessageChannel;
            await channel.SendMessageAsync("", embed: DiscordHelpers.GetTradersEmbed(traders, "Leaderboard"));
        }

        public async Task SendBadTraders(List<Trader> traders)
        {
            var channel = _client.GetChannel(_settings.NoobiesboardChannelId) as IMessageChannel;
            await channel.SendMessageAsync("", embed: DiscordHelpers.GetTradersEmbed(traders, "Noobiesboard"));
        }

        public async Task SendDowngradedFundingRate(FundingRate fundingRate)
        {
            var channel = _client.GetChannel(_settings.FundingRateChannelId) as IMessageChannel;
            await channel.SendMessageAsync("",
                embed: DiscordHelpers.GetFundingRateEmbed(fundingRate, "Funding rate new opportunity"));
        }

        public Task SendNewTopFundingRate(FundingRate newRate)
        {
            var channel = _client.GetChannel(_settings.FundingRateChannelId) as IMessageChannel;
            return channel.SendMessageAsync("",
                embed: DiscordHelpers.GetFundingRateEmbed(newRate, "Funding rate new opportunity"));
        }

        public Task SendFundingRateUpdate(FundingRate oldRate, FundingRate newRate)
        {
            var channel = _client.GetChannel(_settings.FundingRateChannelId) as IMessageChannel;
            return channel.SendMessageAsync("",
                embed: DiscordHelpers.GetFundingRateEmbed(newRate, "Funding rate new opportunity", oldRate));
        }

        #endregion

        public class CopyTradeData
        {
            [JsonProperty(PropertyName = "D")] public TradeDirection Direction { get; set; }
            [JsonProperty(PropertyName = "T")] public Ticker Ticker { get; set; }
            [JsonProperty(PropertyName = "A")] public string AccountName { get; set; }
            [JsonProperty(PropertyName = "E")] public int ExpirationMinute { get; set; }
            [JsonProperty(PropertyName = "L")] public decimal Leverage { get; set; }
            [JsonProperty(PropertyName = "M")] public string MoneyManagementName { get; internal set; }
        }
    }
}