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

namespace Kompas.Mcp.Core.Modeling;

/// <summary>
/// Операция «Отверстие» через API7 (<c>IHoles3D</c>/<c>IHole3D</c>). В API5 определения
/// отверстия нет, поэтому используется современный API7 (как и прямое редактирование грани).
/// </summary>
[SupportedOSPlatform("windows")]
public sealed class HoleService
{
    private readonly KompasSession _session;
    private readonly KompasDispatcher _dispatcher;

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

    /// <summary>
    /// Просверлить простое цилиндрическое отверстие (ksHTBase) на грани, найденной по мировой точке
    /// (<paramref name="x"/>,<paramref name="y"/>,<paramref name="z"/>), с центром в этой точке.
    /// diameter — диаметр (мм). throughAll — сквозное (depth игнорируется), иначе на глубину depth (мм).
    /// </summary>
    public Task HoleAsync(double x, double y, double z, double diameter, double depth, bool throughAll,
        string? name = null, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => HoleCore(x, y, z, diameter, depth, throughAll, ksHoleTypeEnum.ksHTBase, null, name), ct);

    /// <summary>Отверстие с цековкой (цилиндрическим уширением сверху, под винт с цилиндрической
    /// головкой): основное отверстие diameter + цековка spotDiameter×spotDepth.</summary>
    public Task CounterboreHoleAsync(double x, double y, double z, double diameter, double depth, bool throughAll,
        double spotDiameter, double spotDepth, string? name = null, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            if (!double.IsFinite(spotDiameter) || spotDiameter <= diameter)
                throw new ArgumentOutOfRangeException(nameof(spotDiameter), "Диаметр цековки должен быть больше диаметра отверстия.");
            if (!double.IsFinite(spotDepth) || spotDepth <= 0)
                throw new ArgumentOutOfRangeException(nameof(spotDepth), "Глубина цековки должна быть > 0.");
            if (!throughAll && double.IsFinite(depth) && depth <= spotDepth)
                throw new ArgumentOutOfRangeException(nameof(depth), "Глубина глухого отверстия должна быть больше глубины цековки.");
            HoleCore(x, y, z, diameter, depth, throughAll, ksHoleTypeEnum.ksHTCounterbore, hole =>
            {
                var p = hole.HoleParameters as ISpotfacingHoleParameters
                    ?? throw new InvalidOperationException("HoleParameters не приводится к ISpotfacingHoleParameters.");
                p.SpotfacingDiameter = spotDiameter;
                p.SpotfacingDepth = spotDepth;
            }, name);
        }, ct);

    /// <summary>Отверстие с зенковкой (коническим уширением сверху, под винт с потайной головкой):
    /// основное отверстие diameter + зенковка sinkDiameter под углом sinkAngle (градусы, конус).</summary>
    public Task CountersinkHoleAsync(double x, double y, double z, double diameter, double depth, bool throughAll,
        double sinkDiameter, double sinkAngle, string? name = null, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            if (!double.IsFinite(sinkDiameter) || sinkDiameter <= diameter)
                throw new ArgumentOutOfRangeException(nameof(sinkDiameter), "Диаметр зенковки должен быть больше диаметра отверстия.");
            if (!double.IsFinite(sinkAngle) || sinkAngle <= 0 || sinkAngle >= 180)
                throw new ArgumentOutOfRangeException(nameof(sinkAngle), "Угол зенковки должен быть в (0; 180) градусов.");
            HoleCore(x, y, z, diameter, depth, throughAll, ksHoleTypeEnum.ksHTCountersinking, hole =>
            {
                var p = hole.HoleParameters as ICountersinkHoleParameters
                    ?? throw new InvalidOperationException("HoleParameters не приводится к ICountersinkHoleParameters.");
                p.CountersinkType = ksCountersinkTypeEnum.ksCTDiameterAngle; // задаём диаметр + угол
                p.CountersinkDiameter = sinkDiameter;
                p.CountersinkAngle = sinkAngle;
            }, name);
        }, ct);

    /// <summary>Коническое отверстие (ksHTConic): конусность задаётся углом conicAngle (градусы).
    /// diameter — диаметр у дна, конус расширяется к поверхности.</summary>
    public Task ConicHoleAsync(double x, double y, double z, double diameter, double depth, bool throughAll,
        double conicAngle, string? name = null, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            if (!double.IsFinite(conicAngle) || conicAngle <= 0 || conicAngle >= 180)
                throw new ArgumentOutOfRangeException(nameof(conicAngle), "Угол конуса должен быть в (0; 180) градусов.");
            HoleCore(x, y, z, diameter, depth, throughAll, ksHoleTypeEnum.ksHTConic, hole =>
            {
                var p = hole.HoleParameters as IConicHoleParameters
                    ?? throw new InvalidOperationException("HoleParameters не приводится к IConicHoleParameters.");
                p.ConicType = ksConicTypeEnum.ksCNAngle;
                p.ConicAngle = conicAngle;
            }, name);
        }, ct);

    private void HoleCore(double x, double y, double z, double diameter, double depth, bool throughAll,
        ksHoleTypeEnum holeType, Action<IHole3D>? configure, string? name)
    {
        if (!double.IsFinite(x) || !double.IsFinite(y) || !double.IsFinite(z))
            throw new ArgumentException("Координаты точки отверстия должны быть конечными числами.");
        if (!double.IsFinite(diameter) || diameter <= 0)
            throw new ArgumentOutOfRangeException(nameof(diameter), "Диаметр должен быть конечным и > 0.");
        if (!throughAll && (!double.IsFinite(depth) || depth <= 0))
            throw new ArgumentOutOfRangeException(nameof(depth), "Глубина должна быть конечной и > 0 (или throughAll).");

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

        var face = FindFaceAtPoint(top, x, y, z)
            ?? throw new InvalidOperationException(
                $"В точке ({x}, {y}, {z}) не найдена грань. Сверьте координаты точки на грани (model_snapshot).");

        var container = top as IModelContainer
            ?? throw new InvalidOperationException("Деталь не приводится к IModelContainer.");

        // Точка размещения центра отверстия в мировых координатах.
        var pt = container.Points3D.Add()
            ?? throw new InvalidOperationException("Points3D.Add() вернул null.");
        pt.ParameterType = ksPoint3DTypeEnum.ksPParamCoord;
        pt.X = x; pt.Y = y; pt.Z = z;
        if (!pt.Update())
            throw new InvalidOperationException("Update() точки размещения вернул FALSE.");

        var before = CurrentVolume();

        // Если отверстие не построится — удаляем уже созданную точку (и сам объект отверстия),
        // чтобы не засорять дерево «сиротами» при неуспехе.
        IHole3D? hole = null;
        try
        {
            hole = container.Holes3D.Add()
                ?? throw new InvalidOperationException("Holes3D.Add() вернул null.");
            hole.HoleType = holeType;
            hole.Diameter = diameter;
            hole.DepthType = throughAll ? ksDepthTypeEnum.ksDTReachThrough : ksDepthTypeEnum.ksDTValue;
            if (!throughAll) hole.Depth = depth;
            hole.EndFaceType = ksEndFaceTypeEnum.ksEFFlat;
            hole.Axis = false;
            hole.ShowThread = false;
            // Имя ставим до Update(): иначе в дереве останется «Отверстие:1».
            if (FeatureName.Normalize(name) is { } title && hole is IModelObject named) named.Name = title;
            configure?.Invoke(hole); // параметры цековки/зенковки (после установки HoleType)

            var disp = (IHoleDisposal)hole;
            disp.BaseSurface = (IModelObject)face;
            disp.Perpendicular = true;
            disp.AssociationVertex = (IModelObject)pt;

            // Направление сверления подбираем ПО ФАКТУ удаления материала: нормаль грани может
            // смотреть в любую сторону, а при сквозном отверстии Update() возвращает TRUE даже когда
            // сверлит «в воздух» (наружу тела) — поэтому одного флага успеха Update недостаточно.
            if (!TryDirection(hole, disp, doc3d, true, before) &&
                !TryDirection(hole, disp, doc3d, false, before))
                throw new InvalidOperationException(
                    "Отверстие не удалило материал ни в одном направлении (точка вне тела или диаметр/глубина несовместимы?).");
        }
        catch
        {
            // Best-effort откат: сначала объект отверстия, затем точка размещения.
            try { hole?.Owner?.Delete(); } catch { /* лучшее усилие */ }
            try { pt.Owner?.Delete(); } catch { /* лучшее усилие */ }
            doc3d.RebuildDocument();
            throw;
        }
    }

    /// <summary>Построить отверстие в заданном направлении и подтвердить, что материал убыл
    /// (объём уменьшился). Возвращает true только при фактическом удалении материала.</summary>
    private bool TryDirection(IHole3D hole, IHoleDisposal disp, IKompasDocument3D doc3d, bool direction, double before)
    {
        disp.Direction = direction;
        if (!hole.Update()) return false;
        doc3d.RebuildDocument();
        return CurrentVolume() < before - 1e-6;
    }

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

    private static IFace? FindFaceAtPoint(IPart7 part, double x, double y, double z)
    {
        var hit = part.FindObjectsByPoint(x, y, z, true);
        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;
        }
    }
}
