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

namespace Kompas.Mcp.Core.Assemblies;

/// <summary>
/// Операции со сборками через API7 (<c>IParts7</c>). Аналогично <c>HoleService</c>/<c>FaceEditService</c>:
/// контейнеры берутся COM-QI, все вызовы — на STA-потоке <see cref="KompasDispatcher"/>.
/// </summary>
[SupportedOSPlatform("windows")]
public sealed class AssemblyService
{
    private readonly KompasSession _session;
    private readonly KompasDispatcher _dispatcher;

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

    /// <summary>
    /// Вставить компонент (деталь .m3d или подсборку .a3d) из файла в активную сборку, разместив
    /// его origin в мировой точке (<paramref name="x"/>,<paramref name="y"/>,<paramref name="z"/>) мм.
    /// </summary>
    public Task<AddedComponent> AddComponentAsync(string filePath, double x, double y, double z, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => AddComponentCore(filePath, x, y, z), ct);

    private AddedComponent AddComponentCore(string filePath, double x, double y, double z)
    {
        var path = AssemblyValidation.NormalizeComponentPath(filePath);
        AssemblyValidation.RequireFinitePosition(x, y, z);
        if (!File.Exists(path))
            throw new FileNotFoundException($"Файл компонента не найден: {path}", path);

        var top = RequireActiveAssembly();
        var parts = top.Parts
            ?? throw new InvalidOperationException("Не удалось получить коллекцию компонентов сборки (Parts).");

        // Фиксируем число компонентов ДО вставки — для верифицируемого отката (откатываем всё, что
        // появилось сверх beforeCount), не полагаясь на возможный null-возврат AddFromFile.
        int beforeCount = parts.Count;

        // ExternalFile=true: компонент ссылается на внешний .m3d/.a3d. Redraw=true: вставить и
        // перерисовать в исходном положении (само перемещение применит SetOrigin+RebuildModel ниже).
        var part = parts.AddFromFile(path, true, true) as IPart7;
        if (part is null)
        {
            // AddFromFile мог вставить компонент и всё равно вернуть null/не-IPart7 — почистим, если
            // число компонентов выросло, чтобы не оставить «осиротевший» компонент в сборке.
            RollbackInsertion(top, parts, beforeCount);
            throw new InvalidOperationException(
                $"Не удалось вставить компонент из файла (AddFromFile вернул null или не IPart7): {path}");
        }

        try
        {
            var placement = part.Placement as IPlacement3D
                ?? throw new InvalidOperationException("Placement компонента не приводится к IPlacement3D.");
            if (!placement.SetOrigin(x, y, z))
                throw new InvalidOperationException("SetOrigin вернул FALSE — не удалось задать положение компонента.");

            // UpdatePlacement применяет изменение Placement; для вручную позиционируемого компонента
            // (без сопряжений/зависимостей) он может вернуть FALSE — это НЕ ошибка (нечего
            // пересчитывать). Фактически положение становится видимым в геометрии после RebuildModel.
            part.UpdatePlacement(true);
            if (!top.RebuildModel(true))
                throw new InvalidOperationException("RebuildModel вернул FALSE — сборка не перестроена после вставки компонента.");

            return new AddedComponent { Index = parts.Count - 1, Name = SafeName(part) };
        }
        catch
        {
            // Верифицируемый откол: удаляем всё, что добавилось сверх beforeCount, и проверяем.
            RollbackInsertion(top, parts, beforeCount);
            throw;
        }
    }

    /// <summary>
    /// Наложить сопряжение <paramref name="type"/> между гранями двух компонентов активной сборки,
    /// выбранными по мировым точкам (<paramref name="x1"/>,<paramref name="y1"/>,<paramref name="z1"/>)
    /// и (<paramref name="x2"/>,<paramref name="y2"/>,<paramref name="z2"/>). Для расстояния
    /// <paramref name="value"/> — зазор (мм). Решатель спозиционирует компоненты; результат — статус
    /// построенного сопряжения.
    /// </summary>
    public Task<AddedMate> AddMateAsync(MateType type, double x1, double y1, double z1,
        double x2, double y2, double z2, double value, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => AddMateCore(type, x1, y1, z1, x2, y2, z2, value), ct);

