using System.Runtime.Versioning;
using Microsoft.Win32;

namespace Kompas.Mcp.Core.Interop;

/// <summary>
/// Поиск каталога установленного КОМПАС-3D по реестру COM.
/// <para>
/// Опорная точка — регистрация COM-сервера: <c>HKCR\&lt;ProgID&gt;\CLSID</c> →
/// <c>HKCR\CLSID\{...}\LocalServer32</c>, значение которого содержит путь до исполняемого файла
/// (проверено на v24 Home: <c>"C:\Program Files\ASCON\KOMPAS-3D v24 Home\Bin\kHome.Exe"</c>).
/// Ветка <c>HKLM\SOFTWARE\ASCON\KOMPAS-3D</c> версионная и хранит настройки, а не путь установки,
/// поэтому не используется: <c>LocalServer32</c> заодно гарантирует, что найденная установка —
/// именно та, к которой сервер потом подключится по тому же ProgID.
/// </para>
/// </summary>
public static class KompasInstallLocator
{
    // Тот же порядок и тот же набор, что в KompasSession: .7 годится для поиска установки
    // (нужен только путь), хотя точкой входа при подключении служит API5.
    private static readonly string[] ProgIds =
    {
        "KOMPAS.Application.7",
        "KOMPAS.Application.5",
        "KOMPASLT.Application.5",
    };

    /// <summary>Каталог установки КОМПАС или <c>null</c>, если COM-класс не зарегистрирован.</summary>
    [SupportedOSPlatform("windows")]
    public static string? TryFindInstallDirectory()
    {
        foreach (var progId in ProgIds)
        {
            var root = InstallRootFromServerCommand(TryReadLocalServer(progId));
            if (root is not null && Directory.Exists(root))
                return root;
        }

        return null;
    }

    [SupportedOSPlatform("windows")]
    private static string? TryReadLocalServer(string progId)
    {
        using var progIdKey = Registry.ClassesRoot.OpenSubKey($@"{progId}\CLSID");
        if (progIdKey?.GetValue(null) is not string clsid || clsid.Length == 0)
            return null;

        using var serverKey = Registry.ClassesRoot.OpenSubKey($@"CLSID\{clsid}\LocalServer32");
        return serverKey?.GetValue(null) as string;
    }

    /// <summary>
    /// Выделить путь до exe из значения <c>LocalServer32</c>: путь может быть в кавычках и
    /// сопровождаться аргументами командной строки (<c>...\kHome.Exe /automation</c>).
    /// </summary>
    public static string? ParseServerCommand(string? rawServerCommand)
    {
        if (string.IsNullOrWhiteSpace(rawServerCommand))
            return null;

        var value = rawServerCommand.Trim();
        if (value[0] == '"')
        {
            var closing = value.IndexOf('"', 1);
            return closing > 1 ? value[1..closing] : null;
        }

        // Без кавычек аргументы отделить надёжно нельзя (пробелы бывают и в пути) — режем
        // по расширению исполняемого файла, а не по первому пробелу.
        var extension = value.IndexOf(".exe", StringComparison.OrdinalIgnoreCase);
        return extension >= 0 ? value[..(extension + 4)] : value;
    }

    /// <summary>
    /// Каталог установки по значению <c>LocalServer32</c>: <c>&lt;root&gt;\Bin\kHome.Exe</c> →
    /// <c>&lt;root&gt;</c>. Если exe лежит не в <c>Bin</c>, возвращается его собственный каталог.
    /// </summary>
    /// <remarks>
    /// Разбор пути ручной, а не через <see cref="Path.GetDirectoryName(string)"/>: значение из
    /// реестра всегда Windows-пути, а unit-тесты гоняются в том числе на Linux-раннере, где
    /// <c>\</c> для <see cref="Path"/> — обычный символ.
    /// </remarks>
    public static string? InstallRootFromServerCommand(string? rawServerCommand)
    {
        var executable = ParseServerCommand(rawServerCommand);
        if (executable is null)
            return null;

        var binDirectory = TrimLastSegment(executable);
        if (binDirectory is null)
            return null;

        if (!string.Equals(LastSegment(binDirectory), "Bin", StringComparison.OrdinalIgnoreCase))
            return binDirectory;

        return TrimLastSegment(binDirectory) ?? binDirectory;
    }

    private static readonly char[] Separators = { '\\', '/' };

    private static string? TrimLastSegment(string path)
    {
        var trimmed = path.TrimEnd(Separators);
        var separator = trimmed.LastIndexOfAny(Separators);
        return separator > 0 ? trimmed[..separator] : null;
    }

    private static string LastSegment(string path)
    {
        var trimmed = path.TrimEnd(Separators);
        var separator = trimmed.LastIndexOfAny(Separators);
        return separator >= 0 ? trimmed[(separator + 1)..] : trimmed;
    }
}
