﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using Oxide.Ext.Discord.Attributes;
using Oxide.Ext.Discord.Builders;
using Oxide.Ext.Discord.Cache;
using Oxide.Ext.Discord.Clients;
using Oxide.Ext.Discord.Connections;
using Oxide.Ext.Discord.Constants;
using Oxide.Ext.Discord.Entities;
using Oxide.Ext.Discord.Extensions;
using Oxide.Ext.Discord.Interfaces;
using Oxide.Ext.Discord.Libraries;
using Oxide.Ext.Discord.Logging;
using Oxide.Ext.Discord.Types;

#if RUST
using UnityEngine;
#endif

namespace Oxide.Plugins
{
    // ReSharper disable once UnusedType.Global
    [Info("Discord PM", "MJSU", "3.0.0")]
    [Description("Allows private messaging through discord")]
    internal class DiscordPM : CovalencePlugin, IDiscordPlugin, IDiscordPool
    {
        #region Class Fields
        public DiscordClient Client { get; set; }
        public DiscordPluginPool Pool { get; set; }
        
        private PluginConfig _pluginConfig;

        private const string AccentColor = "de8732";
        private const string PmCommand = "pm";
        private const string ReplyCommand = "r";
        private const string NameArg = "name";
        private const string MessageArg = "message";

        private readonly DiscordPlaceholders _placeholders = GetLibrary<DiscordPlaceholders>();
        private readonly DiscordMessageTemplates _templates = GetLibrary<DiscordMessageTemplates>();
        private readonly DiscordCommandLocalizations _localizations = GetLibrary<DiscordCommandLocalizations>();

        private readonly Hash<string, IPlayer> _replies = new();

        private readonly BotConnection  _discordSettings = new()
        {
            Intents = GatewayIntents.Guilds
        };

        private DiscordChannel _logChannel;
        private DiscordApplicationCommand _pmCommand;
        
#if RUST
        private Effect _effect;
#endif
        #endregion

        #region Setup & Loading
        // ReSharper disable once UnusedMember.Local
        private void Init()
        {
            RegisterServerLangCommand(nameof(DiscordPmChatCommand), LangKeys.ChatPmCommand);
            RegisterServerLangCommand(nameof(DiscordPmChatReplyCommand), LangKeys.ChatReplyCommand);

            _discordSettings.ApiToken = _pluginConfig.DiscordApiKey;
            _discordSettings.LogLevel = _pluginConfig.ExtensionDebugging;
        }

        [HookMethod(DiscordExtHooks.OnDiscordClientCreated)]
        private void OnDiscordClientCreated()
        {
            Client.Connect(_discordSettings);
                
            RegisterPlaceholders();
            RegisterGlobalTemplates();
            RegisterEnTemplates();
            RegisterRuTemplates();
        }
        
        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                [LangKeys.Chat] = $"[#BEBEBE][[#{AccentColor}]{Title}[/#]] {PlaceholderKeys.Chat}[/#]",
                [LangKeys.ToFormat] = $"[#BEBEBE][#{AccentColor}]PM to {DefaultKeys.PlayerTarget.NameClan}:[/#] {PlaceholderKeys.Message}[/#]",
                [LangKeys.FromFormat] = $"[#BEBEBE][#{AccentColor}]PM from {DefaultKeys.Player.NameClan}:[/#] {PlaceholderKeys.Message}[/#]",
                [LangKeys.LogFormat] = $"{DefaultKeys.Player.NameClan} -> {DefaultKeys.PlayerTarget.NameClan}: {PlaceholderKeys.Message}",
                
                [LangKeys.InvalidPmSyntax] = $"Invalid Syntax. Type [#{AccentColor}]/{DefaultKeys.Plugin.Lang.WithFormat(LangKeys.ChatPmCommand)} MJSU Hi![/#]",
                [LangKeys.InvalidReplySyntax] = $"Invalid Syntax. Ex: [#{AccentColor}]/{DefaultKeys.Plugin.Lang.WithFormat(LangKeys.ChatReplyCommand)} Hi![/#]",
                [LangKeys.NoPreviousPm] = $"You do not have any previous discord PM's. Please use /{DefaultKeys.Plugin.Lang.WithFormat(LangKeys.ChatPmCommand)} to be able to use this command.",
                [LangKeys.NoPlayersFound] = $"No players found with the name '{PlaceholderKeys.NotFound}'",
                [LangKeys.MultiplePlayersFound] = $"Multiple players found with the name '{PlaceholderKeys.NotFound}'.",

