using System.Globalization;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text;

namespace Kompas.Mcp.Core.Interop;

/// <summary>
/// Поиск каталога с интероп-сборками КОМПАС, которые сервер не поставляет вместе с собой.
/// <para>
/// Сборки (<c>KompasAPI7</c>, <c>Kompas6API5</c>, <c>Kompas6Constants</c>, <c>Kompas6Constants3D</c>)
/// принадлежат АСКОН и распространяются в составе КОМПАС-3D, поэтому в релизный архив не входят —
/// они берутся из установленного экземпляра. В установке отдельных DLL нет: они лежат внутри
/// <c>SDK\Samples\CSharp.zip</c> в каталоге <c>Common/</c>, поэтому при первом обращении архив
/// распаковывается в кеш <c>%LOCALAPPDATA%\kompas-mcp\interop\&lt;ключ&gt;</c>.
/// </para>
/// </summary>
public sealed class KompasInteropProvider
{
    /// <summary>Переменная окружения с готовым каталогом сборок (обход поиска установки).</summary>
    public const string OverrideVariable = "KOMPAS_INTEROP_DIR";

    private const string ZipEntryFolder = "Common/";

    /// <summary>Имена сборок, которые сервер отдаёт из установки КОМПАС.</summary>
    /// <remarks>
    /// Компилируется код только против первых четырёх; остальные перечислены потому, что могут
    /// понадобиться как зависимости (<c>Kompas6API3D5COM</c> ссылается на <c>Kompas6API2D5COM</c>).
    /// </remarks>
    public static readonly IReadOnlyList<string> AssemblyNames = new[]
    {
        "KompasAPI7",
        "Kompas6API5",
        "Kompas6Constants",
        "Kompas6Constants3D",
        "Kompas6API2D5COM",
        "Kompas6API3D5COM",
        "KAPITypes",
    };

    // Каталог считается пригодным, только если в нём есть все сборки, против которых собран Core.
    private static readonly string[] RequiredFiles =
    {
        "KompasAPI7.dll",
        "Kompas6API5.dll",
        "Kompas6Constants.dll",
        "Kompas6Constants3D.dll",
    };

    private readonly string? _overrideDirectory;
    private readonly string _baseDirectory;
    private readonly string _cacheRoot;
    private readonly Func<string?> _installDirectory;
    private readonly object _gate = new();

    private string? _resolved;

    public KompasInteropProvider(
        string? overrideDirectory,
        string baseDirectory,
        string cacheRoot,
        Func<string?> installDirectory)
    {
        _overrideDirectory = overrideDirectory;
        _baseDirectory = baseDirectory;
        _cacheRoot = cacheRoot;
        _installDirectory = installDirectory ?? throw new ArgumentNullException(nameof(installDirectory));
    }

