// WingsEmu
// 
// Developed by NosWings Team

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using PhoenixLib.Logging;
using Qmmands;
using WingsEmu.Commands.Checks;
using WingsEmu.Commands.Entities;
using WingsEmu.Commands.Interfaces;
using WingsEmu.DTOs.Account;
using WingsEmu.Game;
using WingsEmu.Game.Commands;
using WingsEmu.Game.Extensions;
using WingsEmu.Game.Networking;
using WingsEmu.Packets.Enums.Chat;

namespace WingsEmu.Commands
{
    /* todo: find a better way to deal with TAP in world and here.
     *       handle errors correctly and return them to the user ingame.
     */
    public class CommandHandler : ICommandContainer, IGlobalCommandExecutor
    {
        private readonly CommandService _commands;

        /// <summary>
        ///     This class should be instanciated with our Container.
        /// </summary>
        public CommandHandler(IServiceProvider provider)
        {
            _commands = new CommandService(new CommandServiceConfiguration
            {
                StringComparison = StringComparison.OrdinalIgnoreCase
            });

            _commands.CommandExecuted += _commands_CommandExecuted;
            _commands.CommandExecutionFailed += _commands_CommandErrored;

            Services = provider;
        }

        public IServiceProvider Services { get; }

        public void AddModule<T>() where T : SaltyModuleBase
        {
            Log.Info($"[ADD_MODULE] {typeof(T).Name}");
            _commands.AddModule<T>();

            IReadOnlyList<Command> readOnlyList = _commands.GetAllModules().FirstOrDefault(s => s.Type == typeof(T))?.Commands;
            if (readOnlyList != null)
            {
                foreach (Command command in readOnlyList)
                {
                    Log.Info($"[ADD_COMMAND] {command}");
                }
            }
        }

        public void RemoveModule<T>() where T : SaltyModuleBase
        {
            Module module = _commands.GetAllModules().FirstOrDefault(s => s.Type == typeof(T));

            if (module is null)
            {
                throw new ArgumentException("The given module is not registered in the command container.");
            }

            _commands.RemoveModule(module);
        }

        public Module[] GetModulesByName(string name, bool caseSensitive = true)
        {
            return _commands.GetAllModules().Where(x => caseSensitive ? x.Name == name : x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)).ToArray();
        }

