using System.ComponentModel;
using System.Reflection;
using System.Text.Json.Serialization;

namespace MiGu.Server.Signal;

/// <summary>
/// 从 Simple3 <c>plugins/StandardScene.Signal.dll</c> 反射数据中心表和列。
/// 平台不编译引用该插件工程，避免仓外路径和 Windows TFM 耦合。
/// </summary>
public static class SignalModelSchemaResolver
{
    private const string ModelNamespace = "StandardScene.Signal.Model";
    private const string AssemblyFileName = "StandardScene.Signal.dll";

    public static string? FindAssemblyPath(string? pluginsDir)
    {
        if (string.IsNullOrWhiteSpace(pluginsDir))
            return null;
        var path = Path.Combine(pluginsDir, AssemblyFileName);
        return File.Exists(path) ? path : null;
    }

    public static IReadOnlyList<SignalTableDef> ResolveTables(string? pluginsDir, string? workingDirectory = null)
    {
        return WithSignalAssembly(pluginsDir, workingDirectory, assembly =>
        {
            if (assembly == null)
                return Array.Empty<SignalTableDef>();
            try
            {
                return BuildTablesFromAssembly(assembly);
            }
            catch
            {
                return Array.Empty<SignalTableDef>();
            }
        });
    }

    public static IReadOnlyList<SignalColumn> ResolveColumns(string? modelName, string? pluginsDir, string? workingDirectory = null)
    {
        if (string.IsNullOrWhiteSpace(modelName))
            return Array.Empty<SignalColumn>();

        return WithSignalAssembly(pluginsDir, workingDirectory, assembly =>
        {
            if (assembly == null)
                return Array.Empty<SignalColumn>();
            try
            {
                var type = assembly.GetType($"{ModelNamespace}.{modelName.Trim()}", throwOnError: false, ignoreCase: true);
                if (type == null)
                    return Array.Empty<SignalColumn>();
                return DiscoverColumns(type);
            }
            catch
            {
                return Array.Empty<SignalColumn>();
            }
        });
    }

    private static T WithSignalAssembly<T>(string? pluginsDir, string? workingDirectory, Func<Assembly?, T> use)
    {
        var assemblyPath = FindAssemblyPath(pluginsDir);
        if (assemblyPath == null)
            return use(null);

        var expected = Path.GetFullPath(assemblyPath);
        var cached = FindLoadedFrom(expected);
        if (cached != null)
            return use(cached);

        var probeDirs = BuildProbeDirs(pluginsDir, workingDirectory);
        ResolveEventHandler handler = (_, args) => ResolveAssembly(args.Name, probeDirs);
        AppDomain.CurrentDomain.AssemblyResolve += handler;
        try
        {
            var loaded = Assembly.LoadFrom(expected);
            return use(HasModelTypes(loaded) ? loaded : null);
        }
        catch
        {
            return use(null);
        }
        finally
        {
            AppDomain.CurrentDomain.AssemblyResolve -= handler;
        }
    }

    private static Assembly? FindLoadedFrom(string expectedFullPath)
    {
        try
        {
            foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
            {
                if (!string.Equals(assembly.GetName().Name, "StandardScene.Signal", StringComparison.OrdinalIgnoreCase))
                    continue;
                if (string.IsNullOrWhiteSpace(assembly.Location))
                    continue;
                if (!string.Equals(Path.GetFullPath(assembly.Location), expectedFullPath, StringComparison.OrdinalIgnoreCase))
                    continue;
                return HasModelTypes(assembly) ? assembly : null;
            }
        }
        catch
        {
            /* 继续 LoadFrom */
        }

        return null;
    }

    private static bool HasModelTypes(Assembly assembly)
    {
        return SafeGetTypes(assembly).Any(t =>
            t != null && string.Equals(t.Namespace, ModelNamespace, StringComparison.Ordinal));
    }

    private static IReadOnlyList<string> BuildProbeDirs(string? pluginsDir, string? workingDirectory)
    {
        var dirs = new List<string>();
        void Add(string? dir)
        {
            if (string.IsNullOrWhiteSpace(dir)) return;
            var full = Path.GetFullPath(dir);
            if (Directory.Exists(full) && !dirs.Contains(full, StringComparer.OrdinalIgnoreCase))
                dirs.Add(full);
        }

        Add(pluginsDir);
        Add(workingDirectory);
        if (!string.IsNullOrWhiteSpace(pluginsDir))
            Add(Path.GetDirectoryName(pluginsDir));

        return dirs;
    }

