using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Kompas.Mcp.Core.Interop;
using Kompas.Mcp.Core.Threading;
using Kompas6API5;
using KompasAPI7;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace Kompas.Mcp.Core;

/// <summary>
/// Подключение к КОМПАС и владение корневыми COM-объектами на время жизни сервера.
/// <para>
/// Подключается через API5 (<see cref="KompasObject"/>), так как он нужен для построения 3D
/// (ksPart) и снимков (ksDocument3D), а из него получает современный API7 (<see cref="IApplication"/>).
/// Все обращения к COM выполняются на STA-потоке <see cref="KompasDispatcher"/>.
/// </para>
/// </summary>
[SupportedOSPlatform("windows")]
public sealed class KompasSession : IDisposable
{
    // Порядок попыток. Нужен KompasObject (API5), поэтому .7 не используем как точку входа.
    // Проверено на v24 Home: зарегистрирован KOMPAS.Application.5 (не LT).
    private static readonly string[] ProgIds =
    {
        "KOMPAS.Application.5",
        "KOMPASLT.Application.5",
    };

    private readonly KompasDispatcher _dispatcher;
    private readonly ILogger<KompasSession> _log;

    private KompasObject? _kompas;   // API5 root
    private IApplication? _app;      // API7 application
    private bool _launchedByUs;

    public KompasSession(KompasDispatcher dispatcher, ILogger<KompasSession>? log = null)
    {
        _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
        _log = log ?? NullLogger<KompasSession>.Instance;
    }

    public bool IsConnected => _app is not null;

    /// <summary>Сведения о последнем успешном подключении (null, пока не подключались).</summary>
    public KompasConnectionInfo? LastInfo { get; private set; }

    /// <summary>Показать/скрыть окно КОМПАС.</summary>
    public Task SetVisibleAsync(bool visible, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => Application.Visible = visible, ct);

    /// <summary>Корневой объект API5. Доступен только на STA-потоке после подключения.</summary>
    public KompasObject Kompas =>
        _kompas ?? throw new InvalidOperationException("Нет подключения к КОМПАС. Сначала вызовите ConnectAsync.");

    /// <summary>Приложение API7. Доступно только на STA-потоке после подключения.</summary>
    public IApplication Application =>
        _app ?? throw new InvalidOperationException("Нет подключения к КОМПАС. Сначала вызовите ConnectAsync.");

    /// <summary>Подключиться к КОМПАС: attach к запущенному или launch нового экземпляра.</summary>
    public Task<KompasConnectionInfo> ConnectAsync(bool visible = true, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => ConnectCore(visible), ct);

    private KompasConnectionInfo ConnectCore(bool visible)
    {
        if (_app is not null)
            return LastInfo ??= BuildInfo(_lastProgId);

        // 1) attach к уже запущенному экземпляру
        foreach (var id in ProgIds)
        {
            if (NativeMethods.GetActiveObject(id) is KompasObject ko)
            {
                _kompas = ko;
                _lastProgId = id;
                _launchedByUs = false;
                _log.LogInformation("Присоединились к запущенному КОМПАС ({ProgId})", id);
                break;
            }
        }

        // 2) запустить новый экземпляр
        if (_kompas is null)
        {
            foreach (var id in ProgIds)
            {
                var type = Type.GetTypeFromProgID(id, throwOnError: false);
                if (type is null) continue;
                try
                {
                    if (Activator.CreateInstance(type) is KompasObject ko)
                    {
                        _kompas = ko;
                        _lastProgId = id;
                        _launchedByUs = true;
                        _log.LogInformation("Запустили новый КОМПАС ({ProgId})", id);
                        break;
                    }
                }
                catch (Exception ex)
                {
                    _log.LogWarning(ex, "Не удалось создать экземпляр {ProgId}", id);
                }
            }
        }

        if (_kompas is null)
            throw new InvalidOperationException(
                "Не удалось подключиться к КОМПАС. Убедитесь, что КОМПАС-3D установлен и запускается.");

        _app = (IApplication?)_kompas.ksGetApplication7()
            ?? throw new InvalidOperationException("ksGetApplication7() вернул null — API7 недоступен.");

        _app.Visible = visible;

        return LastInfo = BuildInfo(_lastProgId);
    }

    private string _lastProgId = string.Empty;

    private KompasConnectionInfo BuildInfo(string progId)
    {
        int major = 0, minor = 0, release = 0, build = 0;
        try { _app!.GetSystemVersion(out major, out minor, out release, out build); }
        catch (Exception ex) { _log.LogWarning(ex, "GetSystemVersion недоступен"); }

        int buildNumber = 0;
        try { buildNumber = _app!.BuildNumber; }
        catch (Exception ex) { _log.LogWarning(ex, "BuildNumber недоступен"); }

        return new KompasConnectionInfo
        {
            ProgId = progId,
            WasLaunched = _launchedByUs,
            Version = $"{major}.{minor}.{release}",
            BuildNumber = buildNumber,
            Edition = progId.StartsWith("KOMPASLT", StringComparison.OrdinalIgnoreCase) ? "Home/LT" : "Full",
        };
    }

    public void Dispose()
    {
        // Освобождаем COM-объекты на том же STA-потоке, на котором они созданы.
        try
        {
            // Ограниченное ожидание: не виснем на старте остановки, если STA-поток занят COM-вызовом.
            _dispatcher.InvokeAsync(() =>
            {
                // Quit не вызываем: не «убиваем» КОМПАС пользователя (см. docs/OPEN_QUESTIONS.md).
                if (_app is not null && Marshal.IsComObject(_app)) Marshal.ReleaseComObject(_app);
                if (_kompas is not null && Marshal.IsComObject(_kompas)) Marshal.ReleaseComObject(_kompas);
                _app = null;
                _kompas = null;
            }).Wait(TimeSpan.FromSeconds(5));
        }
        catch
        {
            // Диспетчер остановлен / вызов завис — COM освободит GC-финализатор.
        }
    }
}
