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

namespace Kompas.Mcp.Core.Modeling;

/// <summary>
/// Элементарные тела API7 (<c>IElementaryBodies</c>): параллелепипед, цилиндр, сфера, конус.
/// Это второй способ формообразования рядом с «эскиз → выдавливание»: тело задаётся размерами,
/// без эскиза, и сразу может объединяться/вычитаться из существующего
/// (<see cref="PrimitiveResult"/> → <c>ksOperationResultEnum</c>).
/// </summary>
[SupportedOSPlatform("windows")]
public sealed class PrimitiveService
{
    private readonly KompasSession _session;
    private readonly KompasDispatcher _dispatcher;

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

    /// <summary>
    /// Построить примитив в точке (<paramref name="x"/>,<paramref name="y"/>,<paramref name="z"/>) —
    /// для блока это угол, для остальных центр основания. Размеры — по осям текущей системы координат:
    /// length по X, width по Y, height по Z.
    /// </summary>
    public Task PrimitiveAsync(
        PrimitiveKind kind, double x, double y, double z,
        double? length = null, double? width = null, double? height = null, double? diameter = null,
        PrimitiveResult result = PrimitiveResult.NewBody, bool forward = true,
        string? name = null, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            Primitives.Validate(kind, length, width, height, diameter);
            if (!double.IsFinite(x) || !double.IsFinite(y) || !double.IsFinite(z))
                throw new ArgumentException("Координаты точки привязки должны быть конечными числами.");

            if (_session.Application.ActiveDocument is not IKompasDocument3D doc3d)
                throw new InvalidOperationException("Активный документ не является 3D-документом.");
            if (doc3d.TopPart is not IPart7 top)
                throw new InvalidOperationException("Не удалось получить TopPart активного документа.");
            if (top is not IModelContainer container)
                throw new InvalidOperationException("Деталь не приводится к IModelContainer.");

            var bodies = container.ElementaryBodies
                ?? throw new InvalidOperationException("ElementaryBodies недоступны (нужен КОМПАС v21 и новее).");

            var before = CurrentVolume();
            var body = bodies.Add(Primitives.ToObj3dType(kind))
                ?? throw new InvalidOperationException($"ElementaryBodies.Add({kind}) вернул null.");
            try
            {
                var elementary = (IElementaryBody)body;
                elementary.OperationResult = Primitives.ToOperationResult(result);
                // Имя ставим до Update(): иначе в дереве останется «Параллелепипед:2».
                if (FeatureName.Normalize(name) is { } title && elementary is IModelObject named)
                    named.Name = title;
                Place(elementary, x, y, z);
                Configure(body, kind, length, width, height, diameter, forward);

                if (!elementary.Update())
                    throw new InvalidOperationException(
                        $"Update() примитива вернул FALSE: {Explain(result)}");

                RequireVolumeChanged(before, result);
            }
            catch
            {
                // Неудачный примитив не должен оставаться в дереве «сиротой».
                try { ((IElementaryBody)body).Owner?.Delete(); } catch { /* лучшее усилие */ }
                throw;
            }
            finally { ComHelper.Release(body); ComHelper.Release(bodies); }
        }, ct);

    /// <summary>Точка привязки примитива задаётся его локальной системой координат.</summary>
    private static void Place(IElementaryBody body, double x, double y, double z)
    {
        var position = body.Position
            ?? throw new InvalidOperationException("Position примитива недоступна.");
        position.X = x;
        position.Y = y;
        position.Z = z;
    }

    private static void Configure(
        object body, PrimitiveKind kind,
        double? length, double? width, double? height, double? diameter, bool forward)
    {
        var direction = forward ? ksDirectionTypeEnum.dtNormal : ksDirectionTypeEnum.dtReverse;
        // Height/HeightType/Diameter у примитивов — ИНДЕКСИРОВАННЫЕ свойства: индекс [true] это
        // прямая сторона, [false] — обратная (для двунаправленных построений). Без индекса не компилируется.
        const bool normalSide = true;

        switch (kind)
        {
            case PrimitiveKind.Block:
                var block = (IBlockBySizes)body;
                block.BlockPosition = ksBlockPositionTypeEnum.ksVertex; // точка привязки — угол
                block.Length = length!.Value;
                block.Width = width!.Value;
                block.Height[normalSide] = height!.Value;
                block.LengthDirection = true;
                block.WidthDirection = true;
                block.HeightDirection = direction;
                block.HeightType[normalSide] = ksEndTypeEnum.etBlind;
                break;
            case PrimitiveKind.Cylinder:
                var cylinder = (ICylinder)body;
                cylinder.Diameter = diameter!.Value;
                cylinder.Height[normalSide] = height!.Value;
                cylinder.HeightDirection = direction;
                cylinder.HeightType[normalSide] = ksEndTypeEnum.etBlind;
                break;
            case PrimitiveKind.Sphere:
                ((ISphere)body).Diameter = diameter!.Value;
                break;
            case PrimitiveKind.Cone:
                var cone = (IConeByHeight)body;
                cone.BaseDiameter = diameter!.Value;
                cone.Diameter[normalSide] = 0;        // вершина сведена в точку — полный конус
                cone.Height[normalSide] = height!.Value;
                cone.ConeDirection = direction;
                cone.HeightType[normalSide] = ksEndTypeEnum.etBlind;
                break;
        }
    }

    /// <summary>
    /// Update()==TRUE ещё не значит, что примитив что-то изменил: вычитание мимо тела КОМПАС
    /// считает успехом и молча оставляет модель как была. Проверяем по объёму.
    /// </summary>
    private void RequireVolumeChanged(double before, PrimitiveResult result)
    {
        const double eps = 1e-6;
        var after = CurrentVolume();
        switch (result)
        {
            case PrimitiveResult.Subtract when after > before - eps:
                throw new InvalidOperationException(
                    "Вычитание примитива не удалило материал: тело не пересекается с ним. " +
                    "Сверьте точку привязки и размеры (для блока x,y,z — это УГОЛ, а не центр).");
            case PrimitiveResult.Intersect when Math.Abs(after - before) < eps:
                throw new InvalidOperationException(
                    "Пересечение с примитивом не изменило тело — примитив накрывает его целиком или не задевает.");
            case PrimitiveResult.NewBody or PrimitiveResult.Union when after < before + eps:
                throw new InvalidOperationException(
                    "Примитив не добавил материала: нулевые размеры или он целиком внутри существующего тела.");
        }
    }

    /// <summary>Текущий объём детали (мм³) через API5 МЦХ — та же метрика, что у QueryService.</summary>
    private double CurrentVolume()
    {
        const int mixMmKg = 0x1 | 0x10; // ST_MIX_MM | ST_MIX_KG
        var doc3d = _session.Kompas.ActiveDocument3D() as Kompas6API5.ksDocument3D
            ?? throw new InvalidOperationException("Нет активного 3D-документа (API5).");
        var part = doc3d.GetPart((short)Kompas6Constants3D.Part_Type.pTop_Part) as Kompas6API5.ksPart
            ?? throw new InvalidOperationException("Не удалось получить вершинный компонент (ksPart).");
        var mass = part.CalcMassInertiaProperties(mixMmKg) as Kompas6API5.ksMassInertiaParam;
        return mass?.v ?? 0;
    }

    /// <summary>Update() у примитива возвращает FALSE молча — подсказываем вероятную причину.</summary>
    private static string Explain(PrimitiveResult result) => result switch
    {
        PrimitiveResult.Subtract => "вычитание не пересекает ни одно тело или удалило бы его целиком?",
        PrimitiveResult.Union => "объединять не с чем — примитив не касается существующих тел?",
        PrimitiveResult.Intersect => "пересечение пустое?",
        _ => "нулевые или несовместимые размеры?",
    };
}