    /// <summary>Провайдер с боевыми источниками: переменная окружения → каталог сервера → реестр.</summary>
    public static KompasInteropProvider CreateDefault() => new(
        Environment.GetEnvironmentVariable(OverrideVariable),
        AppContext.BaseDirectory,
        Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
            "kompas-mcp",
            "interop"),
        TryFindInstallDirectory);

    /// <summary>Относится ли сборка с таким простым именем к интеропу КОМПАС.</summary>
    public static bool IsKompasAssembly(string? simpleName) =>
        simpleName is not null
        && AssemblyNames.Any(name => string.Equals(name, simpleName, StringComparison.OrdinalIgnoreCase));

    /// <summary>
    /// Каталог со сборками. Результат кешируется на время жизни процесса; при неудаче бросает
    /// <see cref="KompasInteropException"/> с текстом, пригодным для показа пользователю.
    /// </summary>
    public string ResolveDirectory()
    {
        lock (_gate)
            return _resolved ??= Locate();
    }

    private string Locate()
    {
        if (!string.IsNullOrWhiteSpace(_overrideDirectory))
        {
            var directory = _overrideDirectory.Trim();
            if (HasRequiredFiles(directory))
                return directory;

            throw new KompasInteropException(
                $"В каталоге из переменной {OverrideVariable} ({directory}) нет интероп-сборок КОМПАС " +
                $"({string.Join(", ", RequiredFiles)}). Уберите переменную, чтобы сервер взял сборки из " +
                "установленного КОМПАС-3D, либо укажите каталог, где эти файлы лежат.");
        }

        // Сборки, положенные рядом с kompas-mcp.exe вручную, имеют приоритет над установкой:
        // это запасной путь для машин, где КОМПАС установлен без компонента SDK.
        if (HasRequiredFiles(_baseDirectory))
            return _baseDirectory;

        var installDirectory = _installDirectory();
        if (string.IsNullOrWhiteSpace(installDirectory))
            throw new KompasInteropException(Unavailable(
                "КОМПАС-3D не найден: COM-класс KOMPAS.Application не зарегистрирован в системе."));

        var archive = Path.Combine(installDirectory, "SDK", "Samples", "CSharp.zip");
        if (!File.Exists(archive))
            throw new KompasInteropException(Unavailable(
                $"в установке КОМПАС ({installDirectory}) нет компонента SDK — отсутствует файл {archive}."));

        var cacheDirectory = Path.Combine(_cacheRoot, CacheKey(new FileInfo(archive)));
        if (HasRequiredFiles(cacheDirectory))
            return cacheDirectory;

        Extract(archive, cacheDirectory);

        if (!HasRequiredFiles(cacheDirectory))
            throw new KompasInteropException(Unavailable(
                $"в архиве {archive} нет ожидаемых сборок в каталоге {ZipEntryFolder}."));

        return cacheDirectory;
    }

    private static string Unavailable(string reason) =>
        "Не удалось найти интероп-сборки КОМПАС (" + string.Join(", ", RequiredFiles) + "). " +
        "Они не входят в поставку сервера и берутся из установленного КОМПАС-3D " +
        @"(SDK\Samples\CSharp.zip, каталог Common). Причина: " + reason + " " +
        "Установите КОМПАС-3D вместе с компонентом SDK или укажите каталог с этими сборками " +
        $"в переменной окружения {OverrideVariable}.";

    /// <summary>
    /// Ключ кеша распаковки. Считается по самому архиву SDK, поэтому обновление КОМПАС (новый
    /// размер/время файла) даёт новый каталог кеша, а не подсовывает сборки от старой версии.
    /// </summary>
    public static string CacheKey(FileInfo archive)
    {
        var stamp = string.Join(
            '|',
            archive.FullName.ToLowerInvariant(),
            archive.Length.ToString(CultureInfo.InvariantCulture),
            archive.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture));

        return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(stamp)))[..16].ToLowerInvariant();
    }

    private static bool HasRequiredFiles(string? directory) =>
        !string.IsNullOrWhiteSpace(directory)
        && Directory.Exists(directory)
        && RequiredFiles.All(file => File.Exists(Path.Combine(directory, file)));

    /// <summary>
    /// Распаковать <c>Common/*.dll</c> во временный каталог и атомарно переименовать его в целевой:
    /// два стартующих одновременно сервера не должны видеть наполовину распакованный кеш.
    /// </summary>
    private static void Extract(string archivePath, string targetDirectory)
    {
        var parent = Path.GetDirectoryName(targetDirectory)
                     ?? throw new KompasInteropException($"Некорректный путь кеша: {targetDirectory}");

        Directory.CreateDirectory(parent);
        var staging = Path.Combine(parent, ".tmp-" + Guid.NewGuid().ToString("N")[..8]);
        Directory.CreateDirectory(staging);

        try
        {
            using (var archive = ZipFile.OpenRead(archivePath))
            {
                foreach (var entry in archive.Entries)
                {
                    if (!entry.FullName.StartsWith(ZipEntryFolder, StringComparison.OrdinalIgnoreCase)) continue;
                    if (!entry.FullName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) continue;

                    entry.ExtractToFile(Path.Combine(staging, entry.Name), overwrite: true);
                }
            }

            // Негодный остаток предыдущей попытки (оборванная распаковка, архив без Common)
            // сносим: иначе Move ниже упал бы IOException вместо внятного сообщения.
            if (Directory.Exists(targetDirectory) && !HasRequiredFiles(targetDirectory))
            {
                try { Directory.Delete(targetDirectory, recursive: true); } catch { }
            }

            try
            {
                Directory.Move(staging, targetDirectory);
            }
            catch (IOException) when (HasRequiredFiles(targetDirectory))
            {
                // Успел параллельный процесс — его результат ничем не хуже нашего.
            }
        }
        finally
        {
            try
            {
                if (Directory.Exists(staging)) Directory.Delete(staging, recursive: true);
            }
            catch
            {
                // Мусор во временном каталоге не повод валить запуск сервера.
            }
        }
    }

    private static string? TryFindInstallDirectory()
    {
        if (!OperatingSystem.IsWindows())
            return null;

        try
        {
            return KompasInstallLocator.TryFindInstallDirectory();
        }
        catch
        {
            // Недоступный/нестандартный реестр — трактуем как «установка не найдена».
            return null;
        }
    }
}
