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.DependencyInjection;
using StellaOps.Plugin.Hosting;

namespace StellaOps.Feedser.Core.Jobs;

public static class JobPluginRegistrationExtensions
{
    public static IServiceCollection RegisterJobPluginRoutines(
        this IServiceCollection services,
        IConfiguration configuration,
        PluginHostOptions options,
        ILogger? logger = null)
    {
        ArgumentNullException.ThrowIfNull(services);
        ArgumentNullException.ThrowIfNull(configuration);
        ArgumentNullException.ThrowIfNull(options);

        var loadResult = PluginHost.LoadPlugins(options, logger);

        if (!services.Any(sd => sd.ServiceType == typeof(PluginHostResult)))
        {
            services.AddSingleton(loadResult);
        }

        var currentServices = services;
        var seenRoutineTypes = new HashSet<string>(StringComparer.Ordinal);

        foreach (var plugin in loadResult.Plugins)
        {
            foreach (var routineType in GetRoutineTypes(plugin.Assembly))
            {
                if (!typeof(IDependencyInjectionRoutine).IsAssignableFrom(routineType))
                {
                    continue;
                }

                if (routineType.IsInterface || routineType.IsAbstract)
                {
                    continue;
                }

                var routineKey = routineType.FullName ?? routineType.Name;
                if (!seenRoutineTypes.Add(routineKey))
                {
                    continue;
                }

                IDependencyInjectionRoutine? routineInstance;
                try
                {
                    routineInstance = Activator.CreateInstance(routineType) as IDependencyInjectionRoutine;
                }
                catch (Exception ex)
                {
                    logger?.LogWarning(
                        ex,
                        "Failed to create dependency injection routine {Routine} from plugin {Plugin}.",
                        routineType.FullName ?? routineType.Name,
                        plugin.Assembly.FullName ?? plugin.AssemblyPath);
                    continue;
                }

                if (routineInstance is null)
                {
                    continue;
                }

                try
                {
                    var updated = routineInstance.Register(currentServices, configuration);
                    if (updated is not null && !ReferenceEquals(updated, currentServices))
                    {
                        currentServices = updated;
                    }
                }
                catch (Exception ex)
                {
                    logger?.LogError(
                        ex,
                        "Dependency injection routine {Routine} from plugin {Plugin} threw during registration.",
                        routineType.FullName ?? routineType.Name,
                        plugin.Assembly.FullName ?? plugin.AssemblyPath);
                }
            }
        }

        if (loadResult.MissingOrderedPlugins.Count > 0)
        {
            logger?.LogWarning(
                "Missing ordered plugin(s): {Missing}",
                string.Join(", ", loadResult.MissingOrderedPlugins));
        }

        return currentServices;
    }

    private static IEnumerable<Type> GetRoutineTypes(Assembly assembly)
    {
        if (assembly is null)
        {
            yield break;
        }

        Type[] types;
        try
        {
            types = assembly.GetTypes();
        }
        catch (ReflectionTypeLoadException ex)
        {
            types = ex.Types.Where(static t => t is not null)!
                .Select(static t => t!)
                .ToArray();
        }

        foreach (var type in types)
        {
            yield return type;
        }
    }
}
