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

namespace Kompas.Mcp.Core.Modeling;

/// <summary>
/// Построение 3D-детали через API5 (<c>ksPart</c>): эскизы и формообразующие операции.
/// Эскизы и операции регистрируются под целочисленными id, чтобы инструменты MCP могли
/// ссылаться на них в цикле «эскиз → операция → эскиз».
/// <para>Класс разбит на partial-файлы: ядро (этот файл), <c>PartModeler.Sketch.cs</c>
/// (эскизы и 2D-примитивы), <c>PartModeler.Features.cs</c> (формообразующие операции).</para>
/// </summary>
[SupportedOSPlatform("windows")]
public sealed partial class PartModeler : IDisposable
{
    private const int MainLineStyle = 1; // системный стиль «Основная»
    private const int AxisLineStyle = 3; // системный стиль «Осевая» (ось вращения)

    private readonly KompasSession _session;
    private readonly KompasDispatcher _dispatcher;

    private int _nextId = 1;
    private readonly Dictionary<int, SketchEntry> _sketches = new();
    private readonly Dictionary<int, ksEntity> _features = new();

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

    private sealed class SketchEntry
    {
        public required ksEntity Entity { get; init; }
        public required ksSketchDefinition Definition { get; init; }
        public ksDocument2D? Editor { get; set; } // не null, пока эскиз открыт на редактирование
    }

    /// <summary>
    /// Сбросить реестр эскизов/операций и освободить их COM-объекты. Вызывать при смене
    /// активного документа (создание/открытие/закрытие) — id больше не действительны.
    /// </summary>
    public Task ResetAsync(CancellationToken ct = default)
        => _dispatcher.InvokeAsync(ResetCore, ct);

    // ---- Внутреннее --------------------------------------------------------

    /// <summary>Создать параметрическую 2D-структуру через KompasObject (API5) и привести к интерфейсу.
    /// При неудачном приведении освобождает сырой RCW, чтобы не утекал COM-объект.</summary>
    private T NewParam<T>(StructType2DEnum kind) where T : class
    {
        var raw = _session.Kompas.GetParamStruct((short)kind);
        if (raw is T typed) return typed;
        ReleaseCom(raw);
        throw new InvalidOperationException($"GetParamStruct({kind}) вернул не {typeof(T).Name}.");
    }

    private ksDocument3D ActiveDoc3D()
        => _session.Kompas.ActiveDocument3D() as ksDocument3D
           ?? throw new InvalidOperationException("Нет активного 3D-документа.");

    private ksPart GetTopPart()
        => ActiveDoc3D().GetPart((short)Part_Type.pTop_Part) as ksPart
           ?? throw new InvalidOperationException("Не удалось получить вершинный компонент (ksPart).");

    /// <summary>Выбрать ребро детали по точке (мировые координаты). Берёт первое подходящее;
    /// отсутствие ребра — не ошибка: пакетный выбор перечисляет все промахи разом.</summary>
    private static ksEntity? TrySelectEdgeByPoint(ksPart part, double x, double y, double z)
    {
        var edges = part.EntityCollection((short)Obj3dType.o3d_edge) as ksEntityCollection
            ?? throw new InvalidOperationException("Не удалось получить коллекцию рёбер.");
        edges.SelectByPoint(x, y, z);
        if (edges.GetCount() == 0) return null;
        return edges.GetByIndex(0) as ksEntity
            ?? throw new InvalidOperationException("Ребро из коллекции не приводится к ksEntity.");
    }

    /// <summary>Выбрать грань детали по индексу из list_faces (с проверкой диапазона).</summary>
    private static ksEntity SelectFaceByIndex(ksPart part, int faceIndex)
    {
        var faces = part.EntityCollection((short)Obj3dType.o3d_face) as ksEntityCollection
            ?? throw new InvalidOperationException("Не удалось получить коллекцию граней.");
        if (faceIndex < 0 || faceIndex >= faces.GetCount())
            throw new ArgumentOutOfRangeException(nameof(faceIndex),
                $"Индекс грани вне диапазона [0; {faces.GetCount() - 1}]. Сверьтесь с list_faces.");
        return faces.GetByIndex(faceIndex) as ksEntity
            ?? throw new InvalidOperationException("Грань по индексу не приводится к ksEntity.");
    }

