using Discord.Commands.Builders;
using DryIoc;
using Microsoft.Extensions.DependencyInjection;
using Ellie.Common.Marmalade;
using Ellie.Marmalade.Adapters;
using EllieBot.Common.ModuleBehaviors;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace EllieBot.Marmalade;

// ReSharper disable RedundantAssignment
public sealed class MarmaladeLoaderService : IMarmaladeLoaderService, IReadyExecutor, IEService
{
    private readonly CommandService _cmdService;
    private readonly IBehaviorHandler _behHandler;
    private readonly IPubSub _pubSub;
    private readonly IMarmaladeConfigService _marmaladeConfig;
    private readonly IContainer _cont;

    private readonly ConcurrentDictionary<string, ResolvedMarmalade> _resolved = new();
    private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);

    private readonly TypedKey<string> _loadKey = new("marmalade:load");
    private readonly TypedKey<string> _unloadKey = new("marmalade:unload");

    private readonly TypedKey<bool> _stringsReload = new("marmalade:reload_strings");

    private const string BASE_DIR = "data/marmalades";

    public MarmaladeLoaderService(
        CommandService cmdService,
        IContainer cont,
        IBehaviorHandler behHandler,
        IPubSub pubSub,
        IMarmaladeConfigService marmaladeConfig)
    {
        _cmdService = cmdService;
        _behHandler = behHandler;
        _pubSub = pubSub;
        _marmaladeConfig = marmaladeConfig;
        _cont = cont;

        // has to be done this way to support this feature on sharded bots
        _pubSub.Sub(_loadKey, async name => await InternalLoadAsync(name));
        _pubSub.Sub(_unloadKey, async name => await InternalUnloadAsync(name));

        _pubSub.Sub(_stringsReload, async _ => await ReloadStringsInternal());
    }

    public IReadOnlyCollection<string> GetAllMarmalades()
    {
        if (!Directory.Exists(BASE_DIR))
            return Array.Empty<string>();

        return Directory.GetDirectories(BASE_DIR)
                        .Select(x => Path.GetRelativePath(BASE_DIR, x))
                        .ToArray();
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    public IReadOnlyCollection<MarmaladeStats> GetLoadedMarmalades(CultureInfo? culture)
    {
        var toReturn = new List<MarmaladeStats>(_resolved.Count);
        foreach (var (name, resolvedData) in _resolved)
        {
            var canaries = new List<CanaryStats>(resolvedData.CanaryInfos.Count);

            foreach (var canaryInfos in resolvedData.CanaryInfos.Concat(resolvedData.CanaryInfos.SelectMany(x => x.Subcanaries)))
            {
                var commands = new List<CanaryCommandStats>();

                foreach (var command in canaryInfos.Commands)
                {
                    commands.Add(new CanaryCommandStats(command.Aliases.First()));
                }

                canaries.Add(new CanaryStats(canaryInfos.Name, canaryInfos.Instance.Prefix, commands));
            }

            toReturn.Add(new MarmaladeStats(name, resolvedData.Strings.GetDescription(culture), canaries));
        }

        return toReturn;
    }

    public async Task OnReadyAsync()
    {
        foreach (var name in _marmaladeConfig.GetLoadedMarmalades())
        {
            var result = await InternalLoadAsync(name);
            if (result != MarmaladeLoadResult.Success)
                Log.Warning("Unable to load '{MarmaladeName}' marmalade", name);
            else
                Log.Warning("Loaded marmalade '{MarmaladeName}'", name);
        }
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    public async Task<MarmaladeLoadResult> LoadMarmaladeAsync(string marmaladeName)
    {
        // try loading on this shard first to see if it works
        var res = await InternalLoadAsync(marmaladeName);
        if (res == MarmaladeLoadResult.Success)
        {
            // if it does publish it so that other shards can load the marmalade too
            // this method will be ran twice on this shard but it doesn't matter as 
            // the second attempt will be ignored
            await _pubSub.Pub(_loadKey, marmaladeName);
        }

        return res;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    public async Task<MarmaladeUnloadResult> UnloadMarmaladeAsync(string marmaladeName)
    {
        var res = await InternalUnloadAsync(marmaladeName);
        if (res == MarmaladeUnloadResult.Success)
        {
            await _pubSub.Pub(_unloadKey, marmaladeName);
        }

        return res;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    public string[] GetCommandExampleArgs(string marmaladeName, string commandName, CultureInfo culture)
    {
        if (!_resolved.TryGetValue(marmaladeName, out var data))
            return Array.Empty<string>();

        return data.Strings.GetCommandStrings(commandName, culture).Args
               ?? data.CanaryInfos
                      .SelectMany(x => x.Commands)
                      .FirstOrDefault(x => x.Aliases.Any(alias
                          => alias.Equals(commandName, StringComparison.InvariantCultureIgnoreCase)))
                      ?.OptionalStrings
                      .Args
               ?? [string.Empty];
    }

    public Task ReloadStrings()
        => _pubSub.Pub(_stringsReload, true);

    [MethodImpl(MethodImplOptions.NoInlining)]
    private void ReloadStringsSync()
    {
        foreach (var resolved in _resolved.Values)
        {
            resolved.Strings.Reload();
        }
    }

    private async Task ReloadStringsInternal()
    {
        await _lock.WaitAsync();
        try
        {
            ReloadStringsSync();
        }
        finally
        {
            _lock.Release();
        }
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    public string GetCommandDescription(string marmaladeName, string commandName, CultureInfo culture)
    {
        if (!_resolved.TryGetValue(marmaladeName, out var data))
            return string.Empty;

        return data.Strings.GetCommandStrings(commandName, culture).Desc
               ?? data.CanaryInfos
                      .SelectMany(x => x.Commands)
                      .FirstOrDefault(x => x.Aliases.Any(alias
                          => alias.Equals(commandName, StringComparison.InvariantCultureIgnoreCase)))
                      ?.OptionalStrings
                      .Desc
               ?? string.Empty;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private async ValueTask<MarmaladeLoadResult> InternalLoadAsync(string name)
    {
        if (_resolved.ContainsKey(name))
            return MarmaladeLoadResult.AlreadyLoaded;

        var safeName = Uri.EscapeDataString(name);

        await _lock.WaitAsync();
        try
        {
            if (LoadAssemblyInternal(safeName,
                    out var ctx,
                    out var canaryData,
                    out var iocModule,
                    out var strings,
                    out var typeReaders))
            {
                var moduleInfos = new List<ModuleInfo>();

                LoadTypeReadersInternal(typeReaders);

                foreach (var point in canaryData)
                {
                    try
                    {
                        // initialize canary and subcanaries
                        await point.Instance.InitializeAsync();
                        foreach (var sub in point.Subcanaries)
                        {
                            await sub.Instance.InitializeAsync();
                        }

                        var module = await LoadModuleInternalAsync(name, point, strings, iocModule);
                        moduleInfos.Add(module);
                    }
                    catch (Exception ex)
                    {
                        Log.Warning(ex,
                            "Error loading canary {CanaryName}",
                            point.Name);
                    }
                }

                var execs = GetExecsInternal(canaryData, strings);
                await _behHandler.AddRangeAsync(execs);

                _resolved[name] = new(LoadContext: ctx,
                    ModuleInfos: moduleInfos.ToImmutableArray(),
                    CanaryInfos: canaryData.ToImmutableArray(),
                    strings,
                    typeReaders,
                    execs)
                {
                    IocModule = iocModule
                };


                _marmaladeConfig.AddLoadedMarmalade(safeName);
                return MarmaladeLoadResult.Success;
            }

            return MarmaladeLoadResult.Empty;
        }
        catch (Exception ex) when (ex is FileNotFoundException or BadImageFormatException)
        {
            return MarmaladeLoadResult.NotFound;
        }
        catch (Exception ex)
        {
            Log.Error(ex, "An error occurred loading a marmalade");
            return MarmaladeLoadResult.UnknownError;
        }
        finally
        {
            _lock.Release();
        }
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private IReadOnlyCollection<ICustomBehavior> GetExecsInternal(
        IReadOnlyCollection<CanaryInfo> canaryData,
        IMarmaladeStrings strings)
    {
        var behs = new List<ICustomBehavior>();
        foreach (var canary in canaryData)
        {
            behs.Add(new BehaviorAdapter(new(canary.Instance), strings, _cont));

            foreach (var sub in canary.Subcanaries)
            {
                behs.Add(new BehaviorAdapter(new(sub.Instance), strings, _cont));
            }
        }


        return behs;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private void LoadTypeReadersInternal(Dictionary<Type, TypeReader> typeReaders)
    {
        var notAddedTypeReaders = new List<Type>();
        foreach (var (type, typeReader) in typeReaders)
        {
            // if type reader for this type already exists, it will not be replaced
            if (_cmdService.TypeReaders.Contains(type))
            {
                notAddedTypeReaders.Add(type);
                continue;
            }

            _cmdService.AddTypeReader(type, typeReader);
        }

        // remove the ones that were not added
        // to prevent them from being unloaded later
        // as they didn't come from this marmalade
        foreach (var toRemove in notAddedTypeReaders)
        {
            typeReaders.Remove(toRemove);
        }
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private bool LoadAssemblyInternal(
        string safeName,
        [NotNullWhen(true)] out WeakReference<MarmaladeAssemblyLoadContext>? ctxWr,
        [NotNullWhen(true)] out IReadOnlyCollection<CanaryInfo>? canaryData,
        [NotNullWhen(true)] out IIocModule? iocModule,
        out IMarmaladeStrings strings,
        out Dictionary<Type, TypeReader> typeReaders)
    {
        ctxWr = null;
        canaryData = null;

        var path = Path.GetFullPath($"{BASE_DIR}/{safeName}/{safeName}.dll");
        var dir = Path.GetFullPath($"{BASE_DIR}/{safeName}");

        if (!Directory.Exists(dir))
            throw new DirectoryNotFoundException($"Marmalade folder not found: {dir}");
        
        if (!File.Exists(path))
            throw new FileNotFoundException($"Marmalade dll not found: {path}");

        strings = MarmaladeStrings.CreateDefault(dir);
        var ctx = new MarmaladeAssemblyLoadContext(path);
        var a = ctx.LoadFromAssemblyPath(Path.GetFullPath(path));
        // ctx.LoadDependencies(a);

        // load services
        iocModule = new MarmaladeNinjectIocModule(_cont, a, safeName);
        iocModule.Load();
        
        var sis = LoadCanariesFromAssembly(safeName, a);
        typeReaders = LoadTypeReadersFromAssembly(a, strings);
        
        if (sis.Count == 0)
        {
            iocModule.Unload();
            return false;
        }

        ctxWr = new(ctx);
        canaryData = sis;

        return true;
    }

    private static readonly Type _paramParserType = typeof(ParamParser<>);

    [MethodImpl(MethodImplOptions.NoInlining)]
    private Dictionary<Type, TypeReader> LoadTypeReadersFromAssembly(
        Assembly assembly,
        IMarmaladeStrings strings)
    {
        var paramParsers = assembly.GetExportedTypes()
                                   .Where(x => x.IsClass
                                               && !x.IsAbstract
                                               && x.BaseType is not null
                                               && x.BaseType.IsGenericType
                                               && x.BaseType.GetGenericTypeDefinition() == _paramParserType);

        var typeReaders = new Dictionary<Type, TypeReader>();
        foreach (var parserType in paramParsers)
        {
            var parserObj = ActivatorUtilities.CreateInstance(_cont, parserType);

            var targetType = parserType.BaseType!.GetGenericArguments()[0];
            var typeReaderInstance = (TypeReader)Activator.CreateInstance(
                typeof(ParamParserAdapter<>).MakeGenericType(targetType),
                args: [parserObj, strings, _cont])!;

            typeReaders.Add(targetType, typeReaderInstance);
        }

        return typeReaders;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private async Task<ModuleInfo> LoadModuleInternalAsync(
        string marmaladeName,
        CanaryInfo canaryInfo,
        IMarmaladeStrings strings,
        IIocModule services)
    {
        var module = await _cmdService.CreateModuleAsync(canaryInfo.Instance.Prefix,
            CreateModuleFactory(marmaladeName, canaryInfo, strings, services));

        return module;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private Action<ModuleBuilder> CreateModuleFactory(
        string marmaladeName,
        CanaryInfo canaryInfo,
        IMarmaladeStrings strings,
        IIocModule iocModule)
        => mb =>
        {
            var m = mb.WithName(canaryInfo.Name);

            foreach (var f in canaryInfo.Filters)
            {
                m.AddPrecondition(new FilterAdapter(f, strings));
            }

            foreach (var cmd in canaryInfo.Commands)
            {
                m.AddCommand(cmd.Aliases.First(),
                    CreateCallback(cmd.ContextType,
                        new(canaryInfo),
                        new(cmd),
                        strings),
                    CreateCommandFactory(marmaladeName, cmd, strings));
            }

            foreach (var subInfo in canaryInfo.Subcanaries)
                m.AddModule(subInfo.Instance.Prefix, CreateModuleFactory(marmaladeName, subInfo, strings, iocModule));
        };

    private static readonly RequireContextAttribute _reqGuild = new RequireContextAttribute(ContextType.Guild);
    private static readonly RequireContextAttribute _reqDm = new RequireContextAttribute(ContextType.DM);

    private Action<CommandBuilder> CreateCommandFactory(string marmaladeName, CanaryCommandData cmd, IMarmaladeStrings strings)
        => (cb) =>
        {
            cb.AddAliases(cmd.Aliases.Skip(1).ToArray());

            if (cmd.ContextType == CommandContextType.Guild)
                cb.AddPrecondition(_reqGuild);
            else if (cmd.ContextType == CommandContextType.Dm)
                cb.AddPrecondition(_reqDm);

            foreach (var f in cmd.Filters)
                cb.AddPrecondition(new FilterAdapter(f, strings));

            foreach (var ubp in cmd.UserAndBotPerms)
            {
                if (ubp is user_permAttribute up)
                {
                    if (up.GuildPerm is { } gp)
                        cb.AddPrecondition(new UserPermAttribute(gp));
                    else if (up.ChannelPerm is { } cp)
                        cb.AddPrecondition(new UserPermAttribute(cp));
                }
                else if (ubp is bot_permAttribute bp)
                {
                    if (bp.GuildPerm is { } gp)
                        cb.AddPrecondition(new BotPermAttribute(gp));
                    else if (bp.ChannelPerm is { } cp)
                        cb.AddPrecondition(new BotPermAttribute(cp));
                }
                else if (ubp is bot_owner_onlyAttribute)
                {
                    cb.AddPrecondition(new OwnerOnlyAttribute());
                }
            }

            cb.WithPriority(cmd.Priority);

            // using summary to save method name
            // method name is used to retrieve desc/usages
            cb.WithRemarks($"marmalade///{marmaladeName}");
            cb.WithSummary(cmd.MethodInfo.Name.ToLowerInvariant());

            foreach (var param in cmd.Parameters)
            {
                cb.AddParameter(param.Name, param.Type, CreateParamFactory(param));
            }
        };

    private Action<ParameterBuilder> CreateParamFactory(ParamData paramData)
        => (pb) =>
        {
            pb.WithIsMultiple(paramData.IsParams)
              .WithIsOptional(paramData.IsOptional)
              .WithIsRemainder(paramData.IsLeftover);

            if (paramData.IsOptional)
                pb.WithDefault(paramData.DefaultValue);
        };

    [MethodImpl(MethodImplOptions.NoInlining)]
    private Func<ICommandContext, object[], IServiceProvider, CommandInfo, Task> CreateCallback(
        CommandContextType contextType,
        WeakReference<CanaryInfo> canaryDataWr,
        WeakReference<CanaryCommandData> canaryCommandDataWr,
        IMarmaladeStrings strings)
        => async (
            context,
            parameters,
            svcs,
            _) =>
        {
            if (!canaryCommandDataWr.TryGetTarget(out var cmdData)
                || !canaryDataWr.TryGetTarget(out var canaryData))
            {
                Log.Warning("Attempted to run an unloaded canary's command");
                return;
            }

            var paramObjs = ParamObjs(contextType, cmdData, parameters, context, svcs, _cont, strings);

            try
            {
                var methodInfo = cmdData.MethodInfo;
                if (methodInfo.ReturnType == typeof(Task)
                    || (methodInfo.ReturnType.IsGenericType
                        && methodInfo.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)))
                {
                    await (Task)methodInfo.Invoke(canaryData.Instance, paramObjs)!;
                }
                else if (methodInfo.ReturnType == typeof(ValueTask))
                {
                    await ((ValueTask)methodInfo.Invoke(canaryData.Instance, paramObjs)!).AsTask();
                }
                else // if (methodInfo.ReturnType == typeof(void))
                {
                    methodInfo.Invoke(canaryData.Instance, paramObjs);
                }
            }
            finally
            {
                paramObjs = null;
                cmdData = null;

                canaryData = null;
            }
        };

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static object[] ParamObjs(
        CommandContextType contextType,
        CanaryCommandData cmdData,
        object[] parameters,
        ICommandContext context,
        IServiceProvider svcs,
        IServiceProvider svcProvider,
        IMarmaladeStrings strings)
    {
        var extraParams = contextType == CommandContextType.Unspecified ? 0 : 1;
        extraParams += cmdData.InjectedParams.Count;

        var paramObjs = new object[parameters.Length + extraParams];

        var startAt = 0;
        if (contextType != CommandContextType.Unspecified)
        {
            paramObjs[0] = ContextAdapterFactory.CreateNew(context, strings, svcs);

            startAt = 1;
        }

        for (var i = 0; i < cmdData.InjectedParams.Count; i++)
        {
            var svc = svcProvider.GetService(cmdData.InjectedParams[i]);
            if (svc is null)
            {
                throw new ArgumentException($"Cannot inject a service of type {cmdData.InjectedParams[i]}");
            }

            paramObjs[i + startAt] = svc;

            svc = null;
        }

        startAt += cmdData.InjectedParams.Count;

        for (var i = 0; i < parameters.Length; i++)
            paramObjs[startAt + i] = parameters[i];

        return paramObjs;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private async Task<MarmaladeUnloadResult> InternalUnloadAsync(string name)
    {
        if (!_resolved.Remove(name, out var lsi))
            return MarmaladeUnloadResult.NotLoaded;

        await _lock.WaitAsync();
        try
        {
            UnloadTypeReaders(lsi.TypeReaders);

            foreach (var mi in lsi.ModuleInfos)
            {
                await _cmdService.RemoveModuleAsync(mi);
            }

            await _behHandler.RemoveRangeAsync(lsi.Execs);

            await DisposeCanaryInstances(lsi);

            var lc = lsi.LoadContext;
            var km = lsi.IocModule;
            
            lsi.IocModule.Unload();
            lsi.IocModule = null!;
           
            if (km is IDisposable d)
                d.Dispose();
            
            lsi = null;

            _marmaladeConfig.RemoveLoadedMarmalade(name);
            return UnloadInternal(lc)
                ? MarmaladeUnloadResult.Success
                : MarmaladeUnloadResult.PossiblyUnable;
        }
        finally
        {
            _lock.Release();
        }
    }

    private void UnloadTypeReaders(Dictionary<Type, TypeReader> valueTypeReaders)
    {
        foreach (var tr in valueTypeReaders)
        {
            _cmdService.TryRemoveTypeReader(tr.Key, false, out _);
        }
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private async Task DisposeCanaryInstances(ResolvedMarmalade marmalade)
    {
        foreach (var si in marmalade.CanaryInfos)
        {
            try
            {
                await si.Instance.DisposeAsync();
                foreach (var sub in si.Subcanaries)
                {
                    await sub.Instance.DisposeAsync();
                }
            }
            catch (Exception ex)
            {
                Log.Warning(ex,
                    "Failed cleanup of Canary {CanaryName}. This marmalade might not unload correctly",
                    si.Instance.Name);
            }
        }

        // marmalades = null;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private bool UnloadInternal(WeakReference<MarmaladeAssemblyLoadContext> lsi)
    {
        UnloadContext(lsi);
        GcCleanup();

        return !lsi.TryGetTarget(out _);
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private void UnloadContext(WeakReference<MarmaladeAssemblyLoadContext> lsiLoadContext)
    {
        if (lsiLoadContext.TryGetTarget(out var ctx))
        {
            ctx.Unload();
        }
    }

    private void GcCleanup()
    {
        // cleanup
        for (var i = 0; i < 10; i++)
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.WaitForFullGCComplete();
            GC.Collect();
        }
    }

    private static readonly Type _canaryType = typeof(Canary);

    // [MethodImpl(MethodImplOptions.NoInlining)]
    // private MarmaladeIoCKernelModule LoadMarmaladeServicesInternal(string name, Assembly a)
    //     => new MarmaladeIoCKernelModule(name, a);


    [MethodImpl(MethodImplOptions.NoInlining)]
    public IReadOnlyCollection<CanaryInfo> LoadCanariesFromAssembly(string name, Assembly a)
    {
        // find all types in teh assembly
        var types = a.GetExportedTypes();
        // canary is always a public non abstract class
        var classes = types.Where(static x => x.IsClass
                                              && (x.IsNestedPublic || x.IsPublic)
                                              && !x.IsAbstract
                                              && x.BaseType == _canaryType
                                              && (x.DeclaringType is null || x.DeclaringType.IsAssignableTo(_canaryType)))
                           .ToList();

        var topModules = new Dictionary<Type, CanaryInfo>();

        foreach (var cl in classes)
        {
            if (cl.DeclaringType is not null)
                continue;

            // get module data, and add it to the topModules dictionary
            var module = GetModuleData(cl);
            topModules.Add(cl, module);
        }

        foreach (var c in classes)
        {
            if (c.DeclaringType is not Type dt)
                continue;

            // if there is no top level module which this module is a child of
            // just print a warning and skip it
            if (!topModules.TryGetValue(dt, out var parentData))
            {
                Log.Warning("Can't load submodule {SubName} because parent module {Name} does not exist",
                    c.Name,
                    dt.Name);
                continue;
            }

            GetModuleData(c, parentData);
        }

        return topModules.Values.ToArray();
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private CanaryInfo GetModuleData(Type type, CanaryInfo? parentData = null)
    {
        var filters = type.GetCustomAttributes<FilterAttribute>(true)
                          .ToArray();

        var instance = (Canary)ActivatorUtilities.CreateInstance(_cont, type);

        var module = new CanaryInfo(instance.Name,
            parentData,
            instance,
            GetCommands(instance, type),
            filters);

        if (parentData is not null)
            parentData.Subcanaries.Add(module);

        return module;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private IReadOnlyCollection<CanaryCommandData> GetCommands(Canary instance, Type type)
    {
        var methodInfos = type
                          .GetMethods(BindingFlags.Instance
                                      | BindingFlags.DeclaredOnly
                                      | BindingFlags.Public)
                          .Where(static x =>
                          {
                              if (x.GetCustomAttribute<cmdAttribute>(true) is null)
                                  return false;

                              if (x.ReturnType.IsGenericType)
                              {
                                  var genericType = x.ReturnType.GetGenericTypeDefinition();
                                  if (genericType == typeof(Task<>))
                                      return true;

                                  // if (genericType == typeof(ValueTask<>))
                                  //     return true;

                                  Log.Warning("Method {MethodName} has an invalid return type: {ReturnType}",
                                      x.Name,
                                      x.ReturnType);

                                  return false;
                              }

                              var succ = x.ReturnType == typeof(Task)
                                         || x.ReturnType == typeof(ValueTask)
                                         || x.ReturnType == typeof(void);

                              if (!succ)
                              {
                                  Log.Warning("Method {MethodName} has an invalid return type: {ReturnType}",
                                      x.Name,
                                      x.ReturnType);
                              }

                              return succ;
                          });


        var cmds = new List<CanaryCommandData>();
        foreach (var method in methodInfos)
        {
            var filters = method.GetCustomAttributes<FilterAttribute>(true).ToArray();
            var userAndBotPerms = method.GetCustomAttributes<MarmaladePermAttribute>(true)
                                        .ToArray();
            var prio = method.GetCustomAttribute<prioAttribute>(true)?.Priority ?? 0;

            var paramInfos = method.GetParameters();
            var cmdParams = new List<ParamData>();
            var diParams = new List<Type>();
            var cmdContext = CommandContextType.Unspecified;
            var canInject = false;
            for (var paramCounter = 0; paramCounter < paramInfos.Length; paramCounter++)
            {
                var pi = paramInfos[paramCounter];

                var paramName = pi.Name ?? "unnamed";
                var isContext = paramCounter == 0 && pi.ParameterType.IsAssignableTo(typeof(AnyContext));

                var leftoverAttribute = pi.GetCustomAttribute<leftoverAttribute>(true);
                var hasDefaultValue = pi.HasDefaultValue;
                var defaultValue = pi.DefaultValue;
                var isLeftover = leftoverAttribute != null;
                var isParams = pi.GetCustomAttribute<ParamArrayAttribute>() is not null;
                var paramType = pi.ParameterType;
                var isInjected = pi.GetCustomAttribute<injectAttribute>(true) is not null;

                if (isContext)
                {
                    if (hasDefaultValue || leftoverAttribute != null || isParams)
                        throw new ArgumentException(
                            "IContext parameter cannot be optional, leftover, constant or params. "
                            + GetErrorPath(method, pi));

                    if (paramCounter != 0)
                        throw new ArgumentException($"IContext parameter has to be first. {GetErrorPath(method, pi)}");

                    canInject = true;

                    if (paramType.IsAssignableTo(typeof(GuildContext)))
                        cmdContext = CommandContextType.Guild;
                    else if (paramType.IsAssignableTo(typeof(DmContext)))
                        cmdContext = CommandContextType.Dm;
                    else
                        cmdContext = CommandContextType.Any;

                    continue;
                }

                if (isInjected)
                {
                    if (!canInject && paramCounter != 0)
                        throw new ArgumentException($"Parameters marked as [Injected] have to come after IContext");

                    canInject = true;

                    diParams.Add(paramType);
                    continue;
                }

                canInject = false;                                                                                           

                if (isParams)
                {
                    if (hasDefaultValue)
                        throw new NotSupportedException("Params can't have const values at the moment. "
                                                        + GetErrorPath(method, pi));
                    // if it's params, it means it's an array, and i only need a parser for the actual type,
                    // as the parser will run on each array element, it can't be null
                    paramType = paramType.GetElementType()!;
                }

                // leftover can only be the last parameter.
                if (isLeftover && paramCounter != paramInfos.Length - 1)
                {
                    var path = GetErrorPath(method, pi);
                    Log.Error("Only one parameter can be marked [Leftover] and it has to be the last one. {Path} ",
                        path);
                    throw new ArgumentException("Leftover attribute error.");
                }

                cmdParams.Add(new ParamData(paramType, paramName, hasDefaultValue, defaultValue, isLeftover, isParams));
            }


            var cmdAttribute = method.GetCustomAttribute<cmdAttribute>(true)!;
            var aliases = cmdAttribute.Aliases;
            if (aliases.Length == 0)
                aliases = [method.Name.ToLowerInvariant()];

            cmds.Add(new(
                aliases,
                method,
                instance,
                filters,
                userAndBotPerms,
                cmdContext,
                diParams,
                cmdParams,
                new(cmdAttribute.desc, cmdAttribute.args),
                prio
            ));
        }

        return cmds;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private string GetErrorPath(MethodInfo m, System.Reflection.ParameterInfo pi)
        => $@"Module: {m.DeclaringType?.Name} 
Command: {m.Name}
ParamName: {pi.Name}
ParamType: {pi.ParameterType.Name}";
}