using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;

namespace StellaOps.Plugin.Hosting;

public static class PluginHost
{
    private static readonly object Sync = new();
    private static readonly Dictionary<string, PluginAssembly> LoadedPlugins = new(StringComparer.OrdinalIgnoreCase);

    public static PluginHostResult LoadPlugins(PluginHostOptions options, ILogger? logger = null)
    {
        if (options == null)
        {
            throw new ArgumentNullException(nameof(options));
        }

        var baseDirectory = options.ResolveBaseDirectory();
        var pluginDirectory = ResolvePluginDirectory(options, baseDirectory);

        if (options.EnsureDirectoryExists && !Directory.Exists(pluginDirectory))
        {
            Directory.CreateDirectory(pluginDirectory);
        }

        if (!Directory.Exists(pluginDirectory))
        {
            logger?.LogWarning("Plugin directory '{PluginDirectory}' does not exist; no plugins will be loaded.", pluginDirectory);
            return new PluginHostResult(pluginDirectory, Array.Empty<string>(), Array.Empty<PluginAssembly>(), Array.Empty<string>());
        }

        var searchPatterns = BuildSearchPatterns(options, pluginDirectory);
        var discovered = DiscoverPluginFiles(pluginDirectory, searchPatterns, options.RecursiveSearch, logger);
        var orderedFiles = ApplyExplicitOrdering(discovered, options.PluginOrder, out var missingOrderedNames);

        var loaded = new List<PluginAssembly>(orderedFiles.Count);

        lock (Sync)
        {
            foreach (var file in orderedFiles)
            {
                if (LoadedPlugins.TryGetValue(file, out var existing))
                {
                    loaded.Add(existing);
                    continue;
                }

                try
                {
                    var loadContext = new PluginLoadContext(file);
                    var assembly = loadContext.LoadFromAssemblyPath(file);
                    var descriptor = new PluginAssembly(file, assembly, loadContext);
                    LoadedPlugins[file] = descriptor;
                    loaded.Add(descriptor);
                    logger?.LogInformation("Loaded plugin assembly '{Assembly}' from '{Path}'.", assembly.FullName, file);
                }
                catch (Exception ex)
                {
                    logger?.LogError(ex, "Failed to load plugin assembly from '{Path}'.", file);
                }
            }
        }

        var missingOrdered = new ReadOnlyCollection<string>(missingOrderedNames);
        return new PluginHostResult(pluginDirectory, searchPatterns, new ReadOnlyCollection<PluginAssembly>(loaded), missingOrdered);
    }

    private static string ResolvePluginDirectory(PluginHostOptions options, string baseDirectory)
    {
        if (string.IsNullOrWhiteSpace(options.PluginsDirectory))
        {
            return Path.Combine(baseDirectory, "PluginBinaries");
        }

        if (Path.IsPathRooted(options.PluginsDirectory))
        {
            return options.PluginsDirectory;
        }

        return Path.Combine(baseDirectory, options.PluginsDirectory);
    }

    private static IReadOnlyList<string> BuildSearchPatterns(PluginHostOptions options, string pluginDirectory)
    {
        var patterns = new List<string>();
        if (options.SearchPatterns.Count > 0)
        {
            patterns.AddRange(options.SearchPatterns);
        }
        else
        {
            var prefixes = new List<string>();
            if (!string.IsNullOrWhiteSpace(options.PrimaryPrefix))
            {
                prefixes.Add(options.PrimaryPrefix);
            }
            else if (System.Reflection.Assembly.GetEntryAssembly()?.GetName().Name is { } entryName)
            {
                prefixes.Add(entryName);
            }

            prefixes.AddRange(options.AdditionalPrefixes);

            if (prefixes.Count == 0)
            {
                // Fallback to directory name
                prefixes.Add(Path.GetFileName(pluginDirectory));
            }

            foreach (var prefix in prefixes.Where(p => !string.IsNullOrWhiteSpace(p)))
            {
                patterns.Add($"{prefix}.Plugin.*.dll");
            }
        }

        return new ReadOnlyCollection<string>(patterns.Distinct(StringComparer.OrdinalIgnoreCase).ToList());
    }

    private static List<string> DiscoverPluginFiles(
        string pluginDirectory,
        IReadOnlyList<string> searchPatterns,
        bool recurse,
        ILogger? logger)
    {
        var files = new List<string>();
        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        var searchOption = recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;

        foreach (var pattern in searchPatterns)
        {
            try
            {
                foreach (var file in Directory.EnumerateFiles(pluginDirectory, pattern, searchOption))
                {
                    if (IsHiddenPath(file))
                    {
                        continue;
                    }

                    if (seen.Add(file))
                    {
                        files.Add(file);
                    }
                }
            }
            catch (DirectoryNotFoundException)
            {
                // Directory could be removed between the existence check and enumeration.
                logger?.LogDebug("Plugin directory '{PluginDirectory}' disappeared before enumeration.", pluginDirectory);
            }
        }

        return files;
    }

    private static List<string> ApplyExplicitOrdering(
        List<string> discoveredFiles,
        IList<string> pluginOrder,
        out List<string> missingNames)
    {
        if (pluginOrder.Count == 0 || discoveredFiles.Count == 0)
        {
            missingNames = new List<string>();
            discoveredFiles.Sort(StringComparer.OrdinalIgnoreCase);
            return discoveredFiles;
        }

        var configuredSet = new HashSet<string>(pluginOrder, StringComparer.OrdinalIgnoreCase);
        var fileLookup = discoveredFiles.ToDictionary(
            k => Path.GetFileNameWithoutExtension(k),
            StringComparer.OrdinalIgnoreCase);

        var specified = new List<string>();
        foreach (var name in pluginOrder)
        {
            if (fileLookup.TryGetValue(name, out var file))
            {
                specified.Add(file);
            }
        }

        var unspecified = discoveredFiles
            .Where(f => !configuredSet.Contains(Path.GetFileNameWithoutExtension(f)))
            .OrderBy(f => f, StringComparer.OrdinalIgnoreCase)
            .ToList();

        missingNames = pluginOrder
            .Where(name => !fileLookup.ContainsKey(name))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToList();

        specified.AddRange(unspecified);
        return specified;
    }

    private static bool IsHiddenPath(string filePath)
    {
        var directory = Path.GetDirectoryName(filePath);
        while (!string.IsNullOrEmpty(directory))
        {
            var name = Path.GetFileName(directory);
            if (name.StartsWith(".", StringComparison.Ordinal))
            {
                return true;
            }

            directory = Path.GetDirectoryName(directory);
        }

        return false;
    }
}