using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Text;
using System.Threading.Tasks;

namespace Discord.Interactions
{
    /// <summary>
    ///     Represents a cached method execution delegate.
    /// </summary>
    /// <param name="context">Execution context that will be injected into the module class.</param>
    /// <param name="args">Method arguments array.</param>
    /// <param name="serviceProvider">Service collection for initializing the module.</param>
    /// <param name="commandInfo">Command info class of the executed method.</param>
    /// <returns>
    ///     A task representing the execution operation.
    /// </returns>
    public delegate Task ExecuteCallback(IInteractionContext context, object[] args, IServiceProvider serviceProvider, ICommandInfo commandInfo);

    /// <summary>
    ///     The base information class for <see cref="InteractionService"/> commands.
    /// </summary>
    /// <typeparam name="TParameter">The type of <see cref="IParameterInfo"/> that is used by this command type.</typeparam>
    public abstract class CommandInfo<TParameter> : ICommandInfo where TParameter : class, IParameterInfo
    {
        private readonly ExecuteCallback _action;
        private readonly ILookup<string, PreconditionAttribute> _groupedPreconditions;

        internal IReadOnlyDictionary<string, TParameter> _parameterDictionary { get; }

        /// <inheritdoc/>
        public ModuleInfo Module { get; }

        /// <inheritdoc/>
        public InteractionService CommandService { get; }

        /// <inheritdoc/>
        public string Name { get; }

        /// <inheritdoc/>
        public string MethodName { get; }

        /// <inheritdoc/>
        public virtual bool IgnoreGroupNames { get; }

        /// <inheritdoc/>
        public abstract bool SupportsWildCards { get; }

        /// <inheritdoc/>
        public bool IsTopLevelCommand { get; }

        /// <inheritdoc/>
        public RunMode RunMode { get; }

        /// <inheritdoc/>
        public IReadOnlyCollection<Attribute> Attributes { get; }

        /// <inheritdoc/>
        public IReadOnlyCollection<PreconditionAttribute> Preconditions { get; }

        /// <inheritdoc cref="ICommandInfo.Parameters"/>
        public abstract IReadOnlyList<TParameter> Parameters { get; }

        public bool TreatNameAsRegex { get; }

        internal CommandInfo(Builders.ICommandBuilder builder, ModuleInfo module, InteractionService commandService)
        {
            CommandService = commandService;
            Module = module;

            Name = builder.Name;
            MethodName = builder.MethodName;
            IgnoreGroupNames = builder.IgnoreGroupNames;
            IsTopLevelCommand = IgnoreGroupNames || CheckTopLevel(Module);
            RunMode = builder.RunMode != RunMode.Default ? builder.RunMode : commandService._runMode;
            Attributes = builder.Attributes.ToImmutableArray();
            Preconditions = builder.Preconditions.ToImmutableArray();
            TreatNameAsRegex = builder.TreatNameAsRegex && SupportsWildCards;

            _action = builder.Callback;
            _groupedPreconditions = builder.Preconditions.ToLookup(x => x.Group, x => x, StringComparer.Ordinal);
            _parameterDictionary = Parameters?.ToDictionary(x => x.Name, x => x).ToImmutableDictionary();
        }

        /// <inheritdoc/>
        public virtual Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services)
        {
            switch (RunMode)
            {
                case RunMode.Sync:
                    return ExecuteInternalAsync(context, services);
                case RunMode.Async:
                    _ = Task.Run(async () =>
                    {
                        await ExecuteInternalAsync(context, services).ConfigureAwait(false);
                    });
                    break;
                default:
                    throw new InvalidOperationException($"RunMode {RunMode} is not supported.");
            }

            return Task.FromResult((IResult)ExecuteResult.FromSuccess());
        }

        protected abstract Task<IResult> ParseArgumentsAsync(IInteractionContext context, IServiceProvider services);

