using System.Diagnostics;
using System.Drawing;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using YMhut.Box.Core.App;
using YMhut.Box.Core.Logging;

namespace YMhut.Box.Core.Tools;

public interface IExternalToolCatalogService
{
    Task<IReadOnlyList<ExternalToolModule>> GetModulesAsync(CancellationToken cancellationToken = default);

    string ResolveToolsRoot();
}

public sealed class ExternalToolCatalogService(AppPaths paths, ILogService? logService = null) : IExternalToolCatalogService
{
    private static readonly string[] LaunchableExtensions = [".exe", ".bat", ".cmd", ".lnk", ".msc", ".ps1", ".vbs"];
    private static readonly string[] ArchSuffixes = ["64", "32", "x64", "x86", "_x64", "_x86", "_64", "_32", "w64", "w32", "_Win64", "_Win32", "ARM64", "_ARM64", "_arm64"];
    private static readonly string[] X64Patterns = ["x64", "_x64", "w64", "_Win64"];
    private static readonly string[] X86Patterns = ["x86", "_x86", "32", "_32", "w32", "_Win32"];
    private static readonly string[] Arm64Patterns = ["ARM64", "_ARM64", "arm64", "_arm64"];
    private readonly SemaphoreSlim _gate = new(1, 1);
    private IReadOnlyList<ExternalToolModule>? _cached;

