using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using StellaOps.Authority.Plugins.Abstractions;

namespace StellaOps.Authority;

internal sealed class AuthorityIdentityProviderRegistry : IAuthorityIdentityProviderRegistry
{
    private readonly IReadOnlyDictionary<string, IIdentityProviderPlugin> providersByName;
    private readonly ReadOnlyCollection<IIdentityProviderPlugin> providers;
    private readonly ReadOnlyCollection<IIdentityProviderPlugin> passwordProviders;
    private readonly ReadOnlyCollection<IIdentityProviderPlugin> mfaProviders;
    private readonly ReadOnlyCollection<IIdentityProviderPlugin> clientProvisioningProviders;

    public AuthorityIdentityProviderRegistry(
        IEnumerable<IIdentityProviderPlugin> providerInstances,
        ILogger<AuthorityIdentityProviderRegistry> logger)
    {
        logger = logger ?? throw new ArgumentNullException(nameof(logger));

        var orderedProviders = providerInstances?
            .Where(static p => p is not null)
            .OrderBy(static p => p.Name, StringComparer.OrdinalIgnoreCase)
            .ToList() ?? new List<IIdentityProviderPlugin>();

        var uniqueProviders = new List<IIdentityProviderPlugin>(orderedProviders.Count);
        var password = new List<IIdentityProviderPlugin>();
        var mfa = new List<IIdentityProviderPlugin>();
        var clientProvisioning = new List<IIdentityProviderPlugin>();

        var dictionary = new Dictionary<string, IIdentityProviderPlugin>(StringComparer.OrdinalIgnoreCase);

        foreach (var provider in orderedProviders)
        {
            if (string.IsNullOrWhiteSpace(provider.Name))
            {
                logger.LogWarning(
                    "Identity provider plugin of type '{PluginType}' was registered with an empty name and will be ignored.",
                    provider.Type);
                continue;
            }

            if (!dictionary.TryAdd(provider.Name, provider))
            {
                logger.LogWarning(
                    "Duplicate identity provider name '{PluginName}' detected; ignoring additional registration for type '{PluginType}'.",
                    provider.Name,
                    provider.Type);
                continue;
            }

            uniqueProviders.Add(provider);

            if (provider.Capabilities.SupportsPassword)
            {
                password.Add(provider);
            }

            if (provider.Capabilities.SupportsMfa)
            {
                mfa.Add(provider);
            }

            if (provider.Capabilities.SupportsClientProvisioning)
            {
                clientProvisioning.Add(provider);
            }
        }

        providersByName = dictionary;
        providers = new ReadOnlyCollection<IIdentityProviderPlugin>(uniqueProviders);
        passwordProviders = new ReadOnlyCollection<IIdentityProviderPlugin>(password);
        mfaProviders = new ReadOnlyCollection<IIdentityProviderPlugin>(mfa);
        clientProvisioningProviders = new ReadOnlyCollection<IIdentityProviderPlugin>(clientProvisioning);

        AggregateCapabilities = new AuthorityIdentityProviderCapabilities(
            SupportsPassword: passwordProviders.Count > 0,
            SupportsMfa: mfaProviders.Count > 0,
            SupportsClientProvisioning: clientProvisioningProviders.Count > 0);
    }

    public IReadOnlyCollection<IIdentityProviderPlugin> Providers => providers;

    public IReadOnlyCollection<IIdentityProviderPlugin> PasswordProviders => passwordProviders;

    public IReadOnlyCollection<IIdentityProviderPlugin> MfaProviders => mfaProviders;

    public IReadOnlyCollection<IIdentityProviderPlugin> ClientProvisioningProviders => clientProvisioningProviders;

    public AuthorityIdentityProviderCapabilities AggregateCapabilities { get; }

    public bool TryGet(string name, [NotNullWhen(true)] out IIdentityProviderPlugin? provider)
    {
        if (string.IsNullOrWhiteSpace(name))
        {
            provider = null;
            return false;
        }

        return providersByName.TryGetValue(name, out provider);
    }
}
