using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using ConVar;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using UnityEngine;
using Random = System.Random;
using Time = Oxide.Core.Libraries.Time;

namespace Oxide.Plugins
{
    [Info("Smart Chat Bot", "Iv Misticos", "2.0.13")]
    [Description("I send chat messages based on some triggers or time.")]
    class SmartChatBot : RustPlugin
    {
        #region Variables

        [PluginReference]
        // ReSharper disable once InconsistentNaming
        private Plugin PlaceholderAPI = null;

        private static readonly Random Random = new Random();
        private readonly Dictionary<BasePlayer, uint> _lastSent = new Dictionary<BasePlayer, uint>();
        private uint _lastSentGlobal;

        private static readonly Time Time = GetLibrary<Time>();

        private const string CountryRequest = "http://ip-api.com/json/{ip}?fields=country,countryCode,status";
        
        #endregion
        
        #region Configuration

        private Configuration _config = new Configuration();

        private class Configuration
        {
            [JsonProperty(PropertyName = "Chat Prefix")]
            public string Prefix = "<color=#787FFF>Bot </color>";

            [JsonProperty(PropertyName = "Show Chat Prefix")]
            public bool ShowPrefix = true;

            [JsonProperty(PropertyName = "Bot Icon (SteamID)")]
            public ulong ChatSteamId = 0;

            [JsonProperty(PropertyName = "Cooldown Between Auto Responses For User")]
            public string Cooldown = "10s";

            [JsonProperty(PropertyName = "Global Cooldown Between Auto Responses")]
            public string CooldownGlobal = "2s";

            [JsonProperty(PropertyName = "Use Default Chat (0), Chat Plus (1), Better Chat (2)")]
            public ushort ChatSystem = 0;

            [JsonIgnore] public uint ParsedCooldown;
            [JsonIgnore] public uint ParsedCooldownGlobal;

            [JsonProperty(PropertyName = "Debug")]
            public bool Debug = false;

            [JsonProperty(PropertyName = "Allow Multiple Auto Responses")]
            public bool MultipleAutoResponses = false;

            [JsonProperty(PropertyName = "Minimal Time Between Message And Answer")]
            public float MinTime = 1.0f;

            [JsonProperty(PropertyName = "Maximal Time Between Message And Answer")]
            public float MaxTime = 3.0f;