    private static Assembly? ResolveAssembly(string? assemblyName, IReadOnlyList<string> probeDirs)
    {
        if (string.IsNullOrWhiteSpace(assemblyName))
            return null;

        string simpleName;
        try
        {
            simpleName = new AssemblyName(assemblyName).Name ?? assemblyName;
        }
        catch
        {
            simpleName = assemblyName.Split(',')[0];
        }

        foreach (var dir in probeDirs)
        {
            var path = Path.Combine(dir, simpleName + ".dll");
            if (!File.Exists(path))
                continue;
            try
            {
                return Assembly.LoadFrom(path);
            }
            catch
            {
                /* try next dir */
            }
        }

        return null;
    }

    private static IReadOnlyList<SignalTableDef> BuildTablesFromAssembly(Assembly assembly)
    {
        var found = new List<(int Order, SignalTableDef Table)>();
        foreach (var type in SafeGetTypes(assembly))
        {
            if (type == null || !string.Equals(type.Namespace, ModelNamespace, StringComparison.Ordinal))
                continue;

            var attr = type.GetCustomAttributes(inherit: false)
                .FirstOrDefault(a => a.GetType().Name == "SignalTableAttribute");
            if (attr == null)
                continue;

            var attrType = attr.GetType();
            var id = (attrType.GetProperty("Id")?.GetValue(attr) as string ?? "").Trim();
            var fileName = (attrType.GetProperty("FileName")?.GetValue(attr) as string ?? "").Trim();
            var title = (attrType.GetProperty("Title")?.GetValue(attr) as string ?? "").Trim();
            var order = attrType.GetProperty("Order")?.GetValue(attr) as int? ?? 0;
            if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(fileName))
                continue;

            var category = type.GetCustomAttribute<CategoryAttribute>()?.Category ?? "";
            if (string.IsNullOrWhiteSpace(title))
                title = type.GetCustomAttribute<DisplayNameAttribute>()?.DisplayName ?? id;

            found.Add((order, new SignalTableDef(id, title, category, fileName, DiscoverColumns(type))));
        }

        return found.OrderBy(x => x.Order).ThenBy(x => x.Table.Id).Select(x => x.Table).ToList();
    }

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

    private static IReadOnlyList<SignalColumn> DiscoverColumns(Type type)
    {
        var list = new List<SignalColumn>();
        foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
                     .Where(p => p.CanRead && p.CanWrite && p.GetIndexParameters().Length == 0)
                     .Where(p => p.GetCustomAttribute<BrowsableAttribute>()?.Browsable != false)
                     .Where(p => !IsJsonIgnored(p))
                     .OrderBy(p => p.MetadataToken))
        {
            var label = prop.GetCustomAttribute<DisplayNameAttribute>()?.DisplayName;
            if (string.IsNullOrWhiteSpace(label))
                label = prop.Name;

            var columnType = MapType(prop.PropertyType);
            string[]? options = null;
            var group = prop.GetCustomAttribute<CategoryAttribute>(inherit: false)?.Category;
            if (string.IsNullOrWhiteSpace(group))
                group = null;

            var select = ReadSelectOptions(prop);
            if (select is { Length: > 0 })
            {
                columnType = "enum";
                options = select;
            }
            else if (columnType == "enum" && (Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType).IsEnum)
            {
                options = Enum.GetNames(Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType);
            }

            list.Add(new SignalColumn(prop.Name, label, columnType, options, group));
        }

        return list;
    }

    private static bool IsJsonIgnored(PropertyInfo prop)
    {
        if (prop.GetCustomAttribute<JsonIgnoreAttribute>() != null)
            return true;

        foreach (var attr in prop.GetCustomAttributes(inherit: true))
        {
            if (attr.GetType().FullName == "Newtonsoft.Json.JsonIgnoreAttribute")
                return true;
        }

        return false;
    }

    private static string[]? ReadSelectOptions(PropertyInfo prop)
    {
        var attr = prop.GetCustomAttributes(inherit: false)
            .FirstOrDefault(a => a.GetType().Name == "SignalSelectAttribute");
        if (attr == null)
            return null;
        var options = attr.GetType().GetProperty("Options")?.GetValue(attr) as string[];
        return options is { Length: > 0 } ? options : null;
    }

    private static string MapType(Type type)
    {
        var underlying = Nullable.GetUnderlyingType(type) ?? type;
        if (underlying == typeof(bool))
            return "bool";
        if (underlying == typeof(int) || underlying == typeof(long) || underlying == typeof(short) ||
            underlying == typeof(byte) || underlying == typeof(uint) || underlying == typeof(ulong))
            return "int";
        if (underlying.IsEnum)
            return "enum";
        return "string";
    }
}