        private async Task<IResult> ExecuteInternalAsync(IInteractionContext context, IServiceProvider services)
        {
            await CommandService._cmdLogger.DebugAsync($"Executing {GetLogString(context)}").ConfigureAwait(false);

            using var scope = services?.CreateScope();

            if (CommandService._autoServiceScopes)
                services = scope?.ServiceProvider ?? EmptyServiceProvider.Instance;

            try
            {
                var preconditionResult = await CheckPreconditionsAsync(context, services).ConfigureAwait(false);
                if (!preconditionResult.IsSuccess)
                    return await InvokeEventAndReturn(context, preconditionResult).ConfigureAwait(false);

                var argsResult = await ParseArgumentsAsync(context, services).ConfigureAwait(false);

                if (!argsResult.IsSuccess)
                    return await InvokeEventAndReturn(context, argsResult).ConfigureAwait(false);

                if (argsResult is not ParseResult parseResult)
                    return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Complex command parsing failed for an unknown reason.");

                var args = parseResult.Args;

                var index = 0;
                foreach (var parameter in Parameters)
                {
                    var result = await parameter.CheckPreconditionsAsync(context, args[index++], services).ConfigureAwait(false);
                    if (!result.IsSuccess)
                        return await InvokeEventAndReturn(context, result).ConfigureAwait(false);
                }

                var task = _action(context, args, services, this);

                if (task is Task<IResult> resultTask)
                {
                    var result = await resultTask.ConfigureAwait(false);
                    await InvokeModuleEvent(context, result).ConfigureAwait(false);
                    if (result is RuntimeResult or ExecuteResult)
                        return result;
                }
                else
                {
                    await task.ConfigureAwait(false);
                    return await InvokeEventAndReturn(context, ExecuteResult.FromSuccess()).ConfigureAwait(false);
                }

                return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.Unsuccessful, "Command execution failed for an unknown reason")).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                var originalEx = ex;
                while (ex is TargetInvocationException)
                    ex = ex.InnerException;

                var interactionException = new InteractionException(this, context, ex);
                await Module.CommandService._cmdLogger.ErrorAsync(interactionException).ConfigureAwait(false);

                var result = ExecuteResult.FromError(ex);
                await InvokeModuleEvent(context, result).ConfigureAwait(false);

                if (Module.CommandService._throwOnError)
                {
                    if (ex == originalEx)
                        throw;
                    else
                        ExceptionDispatchInfo.Capture(ex).Throw();
                }

                return result;
            }
            finally
            {
                await CommandService._cmdLogger.VerboseAsync($"Executed {GetLogString(context)}").ConfigureAwait(false);
            }
        }

        protected abstract Task InvokeModuleEvent(IInteractionContext context, IResult result);
        protected abstract string GetLogString(IInteractionContext context);

        /// <inheritdoc/>
        public async Task<PreconditionResult> CheckPreconditionsAsync(IInteractionContext context, IServiceProvider services)
        {
            async Task<PreconditionResult> CheckGroups(ILookup<string, PreconditionAttribute> preconditions, string type)
            {
                foreach (IGrouping<string, PreconditionAttribute> preconditionGroup in preconditions)
                {
                    if (preconditionGroup.Key == null)
                    {
                        foreach (PreconditionAttribute precondition in preconditionGroup)
                        {
                            var result = await precondition.CheckRequirementsAsync(context, this, services).ConfigureAwait(false);
                            if (!result.IsSuccess)
                                return result;
                        }
                    }
                    else
                    {
                        var results = new List<PreconditionResult>();
                        foreach (PreconditionAttribute precondition in preconditionGroup)
                            results.Add(await precondition.CheckRequirementsAsync(context, this, services).ConfigureAwait(false));

                        if (!results.Any(p => p.IsSuccess))
                            return PreconditionGroupResult.FromError($"{type} precondition group {preconditionGroup.Key} failed.", results);
                    }
                }
                return PreconditionGroupResult.FromSuccess();
            }

            var moduleResult = await CheckGroups(Module.GroupedPreconditions, "Module").ConfigureAwait(false);
            if (!moduleResult.IsSuccess)
                return moduleResult;

            var commandResult = await CheckGroups(_groupedPreconditions, "Command").ConfigureAwait(false);
            return !commandResult.IsSuccess ? commandResult : PreconditionResult.FromSuccess();
        }

        protected async Task<T> InvokeEventAndReturn<T>(IInteractionContext context, T result) where T : IResult
        {
            await InvokeModuleEvent(context, result).ConfigureAwait(false);
            return result;
        }

        private static bool CheckTopLevel(ModuleInfo parent)
        {
            var currentParent = parent;

            while (currentParent != null)
            {
                if (currentParent.IsSlashGroup)
                    return false;

                currentParent = currentParent.Parent;
            }
            return true;
        }

        // ICommandInfo

        /// <inheritdoc/>
        IReadOnlyCollection<IParameterInfo> ICommandInfo.Parameters => Parameters;

        /// <inheritdoc/>
        public override string ToString()
        {
            List<string> builder = new();

            var currentParent = Module;

            while (currentParent != null)
            {
                if (currentParent.IsSlashGroup)
                    builder.Add(currentParent.SlashGroupName);

                currentParent = currentParent.Parent;
            }
            builder.Reverse();
            builder.Add(Name);

            return string.Join(" ", builder);
        }
    }
}