    /// <summary>Выбрать ребро детали по индексу из list_edges.</summary>
    private static ksEntity SelectEdgeByIndex(ksPart part, int edgeIndex)
    {
        var edges = part.EntityCollection((short)Obj3dType.o3d_edge) as ksEntityCollection
            ?? throw new InvalidOperationException("Не удалось получить коллекцию рёбер.");
        if (edgeIndex < 0 || edgeIndex >= edges.GetCount())
            throw new ArgumentOutOfRangeException(nameof(edgeIndex),
                $"Индекс ребра вне диапазона [0; {edges.GetCount() - 1}]. Сверьтесь с list_edges.");
        return edges.GetByIndex(edgeIndex) as ksEntity
            ?? throw new InvalidOperationException("Ребро по индексу не приводится к ksEntity.");
    }

    /// <summary>
    /// Создать сущность детали и сразу дать ей имя в дереве построения. Имя присваивается ДО
    /// <c>Create()</c>: КОМПАС иначе показывает безликое «Операция выдавливания:3», и агент,
    /// вернувшийся к дереву через сотню вызовов, не может сказать, что именно строила операция.
    /// </summary>
    private static ksEntity NewEntity(ksPart part, Obj3dType type, string? name)
    {
        var entity = part.NewEntity((short)type) as ksEntity
            ?? throw new InvalidOperationException($"NewEntity({type}) вернул null.");
        var normalized = FeatureName.Normalize(name);
        if (normalized is not null) entity.name = normalized;
        return entity;
    }

    /// <summary>Зарегистрировать созданную операцию под новым id (ссылки из массивов/зеркала).</summary>
    private int Register(ksEntity entity)
    {
        var id = _nextId++;
        _features[id] = entity;
        return id;
    }

    private SketchEntry RequireSketch(int id)
        => _sketches.TryGetValue(id, out var s) ? s
           : throw new KeyNotFoundException($"Эскиз с id={id} не найден.");

    /// <summary>Получить ksEntity ранее созданной операции по её id (источник для массива/зеркала).</summary>
    private ksEntity RequireFeature(int id)
        => _features.TryGetValue(id, out var f) ? f
           : throw new KeyNotFoundException($"Операция с id={id} не найдена.");

    private SketchEntry RequireOpenSketch(int id)
    {
        var s = RequireSketch(id);
        if (s.Editor is null)
            throw new InvalidOperationException($"Эскиз id={id} уже закрыт — добавление геометрии невозможно.");
        return s;
    }

    private static void CloseSketchCore(SketchEntry s)
    {
        if (s.Editor is null) return;
        s.Definition.EndEdit();
        ReleaseCom(s.Editor); // редактор после EndEdit больше не нужен
        s.Editor = null;
    }

    private void ResetCore()
    {
        foreach (var s in _sketches.Values)
        {
            ReleaseCom(s.Editor);
            ReleaseCom(s.Definition);
            ReleaseCom(s.Entity);
        }
        foreach (var f in _features.Values)
            ReleaseCom(f);
        _sketches.Clear();
        _features.Clear();
        _nextId = 1;
    }

    private static void ReleaseCom(object? comObject) => ComHelper.Release(comObject);

    public void Dispose()
    {
        // Освобождаем COM на STA-потоке; не виснем на старте, если поток занят.
        try { _dispatcher.InvokeAsync(ResetCore).Wait(TimeSpan.FromSeconds(5)); }
        catch { /* при остановке диспетчера COM освободит GC */ }
    }
}
