// Project Makoto
// Copyright (C) 2024  Fortunevale
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY

using Microsoft.Extensions.Logging;

namespace ProjectMakoto.Util.Initializers;
internal static class DisCatSharpExtensionsLoader
{
    static Bot bot = null;
    static List<DisCatSharp.ApplicationCommands.Entities.CommandTranslator> singleCommandTranslations = new();
    static List<DisCatSharp.ApplicationCommands.Entities.GroupTranslator> groupCommandTranslations = new();

    internal static void GetCommandTranslations(ApplicationCommandsTranslationContext x)
    {
        if (singleCommandTranslations.IsNotNullAndNotEmpty() && groupCommandTranslations.IsNotNullAndNotEmpty())
        {
            x.AddSingleTranslation(JsonConvert.SerializeObject(singleCommandTranslations));
            x.AddGroupTranslation(JsonConvert.SerializeObject(groupCommandTranslations));
            return;
        }

        object CreateTranslationRecursively(Type typeToCreate, CommandTranslation translation)
        {
            try
            {
                var nameValues = translation.Names;

                if (nameValues is null)
                    return null;

                var descriptionValues = translation.Descriptions;
                var typeValue = translation.Type;
                var optionsValues = translation.Options;
                var choicesValues = translation.Choices;
                var groupsValues = translation.Groups;
                var commandsValues = translation.Commands;

                Log.Verbose("Creating instance of '{type}'", typeToCreate.Name);
                var translator = Activator.CreateInstance(typeToCreate);

                var createTypeProperties = typeToCreate.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
                createTypeProperties.First(x => x.GetCustomAttributes().Any(attr => attr is JsonPropertyAttribute attribute && attribute.PropertyName == "name")).SetValue(translator, nameValues["en"]);

                if (typeToCreate == typeof(DisCatSharp.ApplicationCommands.Entities.GroupTranslator) || typeToCreate == typeof(DisCatSharp.ApplicationCommands.Entities.CommandTranslator))
                    createTypeProperties.First(x => x.GetCustomAttributes().Any(attr => attr is JsonPropertyAttribute attribute && attribute.PropertyName == "type")).SetValue(translator, (ApplicationCommandType?)typeValue);

                if (createTypeProperties.Any(x => x.GetCustomAttributes().Any(attr => attr is JsonPropertyAttribute attribute && attribute.PropertyName == "description")))
                    createTypeProperties.First(x => x.GetCustomAttributes().Any(attr => attr is JsonPropertyAttribute attribute && attribute.PropertyName == "description")).SetValue(translator, descriptionValues["en"]);

                Dictionary<string, string> NameTranslationDictionary = new();
                foreach (var nameTranslation in nameValues ?? new())
                {
                    if (nameTranslation.Key == "en")
                    {
                        NameTranslationDictionary.Add("en-GB", nameTranslation.Value);
                        NameTranslationDictionary.Add("en-US", nameTranslation.Value);
                        continue;
                    }

                    NameTranslationDictionary.Add(nameTranslation.Key, nameTranslation.Value);
                }
                if (createTypeProperties.Any(x => x.GetCustomAttributes().Any(attr => attr is JsonPropertyAttribute attribute && attribute.PropertyName == "name_translations")))
                    createTypeProperties.First(x => x.GetCustomAttributes().Any(attr => attr is JsonPropertyAttribute attribute && attribute.PropertyName == "name_translations")).SetValue(translator, NameTranslationDictionary);

                Dictionary<string, string> DescriptionTranslationDictionary = new();
                foreach (var descriptionTranslations in descriptionValues ?? new())
                {
                    if (descriptionTranslations.Key == "en")
                    {
                        DescriptionTranslationDictionary.Add("en-GB", descriptionTranslations.Value);
                        DescriptionTranslationDictionary.Add("en-US", descriptionTranslations.Value);
                        continue;
                    }

                    DescriptionTranslationDictionary.Add(descriptionTranslations.Key, descriptionTranslations.Value);
                }
                if (createTypeProperties.Any(x => x.GetCustomAttributes().Any(attr => attr is JsonPropertyAttribute attribute && attribute.PropertyName == "description_translations")))
                    createTypeProperties.First(x => x.GetCustomAttributes().Any(attr => attr is JsonPropertyAttribute attribute && attribute.PropertyName == "description_translations")).SetValue(translator, DescriptionTranslationDictionary);

                if (commandsValues is not null && createTypeProperties.Any(x => x.GetCustomAttributes().Any(attr => attr is JsonPropertyAttribute attribute && attribute.PropertyName == "commands")))
                {
                    Log.Verbose("Creating sub-command translations for command '{name}'", nameValues.First());

                    var commandProperty = createTypeProperties.First(x => x.GetCustomAttributes().Any(attr => attr is JsonPropertyAttribute attribute && attribute.PropertyName == "commands"));
                    commandProperty.SetValue(translator, new List<DisCatSharp.ApplicationCommands.Entities.CommandTranslator>());
                    foreach (var value in commandsValues)
                    {
                        var obj = (DisCatSharp.ApplicationCommands.Entities.CommandTranslator)CreateTranslationRecursively(typeof(DisCatSharp.ApplicationCommands.Entities.CommandTranslator), value);

                        if (obj is null)
                            continue;

                        ((List<DisCatSharp.ApplicationCommands.Entities.CommandTranslator>)commandProperty.GetValue(translator)).Add(obj);
                    }

                    if (((List<DisCatSharp.ApplicationCommands.Entities.CommandTranslator>)commandProperty.GetValue(translator)).Count == 0)
                        commandProperty.SetValue(translator, null);
                }

                if (optionsValues is not null && createTypeProperties.Any(x => x.GetCustomAttributes().Any(attr => attr is JsonPropertyAttribute attribute && attribute.PropertyName == "options")))
                {
                    Log.Verbose("Creating option translations for command '{name}'", nameValues.First());

                    var optionProperty = createTypeProperties.First(x => x.GetCustomAttributes().Any(attr => attr is JsonPropertyAttribute attribute && attribute.PropertyName == "options"));
                    optionProperty.SetValue(translator, new List<DisCatSharp.ApplicationCommands.Entities.OptionTranslator>());
                    foreach (var value in optionsValues)
                    {
                        var obj = (DisCatSharp.ApplicationCommands.Entities.OptionTranslator)CreateTranslationRecursively(typeof(DisCatSharp.ApplicationCommands.Entities.OptionTranslator), value);

                        if (obj is null)
                            continue;

                        ((List<DisCatSharp.ApplicationCommands.Entities.OptionTranslator>)optionProperty.GetValue(translator)).Add(obj);
                    }

                    if (((List<DisCatSharp.ApplicationCommands.Entities.OptionTranslator>)optionProperty.GetValue(translator)).Count == 0)
                        optionProperty.SetValue(translator, null);
                }

                if (choicesValues is not null && createTypeProperties.Any(x => x.GetCustomAttributes().Any(attr => attr is JsonPropertyAttribute attribute && attribute.PropertyName == "choices")))
                {
                    Log.Verbose("Creating choice translations for command '{name}'", nameValues.First());

                    var choiceProperty = createTypeProperties.First(x => x.GetCustomAttributes().Any(attr => attr is JsonPropertyAttribute attribute && attribute.PropertyName == "choices"));
                    choiceProperty.SetValue(translator, new List<DisCatSharp.ApplicationCommands.Entities.ChoiceTranslator>());
                    foreach (var value in choicesValues)
                    {
                        var obj = (DisCatSharp.ApplicationCommands.Entities.ChoiceTranslator)CreateTranslationRecursively(typeof(DisCatSharp.ApplicationCommands.Entities.ChoiceTranslator), value);

                        if (obj is null)
                            continue;

                        ((List<DisCatSharp.ApplicationCommands.Entities.ChoiceTranslator>)choiceProperty.GetValue(translator)).Add(obj);
                    }

                    if (((List<DisCatSharp.ApplicationCommands.Entities.ChoiceTranslator>)choiceProperty.GetValue(translator)).Count == 0)
                        choiceProperty.SetValue(translator, null);
                }

                if (groupsValues is not null && createTypeProperties.Any(x => x.GetCustomAttributes().Any(attr => attr is JsonPropertyAttribute attribute && attribute.PropertyName == "groups")))
                {
                    Log.Verbose("Creating group translations for command '{name}'", nameValues.First());

                    var groupProperty = createTypeProperties.First(x => x.GetCustomAttributes().Any(attr => attr is JsonPropertyAttribute attribute && attribute.PropertyName == "groups"));
                    groupProperty.SetValue(translator, new List<DisCatSharp.ApplicationCommands.Entities.SubGroupTranslator>());

                    foreach (var value in groupsValues)
                    {
                        var obj = (DisCatSharp.ApplicationCommands.Entities.SubGroupTranslator)CreateTranslationRecursively(typeof(DisCatSharp.ApplicationCommands.Entities.SubGroupTranslator), value);

                        if (obj is null)
                            continue;

                        ((List<DisCatSharp.ApplicationCommands.Entities.SubGroupTranslator>)groupProperty.GetValue(translator)).Add(obj);
                    }

                    if (((List<DisCatSharp.ApplicationCommands.Entities.SubGroupTranslator>)groupProperty.GetValue(translator)).Count == 0)
                        groupProperty.SetValue(translator, null);
                }

                return translator;
            }
            catch (Exception ex)
            {
                Log.Error(ex, "Failed to generate DCS-Compatible Translations");
                throw;
            }
        }

        foreach (var translation in bot.LoadedTranslations.CommandList)
            singleCommandTranslations.Add(
                (DisCatSharp.ApplicationCommands.Entities.CommandTranslator)CreateTranslationRecursively(typeof(DisCatSharp.ApplicationCommands.Entities.CommandTranslator), translation));

        foreach (var translation in bot.LoadedTranslations.CommandList)
            groupCommandTranslations.Add(
                (DisCatSharp.ApplicationCommands.Entities.GroupTranslator)CreateTranslationRecursively(typeof(DisCatSharp.ApplicationCommands.Entities.GroupTranslator), translation));

        x.AddSingleTranslation(JsonConvert.SerializeObject(singleCommandTranslations));
        x.AddGroupTranslation(JsonConvert.SerializeObject(groupCommandTranslations));
    }

