using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using ReallifeGamemode.Server.Core.API;
using ReallifeGamemode.Server.Types;
using ReallifeGamemode.Server.Common;
using ReallifeGamemode.Server.Log;
using Microsoft.Extensions.Logging;
using ReallifeGamemode.Database.Entities.Logs;
using ReallifeGamemode.Server.Core.Extensions;

namespace ReallifeGamemode.Server.Core.Commands
{
  class CommandHandler
  {
    private readonly Dictionary<string, Command> registeredCommands = new Dictionary<string, Command>();
    private readonly ILogger logger = LogManager.GetLogger<CommandHandler>();

    private readonly IAPI api;
    private readonly string[] legacyCommands;
    private Random random = new Random();

    public CommandHandler(IAPI api, string[] registeredCommands)
    {
      this.api = api;
      legacyCommands = registeredCommands;
      Main.EventHandler.RegisterClientEvent("Command", OnPlayerCommand);
    }

    private void OnPlayerCommand(IPlayer player, object[] args)
    {
      var command = (args.First() as string)?.ToString()?.ToLower();

      logger.LogInformation("Player '{Name}' executed command '{command}'", player.Name, command);

      using var dbContext = Main.GetDbContext();

      var commandLogEntry = new CommandLogEntry()
      {
        Command = "/" + string.Join(' ', args),
        Time = DateTime.Now,
        User = player.GetUser(dbContext)
      };

      dbContext.CommandLogs.Add(commandLogEntry);
      dbContext.SaveChanges();

      if (legacyCommands.Contains(command))
      {
        return;
      }

      if (!registeredCommands.ContainsKey(command))
      {
        player.SendMessage($"Der Befehl ~b~/{command}~s~ existiert nicht", ChatPrefix.Error);
        return;
      }

      var executor = registeredCommands[command];

      if (!executor.CanExecute(player))
      {
        player.SendMessage($"Du darfst diesen Befehl nicht ausführen", ChatPrefix.Error);
        return;
      }

      var methodInfo = executor.GetType().GetMethod("Handle");

      var arguments = new List<object>
      {
          player
      };

      try
      {
        arguments.AddRange(ComputeParameters(methodInfo, args.Skip(1).ToArray()));

        var result = methodInfo.Invoke(executor, arguments.ToArray());
      }
      catch (ArgumentException ex)
      {
        player.SendMessage($"Der Parameter ~y~'{ex.Message}'~s~ ist ungültig.", ChatPrefix.Error);
      }
      catch (SendHelpTextException)
      {
        player.SendMessage("/" + command + " " + executor.HelpText, ChatPrefix.Usage);
      }
      catch (TargetInvocationException ex)
      {
        var errorCode = GetNewErrorCode();
        ex.Data.Add("ErrorCode", errorCode);
        logger.LogError(ex, "Error (Code: {ErrorCode}) while executing command '{Command}' from user '{Name}'", errorCode, command, player.Name);
        player.SendMessage($"Bei der Ausführung des Befehls gab es einen unbekannten Fehler. ~m~({errorCode})", ChatPrefix.Error);
      }
    }

    public void RegisterCommand(string command, Command commandExecutor)
    {
      command = command.ToLower();
      logger.LogDebug("Adding command '{command}'", command);

      if (command.IsNullOrEmpty())
      {
        logger.LogError("Command name is null");
        return;
      }

      if (commandExecutor == null)
      {
        logger.LogError("Command executor for command '{command}' is null", command);
        return;
      }

      var methodInfo = commandExecutor.GetType().GetMethod("Handle");

      if (methodInfo == null)
      {
        logger.LogError("The executor for command '{command}' does not have a 'Handle' Method", command);
        return;
      }

      registeredCommands[command] = commandExecutor;
    }

    private object[] ComputeParameters(MethodInfo methodInfo, object[] arguments)
    {
      var computedParameters = new List<object>();

      var parameterInfos = methodInfo.GetParameters();
      var parameterCount = parameterInfos.Length - 1;

      var argumentCount = arguments.Length;

      for (var i = 1; i <= parameterCount; i++)
      {
        var currentParameter = parameterInfos[i];

        var parameterType = currentParameter.ParameterType;

        object currentArgument;

        if (i < argumentCount + 1)
        {
          currentArgument = arguments[i - 1];
        }
        else if (argumentCount <= (i - 1) && currentParameter.HasDefaultValue)
        {
          currentArgument = currentParameter.DefaultValue;
          computedParameters.Add(currentArgument);
          continue;
        }
        else
        {
          throw new CommandArgumentCountMismatchException();
        }

        if (parameterType == typeof(IPlayer))
        {
          var player = api.GetPlayerFromNameOrId(currentArgument.ToString());

          if (player == null)
          {
            throw new ArgumentException($"Der Spieler wurde nicht gefunden.");
          }

          computedParameters.Add(player);

          continue;
        }

        if (parameterType == typeof(string))
        {
          if (i == parameterCount)
          {
            var str = string.Join(' ', arguments.Skip(i - 1));
            computedParameters.Add(str);
            break;
          }
          else
          {
            computedParameters.Add(currentArgument);
          }
        }

        if (parameterType.IsEnum)
        {
          var stringArg = currentArgument.ToString();
          if (stringArg.TryParseEnum(parameterType, out var result))
          {
            computedParameters.Add(result);
          }
          else
          {
            throw new ArgumentException(currentArgument.ToString());
          }
        }
        else if (parameterType.IsPrimitive)
        {
          try
          {
            computedParameters.Add(Convert.ChangeType(currentArgument, parameterType));
          }
          catch
          {
            throw new ArgumentException(currentArgument.ToString());
          }
        }
      }

      return computedParameters.ToArray();
    }

    private string GetNewErrorCode()
    {
      const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#?$";
      return new string(Enumerable.Repeat(chars, 6)
        .Select(s => s[random.Next(s.Length)]).ToArray());
    }
  }
}
