using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.Authority.Plugins.Abstractions;
using StellaOps.Plugin.Hosting;

namespace StellaOps.Authority.Plugins;

internal static class AuthorityPluginLoader
{
    public static AuthorityPluginRegistrationSummary RegisterPlugins(
        IServiceCollection services,
        IConfiguration configuration,
        PluginHostOptions hostOptions,
        IReadOnlyCollection<AuthorityPluginContext> pluginContexts,
        ILogger? logger)
    {
        ArgumentNullException.ThrowIfNull(services);
        ArgumentNullException.ThrowIfNull(configuration);
        ArgumentNullException.ThrowIfNull(hostOptions);
        ArgumentNullException.ThrowIfNull(pluginContexts);

        if (pluginContexts.Count == 0)
        {
            return AuthorityPluginRegistrationSummary.Empty;
        }

        var loadResult = PluginHost.LoadPlugins(hostOptions, logger);
        var descriptors = loadResult.Plugins
            .Select(p => new LoadedPluginDescriptor(p.Assembly, p.AssemblyPath))
            .ToArray();

        return RegisterPluginsCore(
            services,
            configuration,
            pluginContexts,
            descriptors,
            loadResult.MissingOrderedPlugins,
            logger);
    }

    internal static AuthorityPluginRegistrationSummary RegisterPluginsCore(
        IServiceCollection services,
        IConfiguration configuration,
        IReadOnlyCollection<AuthorityPluginContext> pluginContexts,
        IReadOnlyCollection<LoadedPluginDescriptor> loadedAssemblies,
        IReadOnlyCollection<string> missingOrdered,
        ILogger? logger)
    {
        var registrarLookup = DiscoverRegistrars(loadedAssemblies, logger);
        var registered = new List<string>();
        var failures = new List<AuthorityPluginRegistrationFailure>();

        foreach (var pluginContext in pluginContexts)
        {
            var manifest = pluginContext.Manifest;

            if (!manifest.Enabled)
            {
                logger?.LogInformation(
                    "Skipping disabled Authority plugin '{PluginName}' ({PluginType}).",
                    manifest.Name,
                    manifest.Type);
                continue;
            }

            if (!IsAssemblyLoaded(manifest, loadedAssemblies))
            {
                var reason = $"Assembly '{manifest.AssemblyName ?? manifest.AssemblyPath ?? manifest.Type}' was not loaded.";
                logger?.LogError(
                    "Failed to register Authority plugin '{PluginName}': {Reason}",
                    manifest.Name,
                    reason);
                failures.Add(new AuthorityPluginRegistrationFailure(manifest.Name, reason));
                continue;
            }

            if (!registrarLookup.TryGetValue(manifest.Type, out var registrar))
            {
                var reason = $"No registrar found for plugin type '{manifest.Type}'.";
                logger?.LogError(
                    "Failed to register Authority plugin '{PluginName}': {Reason}",
                    manifest.Name,
                    reason);
                failures.Add(new AuthorityPluginRegistrationFailure(manifest.Name, reason));
                continue;
            }

            try
            {
                registrar.Register(new AuthorityPluginRegistrationContext(services, pluginContext, configuration));
                registered.Add(manifest.Name);

                logger?.LogInformation(
                    "Registered Authority plugin '{PluginName}' ({PluginType}).",
                    manifest.Name,
                    manifest.Type);
            }
            catch (Exception ex)
            {
                var reason = $"Registration threw {ex.GetType().Name}.";
                logger?.LogError(
                    ex,
                    "Failed to register Authority plugin '{PluginName}'.",
                    manifest.Name);
                failures.Add(new AuthorityPluginRegistrationFailure(manifest.Name, reason));
            }
        }

        if (missingOrdered.Count > 0)
        {
            foreach (var missing in missingOrdered)
            {
                logger?.LogWarning(
                    "Configured plugin '{PluginName}' was not found in the plugin directory.",
                    missing);
            }
        }

        return new AuthorityPluginRegistrationSummary(registered, failures, missingOrdered);
    }

    private static Dictionary<string, IAuthorityPluginRegistrar> DiscoverRegistrars(
        IReadOnlyCollection<LoadedPluginDescriptor> loadedAssemblies,
        ILogger? logger)
    {
        var lookup = new Dictionary<string, IAuthorityPluginRegistrar>(StringComparer.OrdinalIgnoreCase);

        foreach (var descriptor in loadedAssemblies)
        {
            foreach (var type in GetLoadableTypes(descriptor.Assembly))
            {
                if (!typeof(IAuthorityPluginRegistrar).IsAssignableFrom(type) || type.IsAbstract || type.IsInterface)
                {
                    continue;
                }

                try
                {
                    if (Activator.CreateInstance(type) is not IAuthorityPluginRegistrar registrar)
                    {
                        continue;
                    }

                    if (string.IsNullOrWhiteSpace(registrar.PluginType))
                    {
                        logger?.LogWarning(
                            "Authority plugin registrar '{RegistrarType}' returned an empty plugin type and will be ignored.",
                            type.FullName);
                        continue;
                    }

                    if (lookup.TryGetValue(registrar.PluginType, out var existing))
                    {
                        logger?.LogWarning(
                            "Multiple registrars detected for plugin type '{PluginType}'. Replacing '{ExistingType}' with '{RegistrarType}'.",
                            registrar.PluginType,
                            existing.GetType().FullName,
                            type.FullName);
                    }

                    lookup[registrar.PluginType] = registrar;
                }
                catch (Exception ex)
                {
                    logger?.LogError(
                        ex,
                        "Failed to instantiate Authority plugin registrar '{RegistrarType}'.",
                        type.FullName);
                }
            }
        }

        return lookup;
    }

    private static bool IsAssemblyLoaded(
        AuthorityPluginManifest manifest,
        IReadOnlyCollection<LoadedPluginDescriptor> loadedAssemblies)
    {
        if (!string.IsNullOrWhiteSpace(manifest.AssemblyName) &&
            loadedAssemblies.Any(descriptor =>
                string.Equals(
                    descriptor.Assembly.GetName().Name,
                    manifest.AssemblyName,
                    StringComparison.OrdinalIgnoreCase)))
        {
            return true;
        }

        if (!string.IsNullOrWhiteSpace(manifest.AssemblyPath) &&
            loadedAssemblies.Any(descriptor =>
                string.Equals(
                    descriptor.AssemblyPath,
                    manifest.AssemblyPath,
                    StringComparison.OrdinalIgnoreCase)))
        {
            return true;
        }

        // As a fallback, assume any loaded assembly whose simple name contains the plugin type is a match.
        return loadedAssemblies.Any(descriptor =>
            descriptor.Assembly.GetName().Name?.Contains(manifest.Type, StringComparison.OrdinalIgnoreCase) == true);
    }

    private static IEnumerable<Type> GetLoadableTypes(Assembly assembly)
    {
        try
        {
            return assembly.GetTypes();
        }
        catch (ReflectionTypeLoadException ex)
        {
            return ex.Types.Where(static type => type is not null)!;
        }
    }

    internal readonly record struct LoadedPluginDescriptor(
        Assembly Assembly,
        string AssemblyPath);
}
