using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
using StellaOps.Authority.Plugins.Abstractions;
using StellaOps.Authority.Plugin.Standard.Bootstrap;
using StellaOps.Authority.Plugin.Standard.Security;
using StellaOps.Authority.Plugin.Standard.Storage;
using StellaOps.Authority.Storage.Mongo.Stores;
using StellaOps.Cryptography;
using StellaOps.Cryptography.DependencyInjection;

namespace StellaOps.Authority.Plugin.Standard;

internal sealed class StandardPluginRegistrar : IAuthorityPluginRegistrar
{
    public string PluginType => "standard";

    public void Register(AuthorityPluginRegistrationContext context)
    {
        if (context is null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        var pluginName = context.Plugin.Manifest.Name;

        context.Services.AddSingleton<StandardClaimsEnricher>();
        context.Services.AddSingleton<IClaimsEnricher>(sp => sp.GetRequiredService<StandardClaimsEnricher>());

        context.Services.AddStellaOpsCrypto();

        var configPath = context.Plugin.Manifest.ConfigPath;

        context.Services.AddOptions<StandardPluginOptions>(pluginName)
            .Bind(context.Plugin.Configuration)
            .PostConfigure(options =>
            {
                options.Normalize(configPath);
                options.Validate(pluginName);
            })
            .ValidateOnStart();

        context.Services.AddSingleton(sp =>
        {
            var database = sp.GetRequiredService<IMongoDatabase>();
            var optionsMonitor = sp.GetRequiredService<IOptionsMonitor<StandardPluginOptions>>();
            var pluginOptions = optionsMonitor.Get(pluginName);
            var cryptoProvider = sp.GetRequiredService<ICryptoProvider>();
            var passwordHasher = new CryptoPasswordHasher(pluginOptions, cryptoProvider);
            var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
            var registrarLogger = loggerFactory.CreateLogger<StandardPluginRegistrar>();

            var baselinePolicy = new PasswordPolicyOptions();
            if (pluginOptions.PasswordPolicy.IsWeakerThan(baselinePolicy))
            {
                registrarLogger.LogWarning(
                    "Standard plugin '{Plugin}' configured a weaker password policy (minLength={Length}, requireUpper={Upper}, requireLower={Lower}, requireDigit={Digit}, requireSymbol={Symbol}) than the baseline (minLength={BaseLength}, requireUpper={BaseUpper}, requireLower={BaseLower}, requireDigit={BaseDigit}, requireSymbol={BaseSymbol}).",
                    pluginName,
                    pluginOptions.PasswordPolicy.MinimumLength,
                    pluginOptions.PasswordPolicy.RequireUppercase,
                    pluginOptions.PasswordPolicy.RequireLowercase,
                    pluginOptions.PasswordPolicy.RequireDigit,
                    pluginOptions.PasswordPolicy.RequireSymbol,
                    baselinePolicy.MinimumLength,
                    baselinePolicy.RequireUppercase,
                    baselinePolicy.RequireLowercase,
                    baselinePolicy.RequireDigit,
                    baselinePolicy.RequireSymbol);
            }

            return new StandardUserCredentialStore(
                pluginName,
                database,
                pluginOptions,
                passwordHasher,
                loggerFactory.CreateLogger<StandardUserCredentialStore>());
        });

        context.Services.AddSingleton(sp =>
        {
            var clientStore = sp.GetRequiredService<IAuthorityClientStore>();
            var revocationStore = sp.GetRequiredService<IAuthorityRevocationStore>();
            var timeProvider = sp.GetRequiredService<TimeProvider>();
            return new StandardClientProvisioningStore(pluginName, clientStore, revocationStore, timeProvider);
        });

        context.Services.AddSingleton<IIdentityProviderPlugin>(sp =>
        {
            var store = sp.GetRequiredService<StandardUserCredentialStore>();
            var clientProvisioningStore = sp.GetRequiredService<StandardClientProvisioningStore>();
            var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
            return new StandardIdentityProviderPlugin(
                context.Plugin,
                store,
                clientProvisioningStore,
                sp.GetRequiredService<StandardClaimsEnricher>(),
                loggerFactory.CreateLogger<StandardIdentityProviderPlugin>());
        });

        context.Services.AddSingleton<IClientProvisioningStore>(sp =>
            sp.GetRequiredService<StandardClientProvisioningStore>());

        context.Services.AddSingleton<IHostedService>(sp =>
            new StandardPluginBootstrapper(
                pluginName,
                sp.GetRequiredService<IOptionsMonitor<StandardPluginOptions>>(),
                sp.GetRequiredService<StandardUserCredentialStore>(),
                sp.GetRequiredService<ILogger<StandardPluginBootstrapper>>()));
    }
}