    private AddedMate AddMateCore(MateType type, double x1, double y1, double z1,
        double x2, double y2, double z2, double value)
    {
        AssemblyValidation.RequireFinitePosition(x1, y1, z1);
        AssemblyValidation.RequireFinitePosition(x2, y2, z2);
        AssemblyValidation.RequireMateValue(Mates.RequiresValue(type), value);

        var top = RequireActiveAssembly();
        var face1 = FindFaceInAssembly(top, x1, y1, z1)
            ?? throw new InvalidOperationException(
                $"В точке ({x1},{y1},{z1}) не найдена грань компонента. Укажите точку в середине грани (model_snapshot).");
        var face2 = FindFaceInAssembly(top, x2, y2, z2)
            ?? throw new InvalidOperationException(
                $"В точке ({x2},{y2},{z2}) не найдена грань компонента. Укажите точку в середине грани (model_snapshot).");

        var mates = top.MateConstraints
            ?? throw new InvalidOperationException("Не удалось получить коллекцию сопряжений сборки (MateConstraints).");
        var mate = mates.Add(Mates.ToConstraintType(type))
            ?? throw new InvalidOperationException("MateConstraints.Add вернул null.");

        try
        {
            mate.BaseObject1 = (IModelObject)face1;
            mate.BaseObject2 = (IModelObject)face2;
            if (Mates.RequiresValue(type))
                mate.ParamValue = value;

            if (!mate.Update())
                throw new InvalidOperationException(
                    "Update сопряжения вернул FALSE — связь не наложена (несовместимые грани/значение?).");
            if (!top.RebuildModel(true))
                throw new InvalidOperationException("RebuildModel вернул FALSE — сборка не перестроена после сопряжения.");
            if (!mate.Valid)
                throw new InvalidOperationException(
                    "Сопряжение построено, но вырождено (Valid=false) — вероятно, грани несовместимы " +
                    "или принадлежат одному компоненту. Проверьте выбор граней.");

            return new AddedMate { Type = type, Valid = true };
        }
        catch
        {
            // Полный откат: при ЛЮБОЙ ошибке после Add удаляем сопряжение, чтобы не оставить «сироту».
            try { mate.Owner?.Delete(); top.RebuildModel(true); } catch { /* лучшее усилие */ }
            throw;
        }
    }

    private static IFace? FindFaceInAssembly(IPart7 top, double x, double y, double z)
    {
        // FirstLevel=false — спускаемся в компоненты, чтобы получить их грани в контексте сборки.
        var hit = top.FindObjectsByPoint(x, y, z, false);
        switch (hit)
        {
            case IFace f: return f;
            case object[] arr:
                foreach (var o in arr) if (o is IFace face) return face;
                return null;
            case System.Collections.IEnumerable en:
                foreach (var o in en) if (o is IFace face) return face;
                return null;
            default: return null;
        }
    }

    /// <summary>
    /// Откатить вставку: удалить компоненты, появившиеся сверх <paramref name="beforeCount"/> (обычно
    /// один — только что вставленный). Best-effort, но с верификацией: компонент удаляется через
    /// <c>IFeature7.Delete()</c> своего владельца, затем сборка перестраивается.
    /// </summary>
    private static void RollbackInsertion(IPart7 top, IParts7 parts, int beforeCount)
    {
        try
        {
            // Удаляем с конца, пока число не вернётся к исходному (защита от частичной вставки).
            for (int guard = 0; parts.Count > beforeCount && guard < 8; guard++)
            {
                if (parts.Part[parts.Count - 1] is IPart7 last && last.Owner is { } owner)
                    owner.Delete();
                else
                    break; // не удалось получить владельца — выходим, чтобы не зациклиться
            }
            top.RebuildModel(true);
        }
        catch { /* лучшее усилие: исходное исключение важнее ошибки отката */ }
    }

    /// <summary>
    /// Получить верхний компонент активной сборки, проверив тип документа. Раздельные ошибки:
    /// нет документа / документ не сборка / не 3D / нет TopPart. Деталь — тоже IKompasDocument3D,
    /// поэтому отсекаем по DocumentType ДО доступа к TopPart.
    /// </summary>
    private IPart7 RequireActiveAssembly()
    {
        var doc = _session.Application.ActiveDocument
            ?? throw new InvalidOperationException("Нет активного документа. Создайте сборку: document_create assembly.");
        if (doc.DocumentType != DocumentTypeEnum.ksDocumentAssembly)
            throw new InvalidOperationException(
                $"Активный документ не является сборкой (тип: {doc.DocumentType}). Создайте сборку: document_create assembly.");
        if (doc is not IKompasDocument3D doc3d)
            throw new InvalidOperationException("Активный документ не является 3D-документом.");
        return doc3d.TopPart as IPart7
            ?? throw new InvalidOperationException("Не удалось получить TopPart сборки.");
    }

    private static string SafeName(IPart7 part)
    {
        try { return part.Name ?? string.Empty; }
        catch { return string.Empty; }
    }
}