            [JsonProperty(PropertyName = "Welcome Message", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<string> WelcomeMessage = new List<string> { "Welcome, {name}!", "Hello, dear {name}!", "Hello, {name}! Your IP: { ip }" };

            [JsonProperty(PropertyName = "Welcome Message Enabled")]
            public bool WelcomeMessageEnabled = false;

            [JsonProperty(PropertyName = "Joining Message", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<string> JoiningMessage = new List<string> { "Welcome, {name} ({id}, { ip })!", "Hello, dear {name} ({id}, { ip })!", "{name} came from {country} ({countrycode})" };

            [JsonProperty(PropertyName = "Leaving Message", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<string> LeavingMessage = new List<string> { "Bye, {name} ({id}, { ip })!\nReason: {reason}", "{name} ({id}, { ip }) left the game. Reason: {reason}", "{name} from {country} ({countrycode}) just left the game!" };

            [JsonProperty(PropertyName = "Joining Message Enabled")]
            public bool JoiningMessageEnabled = false;

            [JsonProperty(PropertyName = "Leaving Message Enabled")]
            public bool LeavingMessageEnabled = false;

            [JsonProperty(PropertyName = "Show Joining Message To Player That Joined")]
            public bool JoiningMessageSelfEnabled = false;

            [JsonProperty(PropertyName = "Show Leaving Message To Player That Left")]
            public bool LeavingMessageSelfEnabled = false;
            
            [JsonProperty(PropertyName = "Auto Messages", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<AutoMessageGroup> AutoMessages = new List<AutoMessageGroup> { new AutoMessageGroup() };
            
            [JsonProperty(PropertyName = "Auto Responses", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<AutoResponseGroup> AutoResponses = new List<AutoResponseGroup> { new AutoResponseGroup() };
        }

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                _config = Config.ReadObject<Configuration>();
                if (_config == null) throw new Exception();
            }
            catch
            {
                PrintError("Your configuration file contains an error. Using default configuration values.");
                LoadDefaultConfig();
            }
        }

        protected override void LoadDefaultConfig() => _config = new Configuration();

        protected override void SaveConfig() => Config.WriteObject(_config);

        private class AutoMessageGroup
        {
            [JsonProperty(PropertyName = "Permission")]
            public string Permission = "smartchatbot.messages";
            
            [JsonProperty(PropertyName = "Message Frequency")]
            public string Frequency = "5m";

            [JsonIgnore] public uint ParsedFrequency;
            [JsonIgnore] public short ActiveMessage;

            [JsonProperty(PropertyName = "Auto Messages", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<AutoMessage> AutoMessages = new List<AutoMessage> { new AutoMessage() };
        }

        private class AutoMessage
        {
            [JsonProperty(PropertyName = "Is Enabled")]
            public bool Enabled = true;

            [JsonProperty(PropertyName = "Message")]
            public string Message = "Do not mind, I am just a stupid bot.";
        }

        private class AutoResponseGroup
        {
            [JsonProperty(PropertyName = "Permission")]
            public string Permission = "smartchatbot.response";
            
            [JsonProperty(PropertyName = "Auto Responses", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<AutoResponse> AutoResponses = new List<AutoResponse> { new AutoResponse() };
        }

        private class AutoResponse
        {
            [JsonProperty(PropertyName = "Is Enabled")]
            public bool Enabled = true;
            
            [JsonProperty(PropertyName = "Remove Message From Sender")]
            public bool RemoveMessage = false;
            
            [JsonProperty(PropertyName = "Send Response For Everyone (true) or Only For Sender (false)")]
            public bool SendPublic = true;
            
            [JsonProperty(PropertyName = "Triggers", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<AutoResponseTrigger> Triggers = new List<AutoResponseTrigger> { new AutoResponseTrigger() };

            [JsonProperty(PropertyName = "Answers", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<string> Answers = new List<string> { "This bot really works.", "IT WORKS OMG!" };

            public bool IsValid() => Triggers.Count > 0 && Answers.Count > 0;
        }

        private class AutoResponseTrigger
        {
            [JsonProperty(PropertyName = "Percentage Of Contained Words")]
            public float ContainedWordsPercentage = 0.75f;

            [JsonProperty(PropertyName = "Regex Enabled")]
            public bool Regex = false;
            
            [JsonProperty(PropertyName = "Words", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<string> Words = new List<string> { "How", "this", "bot", "works" };
        }
        
        #endregion
        
        #region Hooks

        private void OnServerInitialized()
        {
            LoadConfig();
            Unsubscribe(nameof(OnChatPlusMessage));
            Unsubscribe(nameof(OnBetterChat));
            Unsubscribe(nameof(OnPlayerChat));

            if (_config.ChatSystem == 0)
                Subscribe(nameof(OnPlayerChat));
            else if (_config.ChatSystem == 1)
                Subscribe(nameof(OnChatPlusMessage));
            else if (_config.ChatSystem == 2)
                Subscribe(nameof(OnBetterChat));

            var pl = plugins.GetAll();
            var plCount = pl.Length;
            for (var i = 0; i < plCount; i++)
            {
                var p = pl[i];
                // ReSharper disable once ConvertIfStatementToSwitchStatement
                if (p.Name == "BetterChat" || p.Name == "Better Chat")
                    PrintWarning("Detected Better Chat. Make sure you enabled it in your configuration.");
                else if (p.Name == "ChatPlus" || p.Name == "Chat Plus")
                    PrintWarning("Detected Chat Plus. Make sure you enabled it in your configuration.");
            }

            if (!ConvertToSeconds(_config.Cooldown, out _config.ParsedCooldown))
            {
                PrintError($"Unable to convert \"{_config.Cooldown}\" to seconds!");
                _config.ParsedCooldown = 0;
            }

            if (!ConvertToSeconds(_config.CooldownGlobal, out _config.ParsedCooldownGlobal))
            {
                PrintError($"Unable to convert \"{_config.CooldownGlobal}\" to seconds!");
                _config.ParsedCooldownGlobal = 0;
            }

            var messageGroupsCount = _config.AutoMessages.Count;
            for (var i = 0; i < messageGroupsCount; i++)
            {
                var messageGroup = _config.AutoMessages[i];
                PrintDebug($"Handling Message Group (ID: {i})");
                
                // Time Handling
                if (!ConvertToSeconds(messageGroup.Frequency, out messageGroup.ParsedFrequency))
                {
                    PrintError($"Unable to convert \"{messageGroup.Frequency}\" to seconds!");
                    messageGroup.ParsedFrequency = 60;
                }
                
                // Permissions
                permission.RegisterPermission(messageGroup.Permission, this);
                
                // Timers
                timer.Every(messageGroup.ParsedFrequency, () => HandleBroadcast(messageGroup));
            }

            var responseGroupsCount = _config.AutoResponses.Count;
            for (var i = 0; i < responseGroupsCount; i++)
            {
                PrintDebug($"Handling Response Group (ID: {i})");
                var responseGroup = _config.AutoResponses[i];
                
                // Permissions
                permission.RegisterPermission(responseGroup.Permission, this);
            }
        }
 
        // ReSharper disable once SuggestBaseTypeForParameter
        private object OnChatPlusMessage(Dictionary<string, object> data)
        {
            PrintDebug("Called OnChatPlusMessage");
            object playerObj, messageObj;
            if (!data.TryGetValue("Player", out playerObj) || !data.TryGetValue("Message", out messageObj))
                return null;
            
            var player = BasePlayer.Find((playerObj as IPlayer)?.Id);
            var message = messageObj?.ToString();
            if (player == null || !player.IsConnected || string.IsNullOrEmpty(message))
                return null;
            
            return HandleChatMessage(player, message);
        }

        private object OnChatPlusMessage(BasePlayer player, string message)
        {
            PrintDebug("Called OnChatPlusMessage");
            return HandleChatMessage(player, message);
        }

        // ReSharper disable once SuggestBaseTypeForParameter
        private object OnBetterChat(Dictionary<string, object> data)
        {
            PrintDebug("Called OnBetterChat");
            object playerObj, messageObj, chatChannel;
            if (!data.TryGetValue("Player", out playerObj) || !data.TryGetValue("Message", out messageObj) ||
                !data.TryGetValue("ChatChannel", out chatChannel))
            {
                return null;
            }

            if ((Chat.ChatChannel) chatChannel != Chat.ChatChannel.Global)
            {
                return null;
            }
            
            var player = BasePlayer.Find((playerObj as IPlayer)?.Id);
            var message = messageObj?.ToString();
            if (player == null || !player.IsConnected || string.IsNullOrEmpty(message))
                return null;

            if (HandleChatMessage(player, message) == null)
                return null;
            
            data["CancelOption"] = 2;
            return null;
        }

        private object OnPlayerChat(BasePlayer player, string message, Chat.ChatChannel channel)
        {
            PrintDebug("Called OnPlayerChat");
            if (player == null || string.IsNullOrEmpty(message) || channel != Chat.ChatChannel.Global)
                return null;

            return HandleChatMessage(player, message);
        }

        private void OnPlayerConnected(BasePlayer player)
        {
            if (_config.JoiningMessage.Count > 0 && _config.JoiningMessageEnabled)
            {
                var message =
                    new StringBuilder(_config.JoiningMessage[Random.Next(0, _config.JoiningMessage.Count - 1)]);
                var ip = player.net.connection.ipaddress.Substring(0, player.net.connection.ipaddress.LastIndexOf(':'));
                message = message.Replace("{name}", player.displayName).Replace("{id}", player.UserIDString)
                    .Replace("{ip}", ip);

                var usePlayer = _config.JoiningMessageSelfEnabled ? null : player;
                if (message.ToString().IndexOf("{country}", StringComparison.CurrentCultureIgnoreCase) != -1 || message.ToString().IndexOf("{countrycode}", StringComparison.CurrentCultureIgnoreCase) != -1)
                    HandleCountryMessage(message, ip, usePlayer);
                else Publish(message.ToString(), player2: usePlayer);
            }

            if (_config.WelcomeMessage.Count <= 0 || !_config.WelcomeMessageEnabled) return;
            
            {
                var message = new StringBuilder(_config.WelcomeMessage[Random.Next(0, _config.WelcomeMessage.Count - 1)]);
                var ip = player.net.connection.ipaddress.Substring(0, player.net.connection.ipaddress.LastIndexOf(':'));
                message = message.Replace("{name}", player.displayName).Replace("{id}", player.UserIDString)
                    .Replace("{ip}", ip);
                
                if (message.ToString().IndexOf("{country}", StringComparison.CurrentCultureIgnoreCase) != -1 || message.ToString().IndexOf("{countrycode}", StringComparison.CurrentCultureIgnoreCase) != -1)
                    HandleCountryMessage(message, ip, player, false);
                else Publish(message.ToString(), player2: player, exclude: false);
            }
        }

        private void OnPlayerDisconnected(BasePlayer player, string reason)
        {
            if (_config.LeavingMessage.Count <= 0 || !_config.LeavingMessageEnabled) return;
            
            var message =
                new StringBuilder(_config.LeavingMessage[Random.Next(0, _config.LeavingMessage.Count - 1)]);
            var ip = player.net.connection.ipaddress.Substring(0, player.net.connection.ipaddress.LastIndexOf(':'));
            message = message.Replace("{name}", player.displayName).Replace("{id}", player.UserIDString)
                .Replace("{ip}", ip).Replace("{reason}", reason);

            var usePlayer = _config.LeavingMessageSelfEnabled ? null : player;
            if (message.ToString().IndexOf("{country}", StringComparison.CurrentCultureIgnoreCase) != -1 ||
                message.ToString().IndexOf("{countrycode}", StringComparison.CurrentCultureIgnoreCase) != -1)
                HandleCountryMessage(message, ip, usePlayer);
            else Publish(message.ToString(), string.Empty, usePlayer);
        }

        #endregion
        
        #region Helpers

        private void HandleCountryMessage(StringBuilder message, string ip, BasePlayer player, bool exclude = true)
        {
            webrequest.Enqueue(CountryRequest.Replace("{ip}", ip), string.Empty, (status, result) =>
            {
                PrintDebug("Requested a country!");
                try
                {
                    // ReSharper disable once InvertIf
                    if (status == 200)
                    {
                        var info = JsonConvert.DeserializeObject<Dictionary<string, object>>(result);
                        if ((string) info["status"] == "success")
                        {
                            message = message.Replace("{country}", (string) info["country"])
                                .Replace("{countrycode}", (string) info["countryCode"]);
                        }
                    }
                }
                catch (Exception e)
                {
                    PrintError(e.ToString());
                }
                finally
                {
                    Publish(message.ToString(), string.Empty, player, exclude);
                }
            }, this);
        }

        private void PrintDebug(string message)
        {
            if (!_config.Debug) return;
            Puts($"DEBUG: {message}");
        }

        // ReSharper disable once SuggestBaseTypeForParameter
        private void Publish(string message, string perm = "", BasePlayer player2 = null, bool exclude = true)
        {
            PrintDebug($"Called Publish (Message: {message}; Permission: {perm})");
            var notRequirePermission = string.IsNullOrEmpty(perm);
            message = FormatMessage(message);

            if (exclude)
            {
                var players = BasePlayer.activePlayerList;
                var playersCount = players.Count;
                for (var i = 0; i < playersCount; i++)
                {
                    var player = players[i];
                    if (player != player2 &&
                        (notRequirePermission || permission.UserHasPermission(player.UserIDString, perm)))
                        SendMessage(player, RunPlaceholders(player.IPlayer, message));
                }
            }
            else
                SendMessage(player2, RunPlaceholders(player2?.IPlayer, message));
        }

        private void Publish(BasePlayer player, string message) => SendMessage(player, RunPlaceholders(player.IPlayer, FormatMessage(message)));

        private void SendMessage(BasePlayer player, string message)
        {
            PrintDebug($"SendMessage: {message}");
            player.SendConsoleCommand("chat.add", 2, _config.ChatSteamId, message);
        }

        private string FormatMessage(string message) => _config.ShowPrefix ? _config.Prefix + message : message;

        private string RunPlaceholders(IPlayer player, string message)
        {
            if (PlaceholderAPI == null || !PlaceholderAPI.IsLoaded)
                return message;
            
            var builder = new StringBuilder(message);
            PlaceholderAPI.CallHook("ProcessPlaceholders", player, builder);
            return builder.ToString();
        }
        
        #endregion
        
        #region Parsers
        
        private static readonly Regex RegexStringTime = new Regex(@"(\d+)([dhms])", RegexOptions.Compiled);
        private static bool ConvertToSeconds(string time, out uint seconds)
        {
            seconds = 0;
            if (time == "0" || string.IsNullOrEmpty(time)) return true;
            var matches = RegexStringTime.Matches(time);
            if (matches.Count == 0) return false;
            for (var i = 0; i < matches.Count; i++)
            {
                var match = matches[i];
                // ReSharper disable once SwitchStatementMissingSomeCases
                switch (match.Groups[2].Value)
                {
                    case "d":
                    {
                        seconds += uint.Parse(match.Groups[1].Value) * 24 * 60 * 60;
                        break;
                    }
                    case "h":
                    {
                        seconds += uint.Parse(match.Groups[1].Value) * 60 * 60;
                        break;
                    }
                    case "m":
                    {
                        seconds += uint.Parse(match.Groups[1].Value) * 60;
                        break;
                    }
                    case "s":
                    {
                        seconds += uint.Parse(match.Groups[1].Value);
                        break;
                    }
                }
            }
            return true;
        }
        
        #endregion

        #region Automated Messages

        private object HandleChatMessage(BasePlayer player, string msg)
        {
            PrintDebug("Called HandleChatMessage");

            var response = Interface.GetMod().CallHook("CanSmartHandleMessage", player, msg);
            if (response != null)
                return null;
            
            var autoResponseGroups = _config.AutoResponses;
            var autoResponseGroupsCount = autoResponseGroups.Count;

            var tNow = Time.GetUnixTimestamp();
            uint lastSent;
            _lastSent.TryGetValue(player, out lastSent);

            PrintDebug($"tNow: {tNow}");
            PrintDebug($"lastSent: {lastSent}");
            if (_config.ParsedCooldown != 0 && lastSent + _config.ParsedCooldown > tNow ||
                _config.ParsedCooldownGlobal != 0 && _lastSentGlobal + _config.ParsedCooldownGlobal > tNow)
                return null;

            var matched = false;
            var removeMessage = false;
            // Auto Response Groups
            for (var i1 = 0; i1 < autoResponseGroupsCount; i1++)
            {
                var autoResponseGroup = autoResponseGroups[i1];
                var perm = autoResponseGroup.Permission;
                if (!string.IsNullOrEmpty(perm) &&
                    !permission.UserHasPermission(player.UserIDString, autoResponseGroup.Permission))
                    continue;

                var autoResponses = autoResponseGroup.AutoResponses;
                var autoResponsesCount = autoResponses.Count;
                // Auto Responses
                for (var i2 = 0; i2 < autoResponsesCount; i2++)
                {
                    var autoResponse = autoResponses[i2];
                    if (!autoResponse.IsValid() || !autoResponse.Enabled)
                        continue;

                    var answersCount = autoResponse.Answers.Count;
                    var autoResponseTriggers = autoResponse.Triggers;
                    var autoResponseTriggersCount = autoResponseTriggers.Count;
                    // Triggers
                    for (var i3 = 0; i3 < autoResponseTriggersCount; i3++)
                    {
                        var trigger = autoResponseTriggers[i3];

                        float wordsCount = trigger.Words.Count;
                        ushort wordsMatches = 0;
                        var regex = trigger.Regex;
                        
                        // Each Word In Triggers
                        for (var i4 = 0; i4 < wordsCount; i4++)
                        {
                            if (regex && Regex.IsMatch(msg, trigger.Words[i4]) ||
                                msg.IndexOf(trigger.Words[i4], StringComparison.CurrentCultureIgnoreCase) != -1)
                                wordsMatches++;
                        }

                        var match = wordsMatches / wordsCount;
                        PrintDebug($"Matched: {match}");

                        if (wordsMatches / wordsCount < trigger.ContainedWordsPercentage) continue;

                        matched = true;
                        var answer = autoResponse.Answers[Random.Next(0, answersCount)];
                        PrintDebug("Matched message");

                        response = Interface.GetMod().CallHook("CanSmartAnswerMessage", player, msg, answer);
                        if (response != null)
                            return null;

                        if (_config.MinTime <= 0 && _config.MaxTime <= 0)
                            NextTick(() => TrySend(player, autoResponse.SendPublic, answer)); // Next Tick to be sure it's after player's message.
                        else
                            timer.Once(Mathf.Lerp(_config.MinTime, _config.MaxTime, (float) Random.NextDouble()), () => TrySend(player, autoResponse.SendPublic, answer));

                        if (autoResponse.RemoveMessage)
                            removeMessage = true;

                        break;
                    }

                    if (matched && !_config.MultipleAutoResponses)
                        break;
                }
            }

            if (matched)
            {
                _lastSent[player] = tNow;
                _lastSentGlobal = tNow;
                PrintDebug("Matched. Changing cooldown info.");
            }

            if (removeMessage)
                return false;
            
            return null;
        }

        private void TrySend(BasePlayer player, bool isPublic, string answer)
        {
            if (isPublic)
                Publish(answer, string.Empty);
            else
                Publish(player, answer);
        }

        private void HandleBroadcast(AutoMessageGroup group)
        {
            PrintDebug("Called HandleBroadcast");
            if (group.ActiveMessage > group.AutoMessages.Count - 1)
                group.ActiveMessage = 0;
            PrintDebug($"Active Message: {group.ActiveMessage}");

            var message = group.AutoMessages[group.ActiveMessage++];
            if (message.Enabled)
                Publish(message.Message, group.Permission);
        }

        #endregion
    }
}