        public Command[] GetCommandsByName(string name, bool caseSensitive = true)
        {
            return _commands.GetAllCommands().Where(x => caseSensitive ? x.Name == name : x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)).ToArray();
        }

        public void AddTypeParser<T>(TypeParser<T> typeParser)
        {
            _commands.AddTypeParser(typeParser);
            Log.Info($"[ADD_TYPE_PARSER] {typeParser.GetType().Name}");
        }

        /// <inheritdoc />
        /// <summary>
        ///     This is where every message from the InGame tchat starting with our prefix will arrive.
        ///     In our case, the parameter message represents the raw message sent by the user.
        ///     The parameter of type object would represent the instance of the entity that invoked the command.
        ///     That method could be called on each messages sent in the in-game tchat. We will just check that it starts with our
        ///     prefix ($).
        ///     Then we will create a Context that will propagate onto the command.
        ///     The CommandService will try to parse the message and find a command with the parsed arguments and will perform some
        ///     checks, if necessary.
        /// </summary>
        /// <param name="message">It represents the already parsed command with its parameters.</param>
        /// <param name="entity">It represents the instance of the entity that performed the action of sending a message.</param>
        /// <param name="prefix"></param>
        public async Task HandleMessageAsync(string message, object entity, string prefix)
        {
            if (entity is not IClientSession player)
            {
                return;
            }

            if (!player.HasSelectedCharacter)
            {
                return;
            }

            if (!player.HasCurrentMapInstance)
            {
                return;
            }

            var ctx = new WingsEmuIngameCommandContext(message, player, prefix, _commands, Services);

            IResult result = await _commands.ExecuteAsync(ctx.Input, ctx);
            if (result.IsSuccessful)
            {
                ctx.Command = (result as CommandResult)?.Command;
                var authorityAttribute = (RequireAuthorityAttribute)ctx.Command?.Module?.Checks.FirstOrDefault(check => check is RequireAuthorityAttribute);
                if (authorityAttribute == null || authorityAttribute.Authority < AuthorityType.GameMaster)
                {
                    ctx.Player.PlayerEntity.Session.EmitEvent(new PlayerCommandEvent
                    {
                        Command = ctx.Message
                    });
                    return;
                }

                ctx.Player.PlayerEntity.Session.EmitEvent(new GmCommandEvent
                {
                    Command = ctx.Message,
                    PlayerAuthority = ctx.Player.Account.Authority,
                    CommandAuthority = authorityAttribute.Authority
                });
                return;
            }

            await HandleErrorAsync(result, ctx);
        }

        public void HandleCommand(string command, IClientSession sender, string prefix)
        {
            HandleCommandAsync(command, sender, prefix).ConfigureAwait(false).GetAwaiter().GetResult();
        }

        public async Task HandleCommandAsync(string command, IClientSession sender, string prefix)
        {
            await HandleMessageAsync(command, sender, prefix);
        }

        /// <summary>
        ///     This event is being invoked when the excecuted of a command threw an exception.
        ///     Error results are handled by the result of CommandService#ExecuteAsync.
        /// </summary>
        /// <param name="result">Result with its associated exception.</param>
        /// <param name="context">It represents the context. Must be casted to our custom context (WingsEmuIngameCommandContext)</param>
        /// <param name="services"></param>
        /// <returns></returns>
        private Task _commands_CommandErrored(CommandExecutionFailedEventArgs e)
        {
            switch (e.Result.Exception)
            {
                default:
                    Log.Debug($"{e.Result.Exception.GetType()} occured.\nError message: {e.Result.Exception.Message}.\nStack trace: {e.Result.Exception.StackTrace}");
                    break;
            }

            return Task.CompletedTask;
        }

        /// <summary>
        ///     This event is being invoked when the execution of a command is over. When the command returned a result.
        ///     It could be a custom result that we can cast from our instance of CommandResult.
        /// </summary>
        /// <param name="command">It represents the command that has been executed.</param>
        /// <param name="result">
        ///     It represents the returned result. It can an 'empty' result when the command returned a Task, or a
        ///     custom result.
        /// </param>
        /// <param name="context">It represents the context. Must be casted to our custom context (WingsEmuIngameCommandContext)</param>
        /// <returns></returns>
        private async Task _commands_CommandExecuted(CommandExecutedEventArgs e)
        {
            if (e.Context is not WingsEmuIngameCommandContext ctx)
            {
                Log.Debug($"Command context: {e.Context.GetType()}. This is bad. Please report this.");
                return;
            }

            Log.Debug($"The command {e.Context.Command.Name} (from player {ctx.Player.PlayerEntity.Name} [{ctx.Player.PlayerEntity.Id}]) has successfully been executed.");

            if (e.Result is SaltyCommandResult res && !string.IsNullOrWhiteSpace(res.Message))
            {
                ctx.Player.SendChatMessage("[COMMAND] " + res.Message, e.Result.IsSuccessful ? ChatMessageColorType.Green : ChatMessageColorType.Red);
            }
        }

        /// <summary>
        ///     This is being called when the CommandService returned an unsuccessfull result.
        /// </summary>
        /// <param name="result">This represents the generic result returned by the command service. We'll check what was wrong.</param>
        /// <param name="ctx">This represents our context for this result.</param>
        private async Task HandleErrorAsync(IResult result, WingsEmuIngameCommandContext ctx)
        {
            Log.Debug($"An error occured: {result}");

            var errorBuilder = new StringBuilder();
            bool help = false;

            switch (result)
            {
                case ChecksFailedResult ex:
                    ctx.Command = ex.Command;
                    Log.Debug("Some checks have failed: " + string.Join("\n", ex.FailedChecks.Select(x => x.Result)));
                    break;
                case TypeParseFailedResult ex:
                    ctx.Command = ex.Parameter.Command;
                    errorBuilder.Append(ex.FailureReason);
                    help = true;
                    break;
                case CommandNotFoundResult ex:
                    errorBuilder.Append($"The command was not found: {ctx.Input}");
                    break;
                case ArgumentParseFailedResult ex:
                    ctx.Command = ex.Command;
                    errorBuilder.Append(ex.Command.Parameters == null ? "Too many arguments." : $"The argument for the parameter {ex.Command.Name} was invalid.");
                    help = true;
                    break;
                case SaltyCommandResult ex:
                    ctx.Command = ex.Command;
                    errorBuilder.Append($"{ctx.Command.Name}: {ex.Message}");
                    break;
                case OverloadsFailedResult ex:
                    ctx.Command = ex.FailedOverloads.Select(x => x.Key).FirstOrDefault();
                    Log.Debug($"Every overload failed: {string.Join("\n", ex.FailedOverloads.Select(x => x.Value.FailureReason))}");
                    errorBuilder.Append("Your command syntax was wrong.");
                    help = true;
                    break;
            }

            if (errorBuilder.Length == 0)
            {
                return;
            }

            ctx.Player.SendChatMessage(errorBuilder.ToString(), ChatMessageColorType.Red);
            if (help)
            {
                await _commands.ExecuteAsync($"help {ctx.Command.FullAliases[0]}", ctx);
            }
        }
    }
}