    public static async Task Load(Bot bot)
    {
        DisCatSharpExtensionsLoader.bot = bot;

        if (bot.status.LoadedConfig.Secrets.Discord.Token.Length <= 0)
        {
            Log.Fatal("No discord token provided");
            await Task.Delay(1000);
            Environment.Exit((int)ExitCodes.NoToken);
            return;
        }

        Log.Debug("Registering DiscordClient..");

        bot.DiscordClient = new DiscordShardedClient(new DiscordConfiguration
        {
            Token = bot.status.LoadedConfig.Secrets.Discord.Token,
            TokenType = TokenType.Bot,
            MinimumLogLevel = Microsoft.Extensions.Logging.LogLevel.Trace,
            Intents = DiscordIntents.All,
            AutoReconnect = true,
            LoggerFactory = bot.msLoggerFactory,
            HttpTimeout = TimeSpan.FromSeconds(60),
            MessageCacheSize = 4096,
            EnableSentry = true,
            ReportMissingFields = bot.status.LoadedConfig.IsDev,
            AttachUserInfo = true,
            DeveloperUserId = 411950662662881290,
            DisableUpdateCheck = true,
        });

        bot.ExperienceHandler = new(bot);

        Log.Debug("Registering CommandsNext..");

        var cNext = await bot.DiscordClient.UseCommandsNextAsync(new CommandsNextConfiguration
        {
            EnableDefaultHelp = false,
            EnableMentionPrefix = false,
            IgnoreExtraArguments = true,
            EnableDms = false,
            ServiceProvider = new ServiceCollection()
                            .AddSingleton(bot)
                            .BuildServiceProvider(),
            PrefixResolver = new PrefixResolverDelegate(bot.GetPrefix)
        });

        Log.Debug("Registering DisCatSharp TwoFactor..");

        var tfa = bot.DiscordClient.UseTwoFactorAsync(new TwoFactorConfiguration
        {
            ResponseConfiguration = new TwoFactorResponseConfiguration
            {
                ShowResponse = false,
                AuthenticatorAccountPrefix = "Project Makoto"
            },
            Issuer = "Project Makoto",
        });

        DiscordEventHandler.SetupEvents(bot);
        bot.DiscordClient.GuildDownloadCompleted += bot.GuildDownloadCompleted;

        Log.Debug("Registering Interactivity..");
        _ = await bot.DiscordClient.UseInteractivityAsync(new InteractivityConfiguration { });

        var appCommands = await bot.DiscordClient.UseApplicationCommandsAsync(new ApplicationCommandsConfiguration
        {
            ServiceProvider = new ServiceCollection()
                                .AddSingleton(bot)
                                .BuildServiceProvider(),
            EnableDefaultHelp = false,
            EnableLocalization = true,
            DebugStartup = true
        });

        if (bot.status.CurrentAppHash != bot.status.LoadedConfig.DontModify.LastKnownHash && Directory.Exists("CompiledCommands"))
        {
            Log.Debug("Clearing cached Commands..");
            await FileExtensions.CleanupFilesAndDirectories(new(), Directory.GetFiles("CompiledCommands").ToList());
        }

        await BasePlugin.RaisePreLogin(bot, bot.DiscordClient);

        Log.Debug("Compiling Built-In Commands..");
        var commandModules = Commands.Commands.GetList();
        bot._CommandModules = commandModules;
        var assemblies = await CommandCompiler.BuildCommands(bot, bot.status.CurrentAppHash, commandModules, null);
        CommandCompiler.RegisterAssemblies(bot, cNext, appCommands, GetCommandTranslations, assemblies);

        Log.Debug("Registering Debug Commands..");
        appCommands.RegisterGuildCommands<ApplicationCommands.DebugCommands>(bot.status.LoadedConfig.Discord.DevelopmentGuild, GetCommandTranslations);

        Log.Debug("Registering Command Converters..");
        cNext.RegisterConverter(new CustomArgumentConverter.BoolConverter());
        cNext.RegisterConverter(new CustomArgumentConverter.AttachmentConverter());

        var commandsNextTypes = new List<Type>();
        var applicationCommandTypes = new List<Type>();

        await Util.Initializers.PluginLoader.LoadPluginCommands(bot, cNext, appCommands);

        _ = Task.Run(async () =>
        {
            while (!bot.status.DiscordInitialized)
                await Task.Delay(100);

            Stopwatch sw = new();
            sw.Start();

            _ = bot.DiscordClient.UpdateStatusAsync(userStatus: UserStatus.Online, activity: new DiscordActivity("Registering commands..", ActivityType.Custom));

            var applicationCommandsExtension = bot.DiscordClient.GetFirstShard().GetApplicationCommands();
            while (applicationCommandsExtension?.RegisteredCommands?.Count == 0 && sw.ElapsedMilliseconds < TimeSpan.FromMinutes(5).TotalMilliseconds)
                await Task.Delay(1000);

            if (applicationCommandsExtension?.RegisteredCommands?.Count == 0)
            {
                Log.Fatal("Commands did not register.");
                _ = bot.ExitApplication(true);
                return;
            }

            bot.status.DiscordCommandsRegistered = true;

            while (true)
            {
                try
                {
                    if (bot.DatabaseClient.Disposed)
                        return;

                    await bot.DiscordClient.UpdateStatusAsync(activity: new DiscordActivity($"{bot.DiscordClient.GetGuilds().Count.ToString("N0", CultureInfo.CreateSpecificCulture("en-US"))} guilds | Up for {Math.Round((DateTime.UtcNow - bot.status.startupTime).TotalHours, 1).ToString(CultureInfo.CreateSpecificCulture("en-US"))}h", ActivityType.Custom));
                    await Task.Delay(30000);
                }
                catch (Exception ex)
                {
                    Log.Error(ex, "Failed to update user status");
                    await Task.Delay(30000);
                }
            }
        });
    }
}