                [LangKeys.ChatPmCommand] = "pm",
                [LangKeys.ChatReplyCommand] = "r",
            }, this);
            
            lang.RegisterMessages(new Dictionary<string, string>
            {
                [LangKeys.Chat] = $"[#BEBEBE][[#{AccentColor}]{Title}[/#]] {PlaceholderKeys.Chat}[/#]",
                [LangKeys.ToFormat] = $"[#BEBEBE][#{AccentColor}]ЛС для {DefaultKeys.PlayerTarget.NameClan}:[/#] {PlaceholderKeys.Message}[/#]",
                [LangKeys.FromFormat] = $"[#BEBEBE][#{AccentColor}]ЛС от {DefaultKeys.Player.NameClan}:[/#] {PlaceholderKeys.Message}[/#]",
                [LangKeys.LogFormat] = $"{DefaultKeys.Player.NameClan} -> {DefaultKeys.PlayerTarget.NameClan}: {PlaceholderKeys.Message}",
                
                [LangKeys.InvalidPmSyntax] = $"Недопустимый синтаксис. Введите [#{AccentColor}]/{DefaultKeys.Plugin.Lang.WithFormat(LangKeys.ChatPmCommand)} MJSU, привет![/#]",
                [LangKeys.InvalidReplySyntax] = $"Недопустимый синтаксис. Пример: [#{AccentColor}]/{DefaultKeys.Plugin.Lang.WithFormat(LangKeys.ChatReplyCommand)} Привет![/#]",
                [LangKeys.NoPreviousPm] = $"У вас нет предыдущих личных сообщений. Пожалуйста, используйте /{DefaultKeys.Plugin.Lang.WithFormat(LangKeys.ChatPmCommand)} чтобы иметь возможность использовать эту команду.",
                [LangKeys.NoPlayersFound] = $"Игроки с именем '{PlaceholderKeys.NotFound}' не найдены",
                [LangKeys.MultiplePlayersFound] = $"Найдено несколько игроков с именем '{PlaceholderKeys.NotFound}'.",

                [LangKeys.ChatPmCommand] = "pm",
                [LangKeys.ChatReplyCommand] = "r",
            }, this, "ru");
        }

        protected override void LoadDefaultConfig()
        {
            PrintWarning("Loading Default Config");
        }

        protected override void LoadConfig()
        {
            base.LoadConfig();
            Config.Settings.DefaultValueHandling = DefaultValueHandling.Populate;
            _pluginConfig = AdditionalConfig(Config.ReadObject<PluginConfig>());
            Config.WriteObject(_pluginConfig);
        }

        private PluginConfig AdditionalConfig(PluginConfig config)
        {
            config.Log = new LogSettings(config.Log);
            return config;
        }

        // ReSharper disable once UnusedMember.Local
        private void OnServerInitialized()
        {
            if (string.IsNullOrEmpty(_pluginConfig.DiscordApiKey))
            {
                PrintWarning("Please set the Discord Bot Token and reload the plugin");
                return;
            }

#if RUST
            if (_pluginConfig.EnableEffectNotification)
            {
                _effect = new Effect(_pluginConfig.EffectNotification, Vector3.zero, Vector3.zero);
                _effect.attached = true;
            }
#endif
        }
        #endregion

        #region Chat Commands
        // ReSharper disable once UnusedParameter.Local
        private void DiscordPmChatCommand(IPlayer player, string cmd, string[] args)
        {
            if (args.Length < 2)
            {
                Chat(player, LangKeys.InvalidPmSyntax, GetDefault());
                return;
            }

            IPlayer target;
            if (!TryFindPlayer(player, args[0], out target))
            {
                return;
            }

            _replies[player.Id] = target;
            _replies[target.Id] = player;

            string message = args.Length == 2 ? args[1] : string.Join(" ", args.Skip(1).ToArray());
            
            SendPrivateMessageFromServer(player, target, message);
        }

        // ReSharper disable once UnusedParameter.Local
        private void DiscordPmChatReplyCommand(IPlayer player, string cmd, string[] args)
        {
            if (args.Length < 1)
            {
                Chat(player, LangKeys.InvalidReplySyntax, GetDefault());
                return;
            }

            IPlayer target = _replies[player.Id];
            if (target == null)
            {
                Chat(player, LangKeys.NoPreviousPm, GetDefault());
                return;
            }
            
            string message = args.Length == 1 ? args[0] : string.Join(" ", args);
            SendPrivateMessageFromServer(player, target, message);
        }
        
        public void SendPrivateMessageFromServer(IPlayer sender, IPlayer target, string message)
        {
            using PlaceholderData data = GetPmDefault(sender, target, message);
            data.ManualPool();
            SendPlayerPrivateMessage(sender, LangKeys.ToFormat, TemplateKeys.Messages.To, data);
            SendPlayerPrivateMessage(target, LangKeys.FromFormat, TemplateKeys.Messages.From, data);
            LogPrivateMessage(data);
        }
        #endregion

        #region Discord Setup
        // ReSharper disable once UnusedMember.Local
        [HookMethod(DiscordExtHooks.OnDiscordGatewayReady)]
        private void OnDiscordGatewayReady()
        {
            RegisterApplicationCommands();
            Puts($"{Title} Ready");
        }

        // ReSharper disable once UnusedMember.Local
        [HookMethod(DiscordExtHooks.OnDiscordGuildCreated)]
        private void OnDiscordGuildCreated(DiscordGuild guild)
        {
            _logChannel = guild.Channels[_pluginConfig.Log.LogToChannelId];
        }

        public void RegisterApplicationCommands()
        {
            CreatePmCommand();
            CreateReplyCommand();
        }

        public void CreatePmCommand()
        {
            ApplicationCommandBuilder pmCommand = new ApplicationCommandBuilder(PmCommand, "Private message a player", ApplicationCommandType.ChatInput)
                                                  .AddDefaultPermissions(PermissionFlags.None)
                                                  .AllowInDirectMessages(_pluginConfig.AllowInDm);
            AddCommandNameOption(pmCommand);
            AddCommandMessageOption(pmCommand);

            CommandCreate cmd = pmCommand.Build();
            DiscordCommandLocalization localization = pmCommand.BuildCommandLocalization();

            TemplateKey template = new("PM.Command");
            _localizations.RegisterCommandLocalizationAsync(this, template, localization, new TemplateVersion(1, 0, 0), new TemplateVersion(1, 0, 0)).Then(_ =>
            {
                _localizations.ApplyCommandLocalizationsAsync(this, cmd, template).Then(() =>
                {
                    Client.Bot.Application.CreateGlobalCommand(Client, cmd).Then(c => _pmCommand = c);
                });
            });
        }

        public void CreateReplyCommand()
        {
            ApplicationCommandBuilder replyCommand = new ApplicationCommandBuilder(ReplyCommand, "Reply to the last received private message", ApplicationCommandType.ChatInput)
                                                     .AddDefaultPermissions(PermissionFlags.None)
                                                     .AllowInDirectMessages(_pluginConfig.AllowInDm);
            AddCommandMessageOption(replyCommand);

            CommandCreate cmd = replyCommand.Build();
            DiscordCommandLocalization localization = replyCommand.BuildCommandLocalization();

            TemplateKey template = new("Reply.Command");
            _localizations.RegisterCommandLocalizationAsync(this, template, localization, new TemplateVersion(1, 0, 0), new TemplateVersion(1, 0, 0)).Then(_ =>
            {
                _localizations.ApplyCommandLocalizationsAsync(this, cmd, template).Then(() =>
                {
                    Client.Bot.Application.CreateGlobalCommand(Client, cmd);
                });
            });
        }

        public void AddCommandNameOption(ApplicationCommandBuilder builder)
        {
            builder.AddOption(CommandOptionType.String, NameArg, "Name of the player",
                options => options.Required().AutoComplete());
        }
        
        public void AddCommandMessageOption(ApplicationCommandBuilder builder)
        {
            builder.AddOption(CommandOptionType.String, MessageArg, "Message to send the player",
                options => options.Required());
        }
        #endregion

        #region Discord Commands
        // ReSharper disable once UnusedMember.Local
        [DiscordApplicationCommand(PmCommand)]
        private void HandlePmCommand(DiscordInteraction interaction, InteractionDataParsed parsed)
        {
            IPlayer player = interaction.User.Player;
            if (player == null)
            {
                interaction.CreateTemplateResponse(Client, InteractionResponseType.ChannelMessageWithSource, TemplateKeys.Errors.UnlinkedUser, GetInteractionCallback(interaction), GetDefault());
                return;
            }

            string targetId = parsed.Args.GetString(NameArg);
            string message = parsed.Args.GetString(MessageArg);

            IPlayer target = players.FindPlayerById(targetId);
            if (target == null)
            {
                interaction.CreateTemplateResponse(Client, InteractionResponseType.ChannelMessageWithSource, TemplateKeys.Errors.InvalidAutoCompleteSelection, GetInteractionCallback(interaction), GetDefault());
                return;
            }

            _replies[player.Id] = target;
            _replies[target.Id] = player;

            SendPrivateMessageFromDiscord(interaction, player, target, message);
        }
        
        // ReSharper disable once UnusedMember.Local
        [DiscordApplicationCommand(ReplyCommand)]
        private void HandleReplyCommand(DiscordInteraction interaction, InteractionDataParsed parsed)
        {
            IPlayer player = interaction.User.Player;
            if (player == null)
            {
                interaction.CreateTemplateResponse(Client, InteractionResponseType.ChannelMessageWithSource, TemplateKeys.Errors.UnlinkedUser, GetInteractionCallback(interaction), GetDefault());
                return;
            }
            
            string message = parsed.Args.GetString(MessageArg);

            IPlayer target = _replies[player.Id];
            if (target == null)
            {
                interaction.CreateTemplateResponse(Client, InteractionResponseType.ChannelMessageWithSource, TemplateKeys.Errors.NoPreviousPm, GetInteractionCallback(interaction), GetDefault().AddCommand(_pmCommand));
                return;
            }
            
            _replies[target.Id] = player;
            
            SendPrivateMessageFromDiscord(interaction, player, target, message);
        }
        
        public void SendPrivateMessageFromDiscord(DiscordInteraction interaction, IPlayer player, IPlayer target, string message)
        {
            using PlaceholderData data = GetPmDefault(player, target, message);
            data.ManualPool();
            if (!interaction.GuildId.HasValue)
            {
                ServerPrivateMessage(player, LangKeys.ToFormat, data);
            }
            else
            {
                SendPlayerPrivateMessage(player, LangKeys.ToFormat, TemplateKeys.Messages.To, data);
            }
            interaction.CreateTemplateResponse(Client, InteractionResponseType.ChannelMessageWithSource, TemplateKeys.Messages.To, GetInteractionCallback(interaction), GetPmDefault(player, target, message));
            SendPlayerPrivateMessage(target, LangKeys.FromFormat, TemplateKeys.Messages.From, data);
            LogPrivateMessage(data);
        }

        // ReSharper disable once UnusedMember.Local
        [DiscordAutoCompleteCommand(PmCommand, NameArg)]
        private void HandleNameAutoComplete(DiscordInteraction interaction, InteractionDataOption focused)
        {
            string search = focused.GetString();
            InteractionAutoCompleteBuilder response = interaction.GetAutoCompleteBuilder();
            response.AddAllOnlineFirstPlayers(search, PlayerNameFormatter.ClanName);
            interaction.CreateResponse(Client, response);
        }

        public InteractionCallbackData GetInteractionCallback(DiscordInteraction interaction)
        {
            return new InteractionCallbackData
            {
                Flags = interaction.GuildId.HasValue ? MessageFlags.Ephemeral : MessageFlags.None
            };
        }
        #endregion

        #region Discord Placeholders
        public void RegisterPlaceholders()
        {
            _placeholders.RegisterPlaceholder<string>(this, PlaceholderKeys.Message,  PlaceholderDataKeys.Message);
            _placeholders.RegisterPlaceholder<string>(this, PlaceholderKeys.NotFound,  PlaceholderDataKeys.PlayerName);
            _placeholders.RegisterPlaceholder<string>(this, PlaceholderKeys.Chat,  PlaceholderDataKeys.Chat);
        }

        public PlaceholderData GetPmDefault(IPlayer from, IPlayer to, string message)
        {
            return GetDefault()
                   .AddPlayer(from)
                   .AddTarget(to)
                   .Add(PlaceholderDataKeys.Message, message);
        }
        
        public PlaceholderData GetDefault()
        {
            return _placeholders.CreateData(this);
        }
        #endregion

        #region Discord Templates
        public void RegisterGlobalTemplates()
        {
            DiscordMessageTemplate logMessage = CreateTemplateEmbed($"[{DefaultKeys.TimestampNow.ShortTime}] {DefaultKeys.Player.NameClan} -> {DefaultKeys.PlayerTarget.NameClan}: {PlaceholderKeys.Message}", DiscordColor.Danger);
            _templates.RegisterGlobalTemplateAsync(this, TemplateKeys.Messages.Log, logMessage, new TemplateVersion(1, 0, 0), new TemplateVersion(1, 0 ,0));
        }
        
        public void RegisterEnTemplates()
        {
            DiscordMessageTemplate toMessage = CreateTemplateEmbed($"[{DefaultKeys.TimestampNow.ShortTime}] PM to {DefaultKeys.PlayerTarget.NameClan}: {PlaceholderKeys.Message}", DiscordColor.Success);
            _templates.RegisterLocalizedTemplateAsync(this, TemplateKeys.Messages.To, toMessage, new TemplateVersion(1, 0, 0), new TemplateVersion(1, 0 ,0));
            
            DiscordMessageTemplate fromMessage = CreateTemplateEmbed($"[{DefaultKeys.TimestampNow.ShortTime}] PM from {DefaultKeys.Player.NameClan}: {PlaceholderKeys.Message}", DiscordColor.Danger);
            _templates.RegisterLocalizedTemplateAsync(this, TemplateKeys.Messages.From, fromMessage, new TemplateVersion(1, 0, 0), new TemplateVersion(1, 0 ,0));       
            
            DiscordMessageTemplate errorUnlinkedUser = CreatePrefixedTemplateEmbed("You cannot use this command until you're have linked your game and discord accounts", DiscordColor.Danger);
            _templates.RegisterLocalizedTemplateAsync(this, TemplateKeys.Errors.UnlinkedUser, errorUnlinkedUser, new TemplateVersion(1, 0, 0), new TemplateVersion(1, 0 ,0));
            
            DiscordMessageTemplate errorInvalidAutoComplete = CreatePrefixedTemplateEmbed("The name you have picked does not appear to be a valid auto complete value. Please make sure you select one of the auto complete options.", DiscordColor.Danger);
            _templates.RegisterLocalizedTemplateAsync(this, TemplateKeys.Errors.InvalidAutoCompleteSelection, errorInvalidAutoComplete, new TemplateVersion(1, 0, 0), new TemplateVersion(1, 0 ,0));
            
            DiscordMessageTemplate errorNoPreviousPm = CreatePrefixedTemplateEmbed($"You do not have any previous discord PM's. Please use {DefaultKeys.AppCommand.Mention} to be able to use this command.", DiscordColor.Danger);
            _templates.RegisterLocalizedTemplateAsync(this, TemplateKeys.Errors.NoPreviousPm, errorNoPreviousPm, new TemplateVersion(1, 0, 0), new TemplateVersion(1, 0 ,0));
        }

        public void RegisterRuTemplates()
        {
            DiscordMessageTemplate toMessage = CreateTemplateEmbed($"[{DefaultKeys.TimestampNow.ShortTime}] ЛС для {DefaultKeys.PlayerTarget.NameClan}: {PlaceholderKeys.Message}", DiscordColor.Success);
            _templates.RegisterLocalizedTemplateAsync(this, TemplateKeys.Messages.To, toMessage, new TemplateVersion(1, 0, 0), new TemplateVersion(1, 0 ,0), "ru");
            
            DiscordMessageTemplate fromMessage = CreateTemplateEmbed($"[{DefaultKeys.TimestampNow.ShortTime}] ЛС от {DefaultKeys.Player.NameClan}: {PlaceholderKeys.Message}", DiscordColor.Danger);
            _templates.RegisterLocalizedTemplateAsync(this, TemplateKeys.Messages.From, fromMessage, new TemplateVersion(1, 0, 0), new TemplateVersion(1, 0 ,0), "ru");       
            
            DiscordMessageTemplate errorUnlinkedUser = CreatePrefixedTemplateEmbed("Вы не можете использовать эту команду, пока не свяжете игровой аккаунт с аккаунтом Discord.", DiscordColor.Danger);
            _templates.RegisterLocalizedTemplateAsync(this, TemplateKeys.Errors.UnlinkedUser, errorUnlinkedUser, new TemplateVersion(1, 0, 0), new TemplateVersion(1, 0 ,0), "ru");
            
            DiscordMessageTemplate errorInvalidAutoComplete = CreatePrefixedTemplateEmbed("Имя, которое вы выбрали, не является допустимым значением автозаполнения. Пожалуйста, убедитесь, что вы выбираете один из вариантов автозаполнения.", DiscordColor.Danger);
            _templates.RegisterLocalizedTemplateAsync(this, TemplateKeys.Errors.InvalidAutoCompleteSelection, errorInvalidAutoComplete, new TemplateVersion(1, 0, 0), new TemplateVersion(1, 0 ,0), "ru");
            
            DiscordMessageTemplate errorNoPreviousPm = CreatePrefixedTemplateEmbed($"У вас нет предыдущих личных сообщений. Пожалуйста, используйте {DefaultKeys.AppCommand.Mention}, чтобы использовать эту команду.", DiscordColor.Danger);
            _templates.RegisterLocalizedTemplateAsync(this, TemplateKeys.Errors.NoPreviousPm, errorNoPreviousPm, new TemplateVersion(1, 0, 0), new TemplateVersion(1, 0 ,0), "ru");
        }
        
        public DiscordMessageTemplate CreateTemplateEmbed(string description, DiscordColor color)
        {
            return new DiscordMessageTemplate
            {
                Embeds = new List<DiscordEmbedTemplate>
                {
                    new()
                    {
                        Description = description,
                        Color = color.ToHex()
                    }
                }
            };
        }
        
        public DiscordMessageTemplate CreatePrefixedTemplateEmbed(string description, DiscordColor color)
        {
            return new DiscordMessageTemplate
            {
                Embeds = new List<DiscordEmbedTemplate>
                {
                    new()
                    {
                        Description = $"[{DefaultKeys.Plugin.Title}] {description}",
                        Color = color.ToHex()
                    }
                }
            };
        }
        #endregion
        
        #region Helpers
        public bool TryFindPlayer(IPlayer from, string name, out IPlayer target)
        {
            List<IPlayer> foundPlayers = Pool.GetList<IPlayer>();
            List<IPlayer> activePlayers = Pool.GetList<IPlayer>();
            List<IPlayer> linkedPlayers = Pool.GetList<IPlayer>();
            
            try
            {
                foreach (IPlayer player in ServerPlayerCache.Instance.GetOnlinePlayers(name))
                {
                    if (player.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
                    {
                        target = player;
                        return true;
                    }
                
                    activePlayers.Add(player);
                }
            
                if (activePlayers.Count == 1)
                {
                    target = activePlayers[0];
                    return true;
                }
                
                IPlayer match = null;
                bool multiple = false;
                foreach (IPlayer player in ServerPlayerCache.Instance.GetAllPlayers(name))
                {
                    if (!multiple && player.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
                    {
                        if (match == null)
                        {
                            match = player;
                        }
                        else
                        {
                            match = null;
                            multiple = true;
                        }
                    }
                
                    foundPlayers.Add(player);

                    if (player.IsLinked())
                    {
                        linkedPlayers.Add(player);
                    }

                    if (foundPlayers.Count > 1 && linkedPlayers.Count > 1)
                    {
                        break;
                    }
                }

                if (!multiple && match != null)
                {
                    target = match;
                    return true;
                }
                
                target = null;
                if (foundPlayers.Count == 1)
                {
                    target = foundPlayers[0];
                    return true;
                }
                
                if (linkedPlayers.Count == 1)
                {
                    target = linkedPlayers[0];
                    return true;
                }

                if (foundPlayers.Count > 1)
                {
                    Chat(from, LangKeys.MultiplePlayersFound, GetDefault().Add(PlaceholderDataKeys.PlayerName, name));
                    return false;
                }
                
                Chat(from, LangKeys.NoPlayersFound, GetDefault().Add(PlaceholderDataKeys.PlayerName, name));
                return false;
            }
            finally
            { 
                Pool.FreeList(foundPlayers);
                Pool.FreeList(linkedPlayers);
                Pool.FreeList(activePlayers);
            }
        }

        public void SendPlayerPrivateMessage(IPlayer player, string serverLang, TemplateKey templateKey, PlaceholderData data)
        {
            ServerPrivateMessage(player, serverLang, data);
            DiscordPrivateMessage(player, templateKey, data);
        }

        public void LogPrivateMessage(PlaceholderData data)
        {
            LogSettings settings = _pluginConfig.Log;
            if (!settings.LogToConsole && !settings.LogToFile && _logChannel == null)
            {
                return;
            }

            string log = Lang(LangKeys.LogFormat, null, data);
            if (_pluginConfig.Log.LogToConsole)
            {
                Puts(log);
            }

            if (_pluginConfig.Log.LogToFile)
            {
                LogToFile(string.Empty, log, this);
            }

            _logChannel?.CreateGlobalTemplateMessage(Client, TemplateKeys.Messages.Log, null, data);
        }
        
        public void SendEffectToPlayer(IPlayer player)
        {
#if RUST
            if (!_pluginConfig.EnableEffectNotification)
            {
                return;
            }
            
            if (!player.IsConnected)
            {
                return;
            }
            
            BasePlayer basePlayer = player.Object as BasePlayer;
            if (basePlayer == null)
            {
                return;
            }
            
            Effect effect = _effect;
            effect.entity = basePlayer.net.ID;
            
            EffectNetwork.Send(effect, basePlayer.net.connection);
#endif
        }
        
        public void ServerPrivateMessage(IPlayer player, string langKey, PlaceholderData data)
        {
            if (player.IsConnected && player.Object != null)
            {
                player.Message(Lang(langKey, player, data));
                SendEffectToPlayer(player);
            }
        }

        public void DiscordPrivateMessage(IPlayer player, TemplateKey templateKey, PlaceholderData data)
        {
            if (player.IsLinked())
            {
                player.SendDiscordTemplateMessage(Client, templateKey, null, data);
            }
        }

        public void RegisterServerLangCommand(string command, string langKey)
        {
            HashSet<string> registered = new();
            foreach (string langType in lang.GetLanguages(this))
            {
                Dictionary<string, string> langKeys = lang.GetMessages(langType, this);
                if (langKeys.TryGetValue(langKey, out string commandValue) && !string.IsNullOrEmpty(commandValue) && registered.Add(commandValue))
                {
                    AddCovalenceCommand(commandValue, command);
                }
            }
        }

        public void Chat(IPlayer player, string key, PlaceholderData data)
        {
            if (player.IsConnected)
            {
                player.Reply(Lang(LangKeys.Chat, player, GetDefault().Add(PlaceholderDataKeys.Chat, Lang(key, player, data))));
            }
        }
        
        public string Lang(string key, IPlayer player = null) => lang.GetMessage(key, this, player?.Id);

        public string Lang(string key, IPlayer player, PlaceholderData data)
        {
            string result = _placeholders.ProcessPlaceholders(Lang(key, player), data);
            return result;
        }
        #endregion

        #region Classes
        private class PluginConfig
        {
            [DefaultValue("")]
            [JsonProperty(PropertyName = "Discord Bot Token")]
            public string DiscordApiKey { get; set; }

            [DefaultValue(true)]
            [JsonProperty(PropertyName = "Allow Discord Commands In Direct Messages")]
            public bool AllowInDm { get; set; }

#if RUST
            [DefaultValue(true)]
            [JsonProperty(PropertyName = "Enable Effect Notification")]
            public bool EnableEffectNotification { get; set; }
            
            [DefaultValue("assets/prefabs/tools/pager/effects/vibrate.prefab")]
            [JsonProperty(PropertyName = "Notification Effect")]
            public string EffectNotification { get; set; }
#endif

            [JsonProperty(PropertyName = "Log Settings")]
            public LogSettings Log { get; set; }
            
            [JsonConverter(typeof(StringEnumConverter))]
            [DefaultValue(DiscordLogLevel.Info)]
            [JsonProperty(PropertyName = "Discord Extension Log Level (Verbose, Debug, Info, Warning, Error, Exception, Off)")]
            public DiscordLogLevel ExtensionDebugging { get; set; }
        }
        
        public class LogSettings
        {
            [JsonProperty(PropertyName = "Log To Console")]
            public bool LogToConsole { get; set; }

            [JsonProperty(PropertyName = "Log To File")]
            public bool LogToFile { get; set; }
            
            [JsonProperty(PropertyName = "Log To Channel ID")]
            public Snowflake LogToChannelId { get; set; }

            public LogSettings(LogSettings settings)
            {
                LogToConsole = settings?.LogToConsole ?? true;
                LogToFile = settings?.LogToFile ?? false;
                LogToChannelId = settings?.LogToChannelId ?? default(Snowflake);
            }
        }

        private static class LangKeys
        {
            public const string Base = "V3.";
            
            public const string Chat = Base + nameof(Chat);
            public const string FromFormat = Base + nameof(FromFormat);
            public const string ToFormat = Base + nameof(ToFormat);
            public const string InvalidPmSyntax = Base + nameof(InvalidPmSyntax);
            public const string InvalidReplySyntax = Base + nameof(InvalidReplySyntax);
            public const string NoPreviousPm = Base + nameof(NoPreviousPm);
            public const string MultiplePlayersFound =  Base + nameof(MultiplePlayersFound);
            public const string NoPlayersFound = Base + nameof(NoPlayersFound);
            public const string LogFormat = Base + nameof(LogFormat);

            public const string ChatPmCommand = Base + "Commands.Chat.PM";
            public const string ChatReplyCommand = Base + "Commands.Chat.Reply";
        }

        private static class TemplateKeys
        {
            public static class Messages
            {
                private const string Base = nameof(Messages) + ".";
                
                public static readonly TemplateKey To = new(Base + nameof(To));
                public static readonly TemplateKey From = new(Base + nameof(From));
                public static readonly TemplateKey Log = new(Base + nameof(Log));
            }
            
            public static class Errors
            {
                private const string Base = nameof(Errors) + ".";

                public static readonly TemplateKey UnlinkedUser = new(Base + nameof(UnlinkedUser));
                public static readonly TemplateKey InvalidAutoCompleteSelection = new(Base + nameof(InvalidAutoCompleteSelection));
                public static readonly TemplateKey NoPreviousPm = new(Base + nameof(NoPreviousPm));
            }
        }

        private static class PlaceholderKeys
        {
            public static readonly PlaceholderKey Message = new(nameof(DiscordPM), "message");
            public static readonly PlaceholderKey Chat = new(nameof(DiscordPM), "chat");
            public static readonly PlaceholderKey NotFound = new(nameof(DiscordPM), "player.notfound");
        }

        private class PlaceholderDataKeys
        {
            public static readonly PlaceholderDataKey Message = new("pm.message");
            public static readonly PlaceholderDataKey PlayerName = new("pm.name");
            public static readonly PlaceholderDataKey Chat = new("pm.chat");
        }
        #endregion
    }
}