using System.Globalization;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Kompas.Mcp.Core.Documents;
using Kompas.Mcp.Core.Interop;
using Kompas.Mcp.Core.Threading;
using Kompas6API5;
using Kompas6Constants3D;

namespace Kompas.Mcp.Core.Query;

/// <summary>
/// Структурное «зрение» агента: чтение модели как данных, а не картинки — дерево построения,
/// тела, топология, переменные, drill-down по граням/рёбрам и измерения. Всё через API5
/// <c>ksPart</c> на STA-потоке <see cref="KompasDispatcher"/>.
/// </summary>
[SupportedOSPlatform("windows")]
public sealed class ModelInspectionService
{
    private const uint MixMm = 0x1;          // длины/площади — мм
    private const int MixMmKg = 0x1 | 0x10;  // МЦХ — мм + кг

    private readonly KompasSession _session;
    private readonly KompasDispatcher _dispatcher;

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

    /// <summary>Собрать структурный «паспорт» активной детали одним проходом.
    /// <paramref name="sections"/> ограничивает сбор: незапрошенные разделы не читаются вовсе
    /// (обход топологии по всем граням и рёбрам — самая дорогая часть).</summary>
    public Task<ModelDescription> DescribeModelAsync(
        InspectionText.ModelSections sections = InspectionText.ModelSections.All, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            var part = GetTopPart();
            try
            {
                var (name, typeName) = ActiveDocInfo();
                var partName = ComHelper.SafeStr(() => part.name);
                var partMarking = ComHelper.SafeStr(() => part.marking);

                bool hasGab = part.GetGabarit(false, false,
                    out double x1, out double y1, out double z1, out double x2, out double y2, out double z2);
                var box = hasGab ? Box(x1, y1, z1, x2, y2, z2) : null;
                bool empty = box is null || InspectionText.IsEmptyGabarit(box.SizeX, box.SizeY, box.SizeZ);

                if (empty)
                    return new ModelDescription
                    {
                        Name = name, PartName = partName, PartMarking = partMarking, IsEmpty = true,
                        Bodies = Array.Empty<BodyInfo>(), FaceTypes = Array.Empty<string>(),
                        EdgeTypes = Array.Empty<string>(), Features = Array.Empty<FeatureInfo>(),
                        Imported = false, Variables = Array.Empty<VariableInfo>(),
                        DocumentTypeName = typeName,
                    };

                // Раздел «дерево» решает, импортирована ли модель, а для этого нужно число тел —
                // поэтому тела читаем и тогда, когда печатать их не просили.
                var needBodies = sections.HasFlag(InspectionText.ModelSections.Bodies)
                                 || sections.HasFlag(InspectionText.ModelSections.Tree);

                IReadOnlyList<BodyInfo> bodies = needBodies ? ReadBodies(part) : [];
                IReadOnlyList<string> faceTypes = [], edgeTypes = [];
                if (sections.HasFlag(InspectionText.ModelSections.Topology))
                    (faceTypes, edgeTypes) = ReadTopology(part);
                IReadOnlyList<FeatureInfo> features = sections.HasFlag(InspectionText.ModelSections.Tree)
                    ? ReadFeatures(part) : [];
                IReadOnlyList<VariableInfo> variables = sections.HasFlag(InspectionText.ModelSections.Variables)
                    ? ReadVariables(part) : [];

                return new ModelDescription
                {
                    Name = name,
                    PartName = partName,
                    PartMarking = partMarking,
                    IsEmpty = false,
                    Box = sections.HasFlag(InspectionText.ModelSections.Box) ? box : null,
                    Mass = sections.HasFlag(InspectionText.ModelSections.Mass) ? ReadMass(part) : null,
                    Bodies = bodies,
                    FaceTypes = faceTypes,
                    EdgeTypes = edgeTypes,
                    Features = features,
                    Imported = InspectionText.IsImported(features, bodies.Count),
                    Variables = variables,
                    DocumentTypeName = typeName,
                };
            }
            finally { ComHelper.Release(part); }
        }, ct);

    /// <summary>Перечислить узлы дерева построения по порядку.</summary>
    public Task<IReadOnlyList<FeatureInfo>> ListFeaturesAsync(CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            var part = GetTopPart();
            try { return ReadFeatures(part); }
            finally { ComHelper.Release(part); }
        }, ct);

    /// <summary>Перечислить тела детали.</summary>
    public Task<IReadOnlyList<BodyInfo>> ListBodiesAsync(CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            var part = GetTopPart();
            try { return ReadBodies(part); }
            finally { ComHelper.Release(part); }
        }, ct);

    /// <summary>Перечислить переменные модели.</summary>
    public Task<IReadOnlyList<VariableInfo>> ListVariablesAsync(CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            var part = GetTopPart();
            try { return ReadVariables(part); }
            finally { ComHelper.Release(part); }
        }, ct);

    /// <summary>Детали грани по индексу из list_faces: тип, площадь, нормаль, радиус, число рёбер.</summary>
    public Task<FaceDetail> DescribeFaceAsync(int index, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            var part = GetTopPart();
            try
            {
                var faces = Faces(part);
                var def = FaceAt(faces, index);

                double area = 0; try { area = def.GetArea(MixMm); } catch { }
                int edgeCount = 0; try { if (def.EdgeCollection() is ksEdgeCollection ec) edgeCount = ec.GetCount(); } catch { }
                double? radius = null;
                try { if (def.IsCylinder() && def.GetCylinderParam(out _, out double r)) radius = r; } catch { }

                return new FaceDetail
                {
                    Index = index,
                    Type = GeomClassifiers.FaceType(def),
                    Area = area,
                    Normal = FaceNormal(def),
                    Radius = radius,
                    EdgeCount = edgeCount,
                };
            }
            finally { ComHelper.Release(part); }
        }, ct);

    /// <summary>
    /// Точка на грани с индексом <paramref name="index"/> из list_faces — середина параметрической
    /// области поверхности (та же (u,v), по которой считается нормаль в DescribeFace). Мост «индекс → точка»
    /// для операций API7, которые выбирают грань только по мировой точке (перемещение грани, отверстие):
    /// агент даёт надёжный индекс, а координаты подбирает сервер.
    /// </summary>
    public Task<(double X, double Y, double Z)> FaceCenterPointAsync(int index, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            var part = GetTopPart();
            try
            {
                var def = FaceAt(Faces(part), index);
                if (def.GetSurface() is not ksSurface surf)
                    throw new InvalidOperationException($"Не удалось получить поверхность грани[{index}].");
                double u = (surf.GetParamUMin() + surf.GetParamUMax()) / 2;
                double v = (surf.GetParamVMin() + surf.GetParamVMax()) / 2;
                if (!surf.GetPoint(u, v, out double x, out double y, out double z))
                    throw new InvalidOperationException($"GetPoint для грани[{index}] вернул FALSE.");
                return (x, y, z);
            }
            finally { ComHelper.Release(part); }
        }, ct);

    /// <summary>Детали ребра по индексу из list_edges: тип, длина, смежные грани, концевые вершины.</summary>
    public Task<EdgeDetail> DescribeEdgeAsync(int index, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            var part = GetTopPart();
            try
            {
                var edges = Edges(part);
                var def = EdgeAt(edges, index);

                double len = 0; try { len = def.GetLength(MixMm); } catch { }
                string? f1 = null, f2 = null;
                try { if (def.GetAdjacentFace(true) is ksFaceDefinition a) f1 = GeomClassifiers.FaceType(a); } catch { }
                try { if (def.GetAdjacentFace(false) is ksFaceDefinition b) f2 = GeomClassifiers.FaceType(b); } catch { }

                return new EdgeDetail
                {
                    Index = index,
                    Type = GeomClassifiers.EdgeType(def),
                    Length = len,
                    AdjacentFace1 = f1,
                    AdjacentFace2 = f2,
                    Start = Vertex(def, true),
                    End = Vertex(def, false),
                };
            }
            finally { ComHelper.Release(part); }
        }, ct);

    /// <summary>
    /// Измерить расстояние и угол между двумя объектами модели. <paramref name="kind1"/>/<paramref name="kind2"/>
    /// — face|edge|vertex; индексы — из list_faces/list_edges.
    /// </summary>
    public Task<MeasureResult> MeasureAsync(string kind1, int index1, string kind2, int index2, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            var part = GetTopPart();
            try
            {
                var o1 = ResolveObject(part, kind1, index1);
                var o2 = ResolveObject(part, kind2, index2);

                var meas = part.GetMeasurer() as ksMeasurer
                    ?? throw new InvalidOperationException("GetMeasurer вернул null.");
                meas.SetObject1(o1);
                meas.SetObject2(o2);
                meas.unit = MixMm;
                if (!meas.Calc())
                    throw new InvalidOperationException("Измерение не выполнено (Calc вернул FALSE).");

                bool angleValid = false; try { angleValid = meas.IsAngleValid(); } catch { }
                return new MeasureResult
                {
                    Distance = meas.distance,
                    MinDistance = meas.MinDistance,
                    Angle = angleValid ? meas.angle : 0,
                    AngleValid = angleValid,
                };
            }
            finally { ComHelper.Release(part); }
        }, ct);

    // ── чтение данных (на STA-потоке) ─────────────────────────────────────

    private static IReadOnlyList<FeatureInfo> ReadFeatures(ksPart part)
    {
        // Дерево детали — через ksPart.GetFeature() (приведение part as ksFeature не срабатывает).
        var root = part.GetFeature() as ksFeature ?? part as ksFeature;
        if (root?.SubFeatureCollection(true, false) is not ksFeatureCollection tree) return Array.Empty<FeatureInfo>();

        int n = tree.GetCount();
        var list = new List<FeatureInfo>(n);
        for (int i = 0; i < n; i++)
        {
            if (tree.GetByIndex(i) is not ksFeature f) continue;
            string name = ""; try { name = f.name ?? ""; } catch { }
            var (kind, detail) = ClassifyFeature(f, name);
            list.Add(new FeatureInfo { Index = i, Name = name, Kind = kind, Detail = detail });
        }
        return list;
    }

    // ksFeature.type отдаёт лишь coarse o3d_entity → точный вид и параметры берём из определения.
    private static (string kind, string? detail) ClassifyFeature(ksFeature f, string name)
    {
        try
        {
            if (f.GetObject() is ksEntity ent)
                switch (ent.GetDefinition())
                {
                    case ksBossExtrusionDefinition be:
                        return ("extrusion", be.GetSideParam(true, out _, out double d1, out _, out _) ? Inv($"глубина {d1:F2} мм") : null);
                    case ksCutExtrusionDefinition ce:
                        return ("cut_extrusion", ce.GetSideParam(true, out _, out double d2, out _, out _) ? Inv($"вырез, глубина {d2:F2} мм") : "вырез");
                    case ksFilletDefinition fd:
                        return ("fillet", Inv($"R{fd.radius:F2} мм"));
                    case ksChamferDefinition cd:
                        return ("chamfer", cd.GetChamferParam(out _, out double s1, out double s2) ? Inv($"{s1:F2}×{s2:F2} мм") : null);
                    case ksBossRotatedDefinition:
                        return ("revolve", null);
                    case ksCutRotatedDefinition:
                        return ("cut_revolve", "вырез вращением");
                    case ksSketchDefinition:
                        return ("sketch", null);
                }
        }
        catch { /* параметры/тип best-effort */ }

        // Не распознанная операция: начало координат, импортированное тело либо иной узел-правка.
        return name.StartsWith("Начало", StringComparison.OrdinalIgnoreCase) ? ("origin", null) : ("other", null);
    }

    private static IReadOnlyList<BodyInfo> ReadBodies(ksPart part)
    {
        if (part.BodyCollection() is not ksBodyCollection bc) return Array.Empty<BodyInfo>();
        int n = bc.GetCount();
        var list = new List<BodyInfo>(n);
        for (int i = 0; i < n; i++)
        {
            if (bc.GetByIndex(i) is not ksBody b) continue;
            int faces = 0; try { if (b.FaceCollection() is ksFaceCollection fc) faces = fc.GetCount(); } catch { }
            bool solid = false; try { solid = b.IsSolid(); } catch { }
            list.Add(new BodyInfo { Index = i, IsSolid = solid, FaceCount = faces });
        }
        return list;
    }

    private static IReadOnlyList<VariableInfo> ReadVariables(ksPart part)
    {
        // Переменные детали живут в коллекции корневого ksFeature; ksPart.VariableCollection() даёт
        // только внешние. Берём feature-коллекцию (все переменные, включая созданные), с фолбэком.
        var vc = (part.GetFeature() as ksFeature)?.VariableCollection as ksVariableCollection
                 ?? part.VariableCollection() as ksVariableCollection;
        if (vc is null) return Array.Empty<VariableInfo>();
        int n = vc.GetCount();
        var list = new List<VariableInfo>(n);
        for (int i = 0; i < n; i++)
        {
            if (vc.GetByIndex(i) is not ksVariable v) continue;
            list.Add(new VariableInfo
            {
                Name = ComHelper.SafeStr(() => v.name),
                Expression = ComHelper.SafeStr(() => v.Expression),
                Value = ComHelper.SafeNum(() => v.value),
                External = ComHelper.SafeBool(() => v.external),
                Information = ComHelper.SafeBool(() => v.Information),
                Note = ComHelper.SafeStr(() => v.note),
            });
        }
        return list;
    }

    private static (List<string> faceTypes, List<string> edgeTypes) ReadTopology(ksPart part)
    {
        var faceTypes = new List<string>();
        var edgeTypes = new List<string>();
        if (Faces(part) is { } faces)
            for (int i = 0; i < faces.GetCount(); i++)
                if (faces.GetByIndex(i) is ksEntity e && e.GetDefinition() is ksFaceDefinition d)
                    faceTypes.Add(GeomClassifiers.FaceType(d));
        if (Edges(part) is { } edges)
            for (int i = 0; i < edges.GetCount(); i++)
                if (edges.GetByIndex(i) is ksEntity e && e.GetDefinition() is ksEdgeDefinition d)
                    edgeTypes.Add(GeomClassifiers.EdgeType(d));
        return (faceTypes, edgeTypes);
    }

    private static PartInfo? ReadMass(ksPart part)
    {
        try
        {
            if (part.CalcMassInertiaProperties(MixMmKg) is not ksMassInertiaParam m) return null;
            return new PartInfo
            {
                Volume = m.v, Mass = m.m, Area = m.F,
                CenterX = m.xc, CenterY = m.yc, CenterZ = m.zc,
            };
        }
        catch { return null; }
    }

    private static (double X, double Y, double Z)? FaceNormal(ksFaceDefinition def)
    {
        try
        {
            if (def.GetSurface() is not ksSurface surf) return null;
            double u = (surf.GetParamUMin() + surf.GetParamUMax()) / 2;
            double v = (surf.GetParamVMin() + surf.GetParamVMax()) / 2;
            if (!surf.GetNormal(u, v, out double nx, out double ny, out double nz)) return null;
            if (!def.normalOrientation) { nx = -nx; ny = -ny; nz = -nz; }
            return (nx, ny, nz);
        }
        catch { return null; }
    }

    private static (double X, double Y, double Z)? Vertex(ksEdgeDefinition def, bool start)
    {
        try
        {
            if (def.GetVertex(start) is ksVertexDefinition v && v.GetPoint(out double x, out double y, out double z))
                return (x, y, z);
        }
        catch { }
        return null;
    }

    private object ResolveObject(ksPart part, string kind, int index)
    {
        var type = kind.Trim().ToLowerInvariant() switch
        {
            "face" => Obj3dType.o3d_face,
            "edge" => Obj3dType.o3d_edge,
            "vertex" => Obj3dType.o3d_vertex,
            _ => throw new ArgumentException($"Неизвестный тип объекта '{kind}'. Допустимо: face, edge, vertex."),
        };
        var coll = part.EntityCollection((short)type) as ksEntityCollection
            ?? throw new InvalidOperationException($"Не удалось получить коллекцию {kind}.");
        if (index < 0 || index >= coll.GetCount())
            throw new ArgumentOutOfRangeException(nameof(index), $"Индекс {kind} вне диапазона [0; {coll.GetCount() - 1}].");
        return coll.GetByIndex(index) as ksEntity
            ?? throw new InvalidOperationException($"Объект {kind}[{index}] не приводится к ksEntity.");
    }

    // ── низкоуровневые помощники ──────────────────────────────────────────

    private ksPart GetTopPart()
    {
        var doc3d = _session.Kompas.ActiveDocument3D() as ksDocument3D
            ?? throw new InvalidOperationException("Нет активного 3D-документа.");
        // Освобождаем RCW документа сразу после получения ksPart — part имеет собственный ref.
        try
        {
            return doc3d.GetPart((short)Part_Type.pTop_Part) as ksPart
                ?? throw new InvalidOperationException("Не удалось получить вершинный компонент (ksPart).");
        }
        finally { ComHelper.Release(doc3d); }
    }

    /// <summary>Имя и локализованный тип активного документа. Отдельный API7-вызов от GetTopPart.</summary>
    private (string name, string typeName) ActiveDocInfo()
    {
        var doc = _session.Application.ActiveDocument;
        try
        {
            string name = ComHelper.SafeStr(() => doc?.Name);
            string typeName = "деталь";
            if (doc is not null)
            {
                try
                {
                    typeName = DocumentTypes.FromEnum(doc.DocumentType) switch
                    {
                        KompasDocumentType.Assembly => "сборка",
                        KompasDocumentType.Drawing => "чертёж",
                        KompasDocumentType.Fragment => "фрагмент",
                        KompasDocumentType.Specification => "спецификация",
                        KompasDocumentType.Text => "текст",
                        _ => "деталь",
                    };
                }
                catch { }
            }
            return (name, typeName);
        }
        catch { return ("", "деталь"); }
        finally { ComHelper.Release(doc); }
    }

    private static ksEntityCollection? Faces(ksPart part) => part.EntityCollection((short)Obj3dType.o3d_face) as ksEntityCollection;
    private static ksEntityCollection? Edges(ksPart part) => part.EntityCollection((short)Obj3dType.o3d_edge) as ksEntityCollection;

    private static ksFaceDefinition FaceAt(ksEntityCollection? faces, int index)
    {
        if (faces is null) throw new InvalidOperationException("Не удалось получить коллекцию граней.");
        if (index < 0 || index >= faces.GetCount())
            throw new ArgumentOutOfRangeException(nameof(index), $"Индекс грани вне диапазона [0; {faces.GetCount() - 1}]. Сверьтесь с list_faces.");
        return (faces.GetByIndex(index) as ksEntity)?.GetDefinition() as ksFaceDefinition
            ?? throw new InvalidOperationException($"Грань[{index}] не приводится к ksFaceDefinition.");
    }

    private static ksEdgeDefinition EdgeAt(ksEntityCollection? edges, int index)
    {
        if (edges is null) throw new InvalidOperationException("Не удалось получить коллекцию рёбер.");
        if (index < 0 || index >= edges.GetCount())
            throw new ArgumentOutOfRangeException(nameof(index), $"Индекс ребра вне диапазона [0; {edges.GetCount() - 1}]. Сверьтесь с list_edges.");
        return (edges.GetByIndex(index) as ksEntity)?.GetDefinition() as ksEdgeDefinition
            ?? throw new InvalidOperationException($"Ребро[{index}] не приводится к ksEdgeDefinition.");
    }

    private static BoundingBox Box(double x1, double y1, double z1, double x2, double y2, double z2) => new()
    {
        MinX = Math.Min(x1, x2), MinY = Math.Min(y1, y2), MinZ = Math.Min(z1, z2),
        MaxX = Math.Max(x1, x2), MaxY = Math.Max(y1, y2), MaxZ = Math.Max(z1, z2),
    };

    private static string Inv(FormattableString fs) => FormattableString.Invariant(fs);
}
