using System.Runtime.Versioning;
using Kompas.Mcp.Core.Interop;
using Kompas.Mcp.Core.Threading;
using Kompas6Constants;
using KompasAPI7;

namespace Kompas.Mcp.Core.Documents;

/// <summary>Жизненный цикл документов КОМПАС через API7 (<see cref="IDocuments"/>).</summary>
[SupportedOSPlatform("windows")]
public sealed class DocumentService
{
    private readonly KompasSession _session;
    private readonly KompasDispatcher _dispatcher;

    public DocumentService(KompasSession session, KompasDispatcher dispatcher)
    {
        _session = session ?? throw new ArgumentNullException(nameof(session));
        _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
    }

    /// <summary>Создать новый документ заданного типа и сделать его активным.</summary>
    public Task<DocumentInfo> CreateAsync(KompasDocumentType type, bool visible = true, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            var docs = _session.Application.Documents;
            var doc = (IKompasDocument)docs.Add(DocumentTypes.ToEnum(type), visible);
            try { return ToInfo(doc); }
            finally { ComHelper.Release(doc); ComHelper.Release(docs); }
        }, ct);

    /// <summary>Открыть документ с диска.</summary>
    public Task<DocumentInfo> OpenAsync(string path, bool readOnly = false, bool visible = true, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            var docs = _session.Application.Documents;
            var doc = (IKompasDocument)docs.Open(path, visible, readOnly);
            if (doc is null)
                throw new InvalidOperationException($"Не удалось открыть документ: {path}");
            try { return ToInfo(doc); }
            finally { ComHelper.Release(doc); ComHelper.Release(docs); }
        }, ct);

    /// <summary>Сводка по активному документу или <c>null</c>, если документов нет.</summary>
    public Task<DocumentInfo?> GetActiveAsync(CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            var doc = _session.Application.ActiveDocument;
            if (doc is null) return null;
            try { return ToInfo(doc); }
            finally { ComHelper.Release(doc); }
        }, ct);

    /// <summary>Сохранить активный документ (для уже сохранённого — без диалога).</summary>
    public Task SaveAsync(CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => WithActive(d =>
        {
            d.Save();
            if (d.Changed)
                throw new InvalidOperationException(
                    "КОМПАС не сохранил документ: в нём остались несохранённые изменения. " +
                    "Для нового документа передайте path (Save без пути работает только для ранее сохранённого).");
            return true;
        }), ct);

    /// <summary>Сохранить активный документ по указанному пути (безопасно для новых документов).</summary>
    public Task<string> SaveAsAsync(string path, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            // Каталог создаём ДО SaveAs: в отсутствующую папку КОМПАС молча не пишет.
            var full = OutputPath.Prepare(path);
            return WithActive(d =>
            {
                d.SaveAs(full);
                // SaveAs ничего не возвращает, а КОМПАС молча не сохраняет, если файл с этим именем уже
                // открыт другим документом. Без проверки по факту агент получает ложный «Сохранено».
                var actual = d.PathName ?? string.Empty;
                var samePath = string.Equals(actual, full, StringComparison.OrdinalIgnoreCase);
                if (!samePath || !File.Exists(full))
                    throw new InvalidOperationException(
                        $"КОМПАС не сохранил документ по пути «{full}»" +
                        (actual.Length == 0
                            ? " (документ так и остался несохранённым)"
                            : samePath
                                ? " (путь документу присвоен, но файла на диске нет — проверьте права на запись)"
                                : $" (документ остался по пути «{actual}»)") +
                        (samePath
                            ? "."
                            : ". Обычная причина — файл с этим именем уже открыт в КОМПАС другим документом; " +
                              "освободите имя через document_close(all=true)."));
                return full;
            });
        }, ct);

    /// <summary>
    /// Задать наименование и/или обозначение детали активного 3D-документа. Наименование — это имя
    /// вершинного компонента: оно стоит в дереве вместо безликой «Детали» и попадает в штамп чертежа
    /// и в спецификацию. Пустой аргумент оставляет свойство как есть. Возвращает значения после записи.
    /// </summary>
    public Task<PartIdentity> SetPartIdentityAsync(string? name, string? marking, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            if (_session.Application.ActiveDocument is not IKompasDocument3D doc3d)
                throw new InvalidOperationException("Активный документ не является 3D-документом.");
            if (doc3d.TopPart is not IPart7 top)
                throw new InvalidOperationException("Не удалось получить вершинный компонент (IPart7).");

            try
            {
                if (name is not null) top.Name = name;
                if (marking is not null) top.Marking = marking;
                if (!top.Update())
                    throw new InvalidOperationException("IPart7.Update() вернул FALSE — свойства детали не применены.");

                // Read-back: КОМПАС может не принять значение (например, у компонента-заготовки),
                // а присваивание свойства об этом не сообщает.
                return new PartIdentity { Name = top.Name ?? string.Empty, Marking = top.Marking ?? string.Empty };
            }
            finally { ComHelper.Release(top); ComHelper.Release(doc3d); }
        }, ct);

    /// <summary>Закрыть активный документ. <paramref name="save"/> — сохранять ли изменения.</summary>
    public Task<bool> CloseAsync(bool save = false, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => WithActive(d =>
            d.Close(save ? DocumentCloseOptions.kdSaveChanges : DocumentCloseOptions.kdDoNotSaveChanges)), ct);

    /// <summary>
    /// Закрыть ВСЕ открытые документы без сохранения; возвращает число закрытых. Применяется для
    /// очистки между тестами, чтобы не накапливались вкладки-пустышки. Закрываем активный документ
    /// в цикле (после закрытия активным становится следующий) с защитой от зацикливания: если очередной
    /// документ закрыть не удалось, прекращаем, чтобы не крутить впустую на одном и том же.
    /// </summary>
    public Task<int> CloseAllAsync(CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            int closed = 0;
            for (int guard = 0; guard < 256; guard++)
            {
                var doc = _session.Application.ActiveDocument;
                if (doc is null) break;
                try { doc.Close(DocumentCloseOptions.kdDoNotSaveChanges); closed++; }
                finally { ComHelper.Release(doc); }
            }
            return closed;
        }, ct);

    private void WithActive(Action<IKompasDocument> action)
        => WithActive(d => { action(d); return true; });

    private T WithActive<T>(Func<IKompasDocument, T> func)
    {
        var doc = _session.Application.ActiveDocument
            ?? throw new InvalidOperationException("Нет активного документа.");
        try { return func(doc); }
        finally { ComHelper.Release(doc); }
    }

    private static DocumentInfo ToInfo(IKompasDocument doc) => new()
    {
        Name = doc.Name ?? string.Empty,
        PathName = doc.PathName ?? string.Empty,
        Type = DocumentTypes.FromEnum(doc.DocumentType),
    };
}