    public async Task<IReadOnlyList<ExternalToolModule>> GetModulesAsync(CancellationToken cancellationToken = default)
    {
        if (_cached is not null)
        {
            return _cached;
        }

        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            if (_cached is not null)
            {
                return _cached;
            }

            var start = Stopwatch.StartNew();
            var root = ResolveToolsRoot();
            if (!Directory.Exists(root))
            {
                await WriteLogAsync("Warning", "tool-scan", "External Tools directory not found", root, cancellationToken).ConfigureAwait(false);
                _cached = [];
                return _cached;
            }

            var metadata = ToolMetadataDatabase.Load(ResolveMetadataRoot(root), root, logService);
            var modules = Directory.EnumerateDirectories(root)
                .SelectMany(categoryRoot => ScanCategory(root, categoryRoot, metadata, cancellationToken))
                .GroupBy(module => module.Id, StringComparer.OrdinalIgnoreCase)
                .Select(group => group.First())
                .OrderBy(module => CategorySortIndex(module.Metadata.Category))
                .ThenBy(module => module.Tool.CategoryName, StringComparer.CurrentCultureIgnoreCase)
                .ThenBy(module => module.Tool.Name, StringComparer.CurrentCultureIgnoreCase)
                .ToArray();

            _cached = modules;
            start.Stop();
            await WriteLogAsync("Information", "tool-scan", "External Tools scanned", $"root={root}; count={modules.Length}; elapsedMs={start.ElapsedMilliseconds}", cancellationToken).ConfigureAwait(false);
            return _cached;
        }
        catch (Exception exception)
        {
            await WriteLogAsync("Error", "tool-scan", "External Tools scan failed", Sanitize(exception.Message), cancellationToken).ConfigureAwait(false);
            _cached = [];
            return _cached;
        }
        finally
        {
            _gate.Release();
        }
    }

    public string ResolveToolsRoot()
    {
        foreach (var candidate in CandidateRoots("Tools"))
        {
            if (Directory.Exists(candidate))
            {
                return candidate;
            }
        }

        return Path.Combine(AppContext.BaseDirectory, "Tools");
    }

    private IEnumerable<ExternalToolModule> ScanCategory(string toolsRoot, string categoryRoot, ToolMetadataDatabase metadata, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        var categoryName = Path.GetFileName(categoryRoot);
        if (string.IsNullOrWhiteSpace(categoryName))
        {
            yield break;
        }

        var toolDirs = Directory.EnumerateDirectories(categoryRoot).ToList();
        foreach (var toolDir in MergeArchDirectories(toolDirs))
        {
            cancellationToken.ThrowIfCancellationRequested();
            ExternalToolModule? module = null;
            try
            {
                module = CreateModule(toolsRoot, categoryName, categoryRoot, toolDir, metadata);
            }
            catch (Exception exception)
            {
                _ = WriteLogAsync("Warning", "tool-scan", "External tool skipped", $"{toolDir}: {Sanitize(exception.Message)}", CancellationToken.None);
            }

            if (module is not null)
            {
                yield return module;
            }
        }
    }

    private ExternalToolModule? CreateModule(string toolsRoot, string categoryName, string categoryRoot, string toolDir, ToolMetadataDatabase metadata)
    {
        var launchable = FindPrimaryLaunchable(toolDir, metadata.GetLaunchTarget(toolsRoot, toolDir));
        if (launchable is null)
        {
            return null;
        }

        var relative = Path.GetRelativePath(toolsRoot, launchable);
        var extension = Path.GetExtension(launchable).TrimStart('.').ToUpperInvariant();
        var info = metadata.GetMetadata(toolsRoot, launchable, toolDir);
        var rawName = GetDisplayName(launchable);
        var name = CleanupName(StripArchSuffix(rawName));
        if (string.IsNullOrWhiteSpace(name))
        {
            name = CleanupName(rawName);
        }

        var arch = FormatArchDisplay(DetectArch(Path.GetFileNameWithoutExtension(launchable)));
        var variants = FindAllArchVariants(categoryRoot, toolDir, launchable, metadata);
        var risk = DetermineRisk(launchable);
        var description = FirstUseful(
            info.Description,
            ReadFolderDescription(toolDir),
            $"{categoryName} 外部工具，来自 YMhut Box 随包 Tools 目录。")
            ?? $"{categoryName} 外部工具，来自 YMhut Box 随包 Tools 目录。";
        var item = new ExternalToolItem(
            ExternalToolModule.IdPrefix + Hash(relative),
            name,
            description,
            categoryName,
            MapCategory(categoryName),
            toolDir,
            launchable,
            relative,
            string.IsNullOrWhiteSpace(extension) ? "FILE" : extension,
            info.Publisher,
            info.Version,
            GetIconGlyph(launchable),
            GetCachedOrExtractIcon(launchable),
            string.IsNullOrWhiteSpace(arch) ? null : arch,
            (info.Tags ?? []).Concat([categoryName, extension]).Where(tag => !string.IsNullOrWhiteSpace(tag)).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(),
            variants,
            risk,
            risk == ReferenceToolRiskLevel.High || LooksAdministrative(launchable));

        return new ExternalToolModule(item);
    }

    private List<ExternalToolVariant> FindAllArchVariants(string categoryRoot, string toolDir, string primaryPath, ToolMetadataDatabase metadata)
    {
        var variants = new List<ExternalToolVariant>();
        var allLaunchables = Directory.EnumerateFiles(toolDir, "*", SearchOption.AllDirectories)
            .Where(IsLaunchable)
            .ToArray();
        foreach (var file in allLaunchables)
        {
            if (string.Equals(file, primaryPath, StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }

            var arch = FormatArchDisplay(DetectArch(Path.GetFileNameWithoutExtension(file)));
            if (string.IsNullOrWhiteSpace(arch))
            {
                continue;
            }

            variants.Add(new ExternalToolVariant(CleanupName(StripArchSuffix(Path.GetFileNameWithoutExtension(file))), file, arch));
        }

        foreach (var variant in metadata.GetArchVariants(toolDir))
        {
            var variantPath = ResolveVariantPath(categoryRoot, toolDir, variant);
            if (variantPath is null ||
                string.Equals(variantPath, primaryPath, StringComparison.OrdinalIgnoreCase) ||
                variants.Any(existing => string.Equals(existing.Path, variantPath, StringComparison.OrdinalIgnoreCase)))
            {
                continue;
            }

            variants.Add(new ExternalToolVariant(
                CleanupName(StripArchSuffix(Path.GetFileNameWithoutExtension(variantPath))),
                variantPath,
                variant.Arch));
        }

        return variants
            .OrderBy(variant => variant.Architecture, StringComparer.OrdinalIgnoreCase)
            .ThenBy(variant => variant.Name, StringComparer.CurrentCultureIgnoreCase)
            .ToList();
    }

    private static string? ResolveVariantPath(string categoryRoot, string toolDir, JsonArchVariantResult variant)
    {
        if (!string.IsNullOrWhiteSpace(variant.File))
        {
            var candidate = Path.Combine(toolDir, variant.File);
            if (File.Exists(candidate) && IsLaunchable(candidate))
            {
                return candidate;
            }
        }

        if (!string.IsNullOrWhiteSpace(variant.Dir))
        {
            var candidateDir = Path.Combine(categoryRoot, variant.Dir);
            if (Directory.Exists(candidateDir))
            {
                return FindPrimaryLaunchable(candidateDir, null);
            }
        }

        return null;
    }

    private static string? FindPrimaryLaunchable(string toolDir, string? launchTarget)
    {
        if (!string.IsNullOrWhiteSpace(launchTarget))
        {
            var directTarget = Path.Combine(toolDir, launchTarget);
            if (File.Exists(directTarget) && IsLaunchable(directTarget))
            {
                return directTarget;
            }

            var deepTarget = Directory.EnumerateFiles(toolDir, launchTarget, SearchOption.AllDirectories)
                .FirstOrDefault(IsLaunchable);
            if (deepTarget is not null)
            {
                return deepTarget;
            }
        }

        var allLaunchables = Directory.EnumerateFiles(toolDir, "*", SearchOption.AllDirectories)
            .Where(IsLaunchable)
            .ToList();
        if (allLaunchables.Count == 0)
        {
            return null;
        }

        if (allLaunchables.Count == 1)
        {
            return allLaunchables[0];
        }

        var dirName = Path.GetFileName(toolDir);
        var directLaunchables = Directory.EnumerateFiles(toolDir).Where(IsLaunchable).ToList();
        var exact = directLaunchables.FirstOrDefault(file =>
            Path.GetFileNameWithoutExtension(file).Equals(dirName, StringComparison.OrdinalIgnoreCase));
        if (exact is not null)
        {
            return exact;
        }

        var directArch = directLaunchables
            .Where(file => StripArchSuffix(Path.GetFileNameWithoutExtension(file)).Equals(StripArchSuffix(dirName), StringComparison.OrdinalIgnoreCase))
            .ToList();
        if (directArch.Count > 0)
        {
            return PickPreferredArch(directArch);
        }

        var deepArch = allLaunchables
            .Where(file => StripArchSuffix(Path.GetFileNameWithoutExtension(file)).Equals(StripArchSuffix(dirName), StringComparison.OrdinalIgnoreCase))
            .ToList();
        if (deepArch.Count > 0)
        {
            return PickPreferredArch(deepArch);
        }

        return directLaunchables.FirstOrDefault() ?? allLaunchables[0];
    }

    private static List<string> MergeArchDirectories(IReadOnlyList<string> toolDirs)
    {
        var consumed = new HashSet<int>();
        var result = new List<string>();
        for (var i = 0; i < toolDirs.Count; i++)
        {
            if (consumed.Contains(i))
            {
                continue;
            }

            var stripped = StripArchSuffix(Path.GetFileName(toolDirs[i]));
            result.Add(toolDirs[i]);
            for (var j = i + 1; j < toolDirs.Count; j++)
            {
                if (StripArchSuffix(Path.GetFileName(toolDirs[j])).Equals(stripped, StringComparison.OrdinalIgnoreCase))
                {
                    consumed.Add(j);
                }
            }
        }

        return result;
    }

    private static string PickPreferredArch(IReadOnlyList<string> candidates)
    {
        if (global::System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == global::System.Runtime.InteropServices.Architecture.Arm64)
        {
            var arm64 = candidates.FirstOrDefault(path => Arm64Patterns.Any(pattern => Path.GetFileNameWithoutExtension(path).EndsWith(pattern, StringComparison.OrdinalIgnoreCase)));
            if (arm64 is not null)
            {
                return arm64;
            }
        }

        if (Environment.Is64BitOperatingSystem)
        {
            var x64 = candidates.FirstOrDefault(path => X64Patterns.Any(pattern => Path.GetFileNameWithoutExtension(path).EndsWith(pattern, StringComparison.OrdinalIgnoreCase)));
            if (x64 is not null)
            {
                return x64;
            }
        }

        var x86 = candidates.FirstOrDefault(path => X86Patterns.Any(pattern => Path.GetFileNameWithoutExtension(path).EndsWith(pattern, StringComparison.OrdinalIgnoreCase)));
        return x86 ?? candidates[0];
    }

    private static ReferenceToolRiskLevel DetermineRisk(string path)
    {
        var extension = Path.GetExtension(path).ToLowerInvariant();
        if (extension is ".bat" or ".cmd" or ".ps1" or ".vbs" or ".msc")
        {
            return ReferenceToolRiskLevel.High;
        }

        return extension == ".exe" ? ReferenceToolRiskLevel.Medium : ReferenceToolRiskLevel.Low;
    }

    private static bool LooksAdministrative(string path)
    {
        var text = $"{Path.GetFileNameWithoutExtension(path)} {Path.GetDirectoryName(path)}";
        return text.Contains("defender", StringComparison.OrdinalIgnoreCase) ||
            text.Contains("registry", StringComparison.OrdinalIgnoreCase) ||
            text.Contains("driver", StringComparison.OrdinalIgnoreCase) ||
            text.Contains("clean", StringComparison.OrdinalIgnoreCase) ||
            text.Contains("admin", StringComparison.OrdinalIgnoreCase);
    }

    private string? GetCachedOrExtractIcon(string toolPath)
    {
        var extension = Path.GetExtension(toolPath);
        if (!OperatingSystem.IsWindowsVersionAtLeast(6, 1) ||
            (!extension.Equals(".exe", StringComparison.OrdinalIgnoreCase) && !extension.Equals(".lnk", StringComparison.OrdinalIgnoreCase)) ||
            !File.Exists(toolPath))
        {
            return null;
        }

        var cacheRoot = Path.Combine(paths.Cache, "tool-icons");
        Directory.CreateDirectory(cacheRoot);
        var iconPath = Path.Combine(cacheRoot, $"{Hash(toolPath)}.png");
        if (File.Exists(iconPath) && DateTime.UtcNow - File.GetLastWriteTimeUtc(iconPath) < TimeSpan.FromDays(90))
        {
            return iconPath;
        }

        try
        {
            return ExtractIconToCache(toolPath, iconPath);
        }
        catch (Exception exception)
        {
            _ = WriteLogAsync("Warning", "icon", "External tool icon extraction failed", $"{Path.GetFileName(toolPath)}: {Sanitize(exception.Message)}", CancellationToken.None);
            return null;
        }
    }

    [SupportedOSPlatform("windows6.1")]
    private string? ExtractIconToCache(string toolPath, string iconPath)
    {
        using var icon = Icon.ExtractAssociatedIcon(toolPath);
        if (icon is null)
        {
            return null;
        }

        using var bitmap = icon.ToBitmap();
        bitmap.Save(iconPath, global::System.Drawing.Imaging.ImageFormat.Png);
        _ = WriteLogAsync("Information", "icon", "External tool icon extracted", Path.GetFileName(toolPath), CancellationToken.None);
        return iconPath;
    }

    private static string? GetIconGlyph(string toolPath)
    {
        return Path.GetExtension(toolPath).ToLowerInvariant() switch
        {
            ".bat" or ".cmd" => "\uE756",
            ".ps1" or ".vbs" => "\uE943",
            ".msc" => "\uEC7A",
            ".lnk" => "\uE8A7",
            ".exe" => null,
            _ => "\uE8B7"
        };
    }

    private static ToolCategory MapCategory(string categoryName)
    {
        if (categoryName.Contains("综合", StringComparison.OrdinalIgnoreCase) ||
            categoryName.Contains("其他", StringComparison.OrdinalIgnoreCase))
        {
            return ToolCategory.System;
        }

        if (categoryName.Contains("烤鸡", StringComparison.OrdinalIgnoreCase))
        {
            return ToolCategory.System;
        }

        if (categoryName.Contains("处理器", StringComparison.OrdinalIgnoreCase) ||
            categoryName.Contains("显卡", StringComparison.OrdinalIgnoreCase) ||
            categoryName.Contains("硬盘", StringComparison.OrdinalIgnoreCase) ||
            categoryName.Contains("内存", StringComparison.OrdinalIgnoreCase) ||
            categoryName.Contains("显示器", StringComparison.OrdinalIgnoreCase) ||
            categoryName.Contains("外设", StringComparison.OrdinalIgnoreCase))
        {
            return ToolCategory.System;
        }

        return ToolCategory.Dev;
    }

    private IEnumerable<string> CandidateRoots(string folderName)
    {
        foreach (var root in InstallLayoutPaths.CandidateRoots())
        {
            yield return Path.Combine(root, folderName);

            var directory = new DirectoryInfo(root);
            while (directory is not null)
            {
                yield return Path.Combine(directory.FullName, folderName);
                yield return Path.Combine(directory.FullName, "tubatool-参考补充项目", folderName);
                directory = directory.Parent;
            }
        }
    }

    private string ResolveMetadataRoot(string toolsRoot)
    {
        foreach (var candidate in CandidateRoots("Metadata"))
        {
            if (Directory.Exists(candidate))
            {
                return candidate;
            }
        }

        var parent = Directory.GetParent(toolsRoot)?.Parent?.FullName;
        return parent is null ? Path.Combine(AppContext.BaseDirectory, "Metadata") : Path.Combine(parent, "Metadata");
    }

    private static bool IsLaunchable(string path) => LaunchableExtensions.Contains(Path.GetExtension(path), StringComparer.OrdinalIgnoreCase);

    private static string GetDisplayName(string path)
    {
        var name = Path.GetFileNameWithoutExtension(path);
        return name.Equals("start", StringComparison.OrdinalIgnoreCase)
            ? Directory.GetParent(path)?.Name ?? name
            : name;
    }

    private static string CleanupName(string name)
    {
        return name
            .Replace("_x64", " x64", StringComparison.OrdinalIgnoreCase)
            .Replace("_x86", " x86", StringComparison.OrdinalIgnoreCase)
            .Replace("_ARM64", " ARM64", StringComparison.OrdinalIgnoreCase)
            .Replace("_arm64", " ARM64", StringComparison.OrdinalIgnoreCase)
            .Replace("_", " ", StringComparison.Ordinal)
            .Trim();
    }

    private static string StripArchSuffix(string name)
    {
        foreach (var suffix in ArchSuffixes)
        {
            if (name.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
            {
                return name[..^suffix.Length].TrimEnd('_', '-', ' ');
            }
        }

        return name;
    }

    private static string? DetectArch(string name)
    {
        if (Arm64Patterns.Any(pattern => name.EndsWith(pattern, StringComparison.OrdinalIgnoreCase)))
        {
            return "ARM64";
        }

        if (X64Patterns.Any(pattern => name.EndsWith(pattern, StringComparison.OrdinalIgnoreCase)))
        {
            return "x64";
        }

        if (X86Patterns.Any(pattern => name.EndsWith(pattern, StringComparison.OrdinalIgnoreCase)))
        {
            return "x86";
        }

        return null;
    }

    private static string FormatArchDisplay(string? arch)
    {
        return arch switch
        {
            "ARM64" => "ARM64",
            "x64" or "Win64" => "x64",
            "x86" or "Win32" => "x86",
            _ => arch ?? string.Empty
        };
    }

    private static string? ReadFolderDescription(string toolDir)
    {
        try
        {
            var readme = Directory.EnumerateFiles(toolDir, "*.*", SearchOption.TopDirectoryOnly)
                .FirstOrDefault(path =>
                    Path.GetFileName(path).Contains("readme", StringComparison.OrdinalIgnoreCase) ||
                    Path.GetFileName(path).Contains("说明", StringComparison.CurrentCultureIgnoreCase));
            return readme is null
                ? null
                : File.ReadLines(readme).FirstOrDefault(line => !string.IsNullOrWhiteSpace(line))?.Trim() is { } line
                    ? line.Length > 180 ? line[..180] : line
                    : null;
        }
        catch
        {
            return null;
        }
    }

    private static string? FirstUseful(params string?[] values)
        => values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value))?.Trim();

    private static string Hash(string value)
    {
        var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(value));
        return Convert.ToHexString(bytes)[..16].ToLowerInvariant();
    }

    private static int CategorySortIndex(ToolCategory category)
    {
        return category switch
        {
            ToolCategory.Plugin => 1,
            ToolCategory.Dev => 2,
            ToolCategory.Network => 3,
            ToolCategory.Security => 4,
            ToolCategory.Data => 5,
            ToolCategory.Calculator => 6,
            ToolCategory.Text => 7,
            ToolCategory.Image => 8,
            ToolCategory.Design => 9,
            ToolCategory.Life => 10,
            ToolCategory.System => 11,
            _ => 0
        };
    }

    private static string Sanitize(string message)
        => message.Length > 240 ? message[..240] : message;

    private Task WriteLogAsync(string level, string category, string message, string? detail, CancellationToken cancellationToken)
    {
        return logService?.WriteAsync(level, category, message, detail, cancellationToken) ?? Task.CompletedTask;
    }

    private sealed record ToolInfo(
        string? Description,
        string? Publisher,
        string? Version,
        IReadOnlyList<string>? Tags);

    private sealed record JsonArchVariantResult(string? File, string? Dir, string? Arch);

    private sealed class ToolMetadataDatabase
    {
        private readonly IReadOnlyList<JsonToolMetadata> _items;

        private ToolMetadataDatabase(IReadOnlyList<JsonToolMetadata> items)
        {
            _items = items;
        }

        public static ToolMetadataDatabase Load(string metadataRoot, string toolsRoot, ILogService? logService)
        {
            var path = Path.Combine(metadataRoot, "tools.json");
            if (!File.Exists(path))
            {
                return new ToolMetadataDatabase([]);
            }

            try
            {
                using var stream = File.OpenRead(path);
                var database = JsonSerializer.Deserialize<JsonToolDatabase>(stream, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
                return new ToolMetadataDatabase(database?.Tools ?? []);
            }
            catch (Exception exception)
            {
                _ = logService?.WriteAsync("Warning", "tool-metadata", "External tool metadata load failed", $"{path}: {exception.Message}");
                return new ToolMetadataDatabase([]);
            }
        }

        public ToolInfo GetMetadata(string toolsRoot, string launchPath, string toolDir)
        {
            FileVersionInfo? versionInfo = null;
            try
            {
                if (File.Exists(launchPath))
                {
                    versionInfo = FileVersionInfo.GetVersionInfo(launchPath);
                }
            }
            catch
            {
            }

            var json = FindJsonMetadata(toolsRoot, launchPath, toolDir);
            return new ToolInfo(
                FirstUseful(json?.Description, versionInfo?.FileDescription, versionInfo?.ProductName),
                FirstUseful(json?.Publisher, versionInfo?.CompanyName, versionInfo?.LegalCopyright),
                FirstUseful(versionInfo?.ProductVersion, versionInfo?.FileVersion),
                json?.Tags);
        }

        public string? GetLaunchTarget(string toolsRoot, string toolDir)
            => FindJsonMetadataByDir(toolsRoot, toolDir)?.LaunchTarget;

        public IReadOnlyList<JsonArchVariantResult> GetArchVariants(string toolDir)
        {
            var json = FindJsonMetadataByDir(string.Empty, toolDir);
            return json?.ArchVariants?
                .Select(variant => new JsonArchVariantResult(variant.File, variant.Dir, variant.Arch))
                .ToArray() ?? [];
        }

        private JsonToolMetadata? FindJsonMetadata(string toolsRoot, string launchPath, string toolDir)
        {
            var fileName = Path.GetFileNameWithoutExtension(launchPath);
            var relative = SafeRelative(toolsRoot, launchPath);
            var dirName = Path.GetFileName(toolDir);
            return _items
                .Where(item => !string.IsNullOrWhiteSpace(item.Match) &&
                    (fileName.Contains(item.Match, StringComparison.CurrentCultureIgnoreCase) ||
                     relative.Contains(item.Match, StringComparison.CurrentCultureIgnoreCase) ||
                     MatchesFlexible(dirName, item.Match)))
                .OrderByDescending(item => item.Match!.Length)
                .FirstOrDefault();
        }

        private JsonToolMetadata? FindJsonMetadataByDir(string toolsRoot, string toolDir)
        {
            var dirName = Path.GetFileName(toolDir);
            var relative = SafeRelative(toolsRoot, toolDir);
            return _items
                .Where(item => !string.IsNullOrWhiteSpace(item.Match) &&
                    (relative.Contains(item.Match, StringComparison.CurrentCultureIgnoreCase) ||
                     MatchesFlexible(dirName, item.Match)))
                .OrderByDescending(item => item.Match!.Length)
                .FirstOrDefault();
        }

        private static string SafeRelative(string root, string path)
        {
            try
            {
                return string.IsNullOrWhiteSpace(root) ? path : Path.GetRelativePath(root, path);
            }
            catch
            {
                return path;
            }
        }

        private static bool MatchesFlexible(string? source, string match)
        {
            if (string.IsNullOrWhiteSpace(source))
            {
                return false;
            }

            if (source.Contains(match, StringComparison.CurrentCultureIgnoreCase))
            {
                return true;
            }

            var normalizedSource = source.Replace(" ", "", StringComparison.Ordinal).Replace("-", "", StringComparison.Ordinal).Replace("_", "", StringComparison.Ordinal);
            var normalizedMatch = match.Replace(" ", "", StringComparison.Ordinal).Replace("-", "", StringComparison.Ordinal).Replace("_", "", StringComparison.Ordinal);
            return normalizedSource.Contains(normalizedMatch, StringComparison.CurrentCultureIgnoreCase);
        }

        private sealed class JsonToolDatabase
        {
            public List<JsonToolMetadata> Tools { get; set; } = [];
        }

        private sealed class JsonToolMetadata
        {
            public string? Match { get; set; }

            public string? Description { get; set; }

            public string? Publisher { get; set; }

            public string? LaunchTarget { get; set; }

            public List<string>? Tags { get; set; }

            public List<JsonArchVariant>? ArchVariants { get; set; }
        }

        private sealed class JsonArchVariant
        {
            public string? File { get; set; }

            public string? Dir { get; set; }

            public string? Arch { get; set; }
        }
    }
}
