using StellaOps.Plugin.Hosting;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Threading;
using System.Linq;
using System.Threading.Tasks;

namespace StellaOps.Plugin;

public interface IAvailabilityPlugin
{
    string Name { get; }
    bool IsAvailable(IServiceProvider services);
}

public interface IFeedConnector
{
    string SourceName { get; }
    Task FetchAsync(IServiceProvider services, CancellationToken cancellationToken);
    Task ParseAsync(IServiceProvider services, CancellationToken cancellationToken);
    Task MapAsync(IServiceProvider services, CancellationToken cancellationToken);
}

public interface IFeedExporter
{
    string Name { get; }
    Task ExportAsync(IServiceProvider services, CancellationToken cancellationToken);
}

public interface IConnectorPlugin : IAvailabilityPlugin
{
    IFeedConnector Create(IServiceProvider services);
}

public interface IExporterPlugin : IAvailabilityPlugin
{
    IFeedExporter Create(IServiceProvider services);
}

public sealed class PluginCatalog
{
    private readonly List<Assembly> _assemblies = new();
    private readonly HashSet<string> _assemblyLocations = new(StringComparer.OrdinalIgnoreCase);

    public PluginCatalog AddAssembly(Assembly assembly)
    {
        if (assembly == null) throw new ArgumentNullException(nameof(assembly));
        if (_assemblies.Contains(assembly))
        {
            return this;
        }

        _assemblies.Add(assembly);
        if (!string.IsNullOrWhiteSpace(assembly.Location))
        {
            _assemblyLocations.Add(Path.GetFullPath(assembly.Location));
        }
        return this;
    }

    public PluginCatalog AddFromDirectory(string directory, string searchPattern = "StellaOps.Feedser.*.dll")
    {
        if (string.IsNullOrWhiteSpace(directory)) throw new ArgumentException("Directory is required", nameof(directory));

        var fullDirectory = Path.GetFullPath(directory);
        var options = new PluginHostOptions
        {
            PluginsDirectory = fullDirectory,
            EnsureDirectoryExists = false,
            RecursiveSearch = false,
        };
        options.SearchPatterns.Add(searchPattern);

        var result = PluginHost.LoadPlugins(options);

        foreach (var plugin in result.Plugins)
        {
            AddAssembly(plugin.Assembly);
        }

        return this;
    }

    public IReadOnlyList<IConnectorPlugin> GetConnectorPlugins() => PluginLoader.LoadPlugins<IConnectorPlugin>(_assemblies);

    public IReadOnlyList<IExporterPlugin> GetExporterPlugins() => PluginLoader.LoadPlugins<IExporterPlugin>(_assemblies);

    public IReadOnlyList<IConnectorPlugin> GetAvailableConnectorPlugins(IServiceProvider services)
        => FilterAvailable(GetConnectorPlugins(), services);

    public IReadOnlyList<IExporterPlugin> GetAvailableExporterPlugins(IServiceProvider services)
        => FilterAvailable(GetExporterPlugins(), services);

    private static IReadOnlyList<TPlugin> FilterAvailable<TPlugin>(IEnumerable<TPlugin> plugins, IServiceProvider services)
        where TPlugin : IAvailabilityPlugin
    {
        var list = new List<TPlugin>();
        foreach (var plugin in plugins)
        {
            try
            {
                if (plugin.IsAvailable(services))
                {
                    list.Add(plugin);
                }
            }
            catch
            {
                // Treat exceptions as plugin not available.
            }
        }
        return list;
    }
}

public static class PluginLoader
{
    public static IReadOnlyList<TPlugin> LoadPlugins<TPlugin>(IEnumerable<Assembly> assemblies)
        where TPlugin : class
    {
        if (assemblies == null) throw new ArgumentNullException(nameof(assemblies));

        var plugins = new List<TPlugin>();
        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        foreach (var assembly in assemblies)
        {
            foreach (var candidate in SafeGetTypes(assembly))
            {
                if (candidate.IsAbstract || candidate.IsInterface)
                {
                    continue;
                }

                if (!typeof(TPlugin).IsAssignableFrom(candidate))
                {
                    continue;
                }

                if (Activator.CreateInstance(candidate) is not TPlugin plugin)
                {
                    continue;
                }

                var key = candidate.FullName ?? candidate.Name;
                if (key is null || !seen.Add(key))
                {
                    continue;
                }

                plugins.Add(plugin);
            }
        }

        return plugins;
    }

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

