/// <summary>Initializes a command service.</summary>
        /// <param name="client">WOLF client. Required.</param>
        /// <param name="options">Commands options that will be used as default when running a command. Required.</param>
        /// <param name="services">Services provider that will be used by all commands. Null will cause a backup provider to be used.</param>
        /// <param name="log">Logger to log messages and errors to. If null, all logging will be disabled.</param>
        /// <param name="cancellationToken">Cancellation token that can be used for cancelling all tasks.</param>
        public CommandsService(IWolfClient client, CommandsOptions options, ILogger log, IServiceProvider services = null, CancellationToken cancellationToken = default)
        {
            // init private
            this._commands           = new Dictionary <ICommandInstanceDescriptor, ICommandInstance>();
            this._lock               = new SemaphoreSlim(1, 1);
            this._cts                = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            this._disposableServices = new List <IDisposable>(2);
            this._started            = false;

            // init required
            this._client  = client ?? services?.GetService <IWolfClient>() ?? throw new ArgumentNullException(nameof(client));
            this._options = options ?? services?.GetService <CommandsOptions>() ?? throw new ArgumentNullException(nameof(options));

            // init optionals
            this._log = log ?? services?.GetService <ILogger <CommandsService> >() ?? services?.GetService <ILogger <ICommandsService> >() ?? services.GetService <ILogger>();
            this._argumentConverterProvider = services?.GetService <IArgumentConverterProvider>() ?? CreateAsDisposable <ArgumentConverterProvider>();
            this._handlerProvider           = services?.GetService <ICommandsHandlerProvider>() ?? CreateAsDisposable <CommandsHandlerProvider>();
            this._argumentsParser           = services?.GetService <IArgumentsParser>() ?? new ArgumentsParser();
            this._parameterBuilder          = services?.GetService <IParameterBuilder>() ?? new ParameterBuilder();
            this._initializers   = services?.GetService <ICommandInitializerProvider>() ?? new CommandInitializerProvider();
            this._commandsLoader = services?.GetService <ICommandsLoader>() ?? new CommandsLoader(this._initializers, this._log);

            // init service provider - use combine, to use fallback one as well
            this._fallbackServices = this.CreateFallbackServiceProvider();
            this._services         = CombinedServiceProvider.Combine(services, this._fallbackServices);

            // register event handlers
            this._client.AddMessageListener <ChatMessage>(OnMessageReceived);
        }
        private IServiceProvider BuildServices()
        {
            IServiceProvider?services = null;

            while (_serviceProviders.Count > 0)
            {
                if (services == null)
                {
                    services = _serviceProviders.Last();
                }
                else
                {
                    services = new CombinedServiceProvider(
                        _serviceProviders.Last(),
                        services);
                }
                _serviceProviders.RemoveAt(_serviceProviders.Count - 1);
            }

            if (_services.Count > 0)
            {
                var localServices = new DictionaryServiceProvider(_services);
                return(services is null
                    ? (IServiceProvider)localServices
                    : new CombinedServiceProvider(localServices, services));
            }

            if (services is null)
            {
                throw new InvalidOperationException(
                          "There was no service provider or service specified.");
            }

            return(services);
        }
        /// <summary>
        /// Adds a custom transaction scope handler to the schema.
        /// </summary>
        /// <param name="builder">
        /// The request executor builder.
        /// </param>
        /// <param name="create">
        /// A factory to create the transaction scope.
        /// </param>
        /// <returns>
        /// The request executor builder.
        /// </returns>
        /// <exception cref="ArgumentNullException"></exception>
        public static IRequestExecutorBuilder AddTransactionScopeHandler(
            this IRequestExecutorBuilder builder,
            Func <IServiceProvider, ITransactionScopeHandler> create)
        {
            if (builder is null)
            {
                throw new ArgumentNullException(nameof(builder));
            }

            return(ConfigureSchemaServices(
                       builder,
                       services =>
            {
                services.RemoveAll(typeof(ITransactionScopeHandler));
                services.AddSingleton(sp =>
                {
                    var combined = new CombinedServiceProvider(
                        sp.GetApplicationServices(),
                        sp);
                    return create(combined);
                });
            }));
        }
        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);
            }
        }