private async Task <ICommandResult> ExecuteAsyncInternal(ICommandContext context, CancellationToken cancellationToken = default)
        {
            if (!this._started)
            {
                throw new InvalidOperationException($"This {this.GetType().Name} is not started yet");
            }
            using (CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, this._cts.Token))
            {
                IEnumerable <KeyValuePair <ICommandInstanceDescriptor, ICommandInstance> > commandsCopy;
                // copying might not be the fastest thing to do, but it'll ensure that commands won't be changed out of the lock
                // locking only copying to prevent hangs if user is not careful with their commands, while still preventing race conditions with StartAsync
                await this._lock.WaitAsync(cts.Token).ConfigureAwait(false);

                try
                {
                    // order commands by priority
                    // try to get from concrete Descriptor if possible, as it should be precached and avoid additional reflection and thus faster
                    commandsCopy = this._commands.OrderByDescending(kvp => (kvp.Key is CommandInstanceDescriptor cid) ? cid.Priority : kvp.Key.GetPriority());
                }
                finally
                {
                    this._lock.Release();
                }

                using (IServiceScope serviceScope = this._services.CreateScope())
                {
                    // Because scope won't have fallback services, and commands might need them, combine scope with fallback services
                    // TODO: think of a nicer way to solve fallback services problem - I don't like it as it is now
                    IServiceProvider services = serviceScope.ServiceProvider;
                    if (!object.ReferenceEquals(services, this._fallbackServices))
                    {
                        services = CombinedServiceProvider.Combine(serviceScope.ServiceProvider, this._fallbackServices);
                    }

                    foreach (KeyValuePair <ICommandInstanceDescriptor, ICommandInstance> commandKvp in commandsCopy)
                    {
                        ICommandInstanceDescriptor command  = commandKvp.Key;
                        ICommandInstance           instance = commandKvp.Value;
                        using (this._log.BeginCommandScope(context, command.GetHandlerType().Name, command.Method.Name))
                        {
                            ICommandsHandlerProviderResult handlerResult = null;
                            try
                            {
                                cts.Token.ThrowIfCancellationRequested();

                                // check if the command should run at all - if not, skip
                                ICommandResult matchResult = await instance.CheckMatchAsync(context, services, cts.Token).ConfigureAwait(false);

                                if (!matchResult.IsSuccess)
                                {
                                    continue;
                                }

                                // initialize handler
                                this._log?.LogTrace("Initializing handler handler {Handler} for command {Name}", command.GetHandlerType().Name, command.Method.Name);
                                handlerResult = this._handlerProvider.GetCommandHandler(command, services);
                                if (handlerResult?.HandlerInstance == null)
                                {
                                    this._log?.LogError("Retrieving handler {Handler} for command {Name} has failed, command execution aborting", command.GetHandlerType().Name, command.Method.Name);
                                    return(CommandExecutionResult.FromException(new ArgumentNullException(nameof(ICommandsHandlerProviderResult.HandlerInstance),
                                                                                                          $"Retrieving handler {command.GetHandlerType().Name} for command {command.Method.Name} has failed, command execution aborting")));
                                }
                                this._log?.LogTrace("Executing command {Name} from handler {Handler}", command.Method.Name, command.GetHandlerType().Name);

                                // execute the command
                                ICommandResult executeResult = await instance.ExecuteAsync(context, services, matchResult, handlerResult.HandlerInstance, cts.Token).ConfigureAwait(false);

                                if (executeResult.Exception != null)
                                {
                                    this._log?.LogError(executeResult.Exception, "Exception when executing command {Name} from handler {Handler}", command.Method.Name, command.GetHandlerType().Name);
                                    return(executeResult);
                                }
                                if (executeResult is IMessagesCommandResult messagesResult && messagesResult.Messages?.Any() == true)
                                {
                                    this._log?.LogTrace("Sending command results messages as a command response");
                                    await context.ReplyTextAsync(string.Join("\n", messagesResult.Messages), cts.Token).ConfigureAwait(false);
                                }
                                return(executeResult);
                            }
                            // special error case: operation canceled
                            // operation canceled is normal, so it shouldn't be logged as error
                            catch (OperationCanceledException ex)
                            {
                                this._log?.LogWarning("Execution of command {Name} from handler {Handler} was cancelled", command.Method.Name, command.GetHandlerType().Name);
                                return(CommandExecutionResult.FromException(ex));
                            }
                            // special error case: responding when silenced
                            // bots almost always respond to a command - but if they're silenced, an exception will be thrown
                            // this is normal - so it shouldn't be logged as error. Warning max
                            catch (MessageSendingException ex) when
                                (this.LogSilencedException(ex, context, "Unhandled Exception when executing command {Name} from handler {Handler} - likely due to being silenced or spam filtered", command.Method.Name, command.GetHandlerType().Name))
                            {
                                return(CommandExecutionResult.FromException(ex));
                            }
                            // normal error case
                            catch (Exception ex) when(ex.LogAsError(_log, "Unhandled Exception when executing command {Name} from handler {Handler}", command.Method.Name, command.GetHandlerType().Name))
                            {
                                return(CommandExecutionResult.FromException(ex));
                            }
                            finally
                            {
                                // if handler is allocated, not persistent and disposable, let's dispose it
                                if (handlerResult?.Descriptor?.Attribute?.IsPersistent != true && handlerResult?.HandlerInstance is IDisposable disposableHandler)
                                {
                                    try { disposableHandler?.Dispose(); } catch { }
                                }
                            }
                        }
                    }
                }
                return(CommandExecutionResult.Failure);
            }
        }