using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;

const int Asm = 2;
const int Silent = 1;
const int ReadOnly = 2;

if (args.Length == 0)
{
    Console.Error.WriteLine("StructuralFaultProbe requires one or more assembly paths.");
    return 2;
}

string outputDir = Path.Combine(System.Environment.CurrentDirectory, "runtime", "structural_fault_probe");
Directory.CreateDirectory(outputDir);

var assemblyPaths = args;
var sw = Connect();
var reports = new List<AssemblyReport>();

foreach (string rawPath in assemblyPaths)
{
    string asmPath = Path.GetFullPath(rawPath);
    if (!File.Exists(asmPath))
    {
        reports.Add(new AssemblyReport
        {
            AssemblyPath = asmPath,
            Ok = false,
            Message = "Assembly file does not exist."
        });
        continue;
    }

    reports.Add(AnalyzeAssembly(sw, asmPath));
}

var jsonOptions = new JsonSerializerOptions
{
    WriteIndented = true,
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
string jsonPath = Path.Combine(outputDir, "structural_fault_evidence.json");
File.WriteAllText(jsonPath, JsonSerializer.Serialize(reports, jsonOptions), new UTF8Encoding(false));

string mdPath = Path.Combine(outputDir, "structural_fault_evidence.md");
File.WriteAllText(mdPath, BuildMarkdown(reports), new UTF8Encoding(false));

Console.OutputEncoding = Encoding.UTF8;
Console.WriteLine("Structural fault probe completed.");
Console.WriteLine(jsonPath);
Console.WriteLine(mdPath);
return 0;

[DllImport("ole32.dll", CharSet = CharSet.Unicode)]
static extern int CLSIDFromProgID(string progId, out Guid clsid);

[DllImport("oleaut32.dll", PreserveSig = false)]
[return: MarshalAs(UnmanagedType.IUnknown)]
static extern object GetActiveObject(ref Guid clsid, IntPtr reserved);

static SldWorks Connect()
{
    try
    {
        int hr = CLSIDFromProgID("SldWorks.Application", out var clsid);
        if (hr < 0) Marshal.ThrowExceptionForHR(hr);
        return (SldWorks)GetActiveObject(ref clsid, IntPtr.Zero);
    }
    catch
    {
        var type = Type.GetTypeFromProgID("SldWorks.Application")
            ?? throw new InvalidOperationException("SolidWorks ProgID not found.");
        var sw = (SldWorks?)Activator.CreateInstance(type)
            ?? throw new InvalidOperationException("Cannot create SolidWorks.Application.");
        sw.Visible = true;
        return sw;
    }
}

static AssemblyReport AnalyzeAssembly(SldWorks sw, string asmPath)
{
    var report = new AssemblyReport
    {
        AssemblyPath = asmPath,
        AssemblyName = Path.GetFileNameWithoutExtension(asmPath),
        Ok = true
    };

    int errors = 0;
    int warnings = 0;
    var doc = sw.OpenDoc6(asmPath, Asm, Silent | ReadOnly, "", ref errors, ref warnings) as ModelDoc2;
    report.OpenErrors = errors;
    report.OpenWarnings = warnings;

    if (doc is not AssemblyDoc assy)
    {
        report.Ok = false;
        report.Message = $"OpenDoc6 failed or document is not an assembly. errors={errors}, warnings={warnings}";
        return report;
    }

    report.AssemblyTitle = Safe(() => doc.GetTitle(), "");

    dynamic mathUtility = sw.GetMathUtility();
    var components = EnumerateComponents(assy);
    report.ComponentCount = components.Count;

    foreach (var component in components)
    {
        component.Faces = EnumerateFaces(component, mathUtility);
        component.FaceCount = component.Faces.Count;
    }

    report.Components = components
        .Select(c => new ComponentSummary
        {
            ComponentName = c.Name,
            PartName = c.PartName,
            Category = c.Category,
            Path = c.Path,
            FaceCount = c.FaceCount,
            CylinderFaces = c.Faces
                .Where(f => f.SurfaceType == "cylinder" && f.RadiusMm.HasValue && f.Axis != null && f.AxisPointMm != null)
                .OrderByDescending(f => f.AreaMm2)
                .Take(32)
                .Select(FaceSummary.From)
                .ToList()
        })
        .OrderBy(c => c.Category)
        .ThenBy(c => c.ComponentName, StringComparer.OrdinalIgnoreCase)
        .ToList();

    var frames = components
        .Select(c => new { Component = c, Frame = TryBuildAxialFrame(c) })
        .Where(x => x.Frame != null)
        .ToDictionary(x => x.Component.Name, x => x.Frame!, StringComparer.OrdinalIgnoreCase);

    foreach (var component in components)
    {
        if (frames.TryGetValue(component.Name, out var frame))
            report.ComponentFixations.Add(AnalyzeComponentFixation(component, components, frame));
    }

    var bearings = components.Where(c => c.Category == "bearing").ToList();
    report.BearingCount = bearings.Count;

    foreach (var bearing in bearings)
    {
        frames.TryGetValue(bearing.Name, out var frame);
        report.Bearings.Add(AnalyzeBearingByFaceContact(bearing, components, frame));
    }

    report.Message = $"Analyzed {components.Count} components, {bearings.Count} bearing candidates.";
    return report;
}

static List<ComponentInfo> EnumerateComponents(AssemblyDoc assy)
{
    var result = new List<ComponentInfo>();
    foreach (object item in assy.GetComponents(false) as object[] ?? Array.Empty<object>())
    {
        if (item is not Component2 comp)
            continue;

        string name = Safe(() => comp.Name2, "");
        string path = Safe(() => comp.GetPathName(), "");
        string partName = string.IsNullOrWhiteSpace(path)
            ? StripInstanceSuffix(name)
            : Path.GetFileNameWithoutExtension(path);

        result.Add(new ComponentInfo
        {
            Component = comp,
            Name = name,
            Path = path,
            PartName = partName,
            Category = ClassifyComponent(name, partName, path)
        });
    }

    return result;
}

static string ClassifyComponent(string componentName, string partName, string path)
{
    string text = $"{componentName} {partName} {Path.GetFileName(path)}".ToLowerInvariant();
    if (ContainsAny(text, "轴承", "bearing", "gbt276", "gb276", "gb╱t", "7211", "620", "621", "630", "631", "600", "601"))
        return "bearing";
    if (ContainsAny(text, "端盖", "盖"))
        return "end_cap";
    if (ContainsAny(text, "箱体", "箱座", "箱盖", "housing"))
        return "housing";
    if (ContainsAny(text, "套筒", "隔套", "挡油环", "挡圈", "垫片", "螺母", "sleeve", "spacer", "ring"))
        return "spacer_or_ring";
    if (ContainsAny(text, "齿轮", "gear"))
        return "gear";
    if (ContainsAny(text, "轴", "shaft"))
        return "shaft";
    return "other";
}

static List<FaceInfo> EnumerateFaces(ComponentInfo component, dynamic mathUtility)
{
    var result = new List<FaceInfo>();
    var bodies = EnumerateComponentBodies(component.Component);
    int faceIndex = 0;

    foreach (var body in bodies)
    {
        for (object? faceObj = Safe<object?>(() => body.GetFirstFace(), null); faceObj != null;)
        {
            if (faceObj is not Face2 face)
                break;

            faceIndex++;
            var info = BuildFaceInfo(component, mathUtility, face, faceIndex);
            if (info != null)
                result.Add(info);

            faceObj = Safe<object?>(() => face.GetNextFace(), null);
        }
    }

    return result;
}

static List<Body2> EnumerateComponentBodies(Component2 component)
{
    var result = new List<Body2>();
    try
    {
        if (component.GetModelDoc2() is PartDoc partDoc)
        {
            object bodiesObj = partDoc.GetBodies2((int)swBodyType_e.swSolidBody, true);
            foreach (object bodyObj in ToObjectArray(bodiesObj))
                if (bodyObj is Body2 body)
                    result.Add(body);
        }
    }
    catch { }

    if (result.Count > 0)
        return result;

    try
    {
        object bodyInfo = null!;
        dynamic dynComponent = component;
        object bodiesObj = dynComponent.GetBodies3((int)swBodyType_e.swSolidBody, out bodyInfo);
        foreach (object bodyObj in ToObjectArray(bodiesObj))
            if (bodyObj is Body2 body)
                result.Add(body);
    }
    catch { }

    return result;
}

static FaceInfo? BuildFaceInfo(ComponentInfo component, dynamic mathUtility, Face2 face, int faceIndex)
{
    try
    {
        var surface = face.GetSurface() as Surface;
        if (surface == null)
            return null;

        var localBox = ToDoubleArray(face.GetBox()).Take(6).Select(v => v * 1000.0).ToArray();
        if (localBox.Length < 6)
            return null;
        var box = TransformBBox(mathUtility, component.Component, localBox);

        var info = new FaceInfo
        {
            ComponentName = component.Name,
            ComponentCategory = component.Category,
            PartName = component.PartName,
            FaceIndex = faceIndex,
            BBoxMm = box,
            CenterMm = TransformPoint(mathUtility, component.Component, [(localBox[0] + localBox[3]) / 2.0, (localBox[1] + localBox[4]) / 2.0, (localBox[2] + localBox[5]) / 2.0]),
            AreaMm2 = Safe(() => face.GetArea() * 1_000_000.0, 0.0)
        };

        AddLoopInfo(face, info);

        if (surface.IsPlane())
        {
            var p = ToDoubleArray(GetComProperty(surface, "PlaneParams"));
            if (p.Length < 6)
                return null;

            info.SurfaceType = "plane";
            info.Normal = Normalize(TransformVector(mathUtility, component.Component, [p[0], p[1], p[2]]));
            if (Safe(() => face.FaceInSurfaceSense(), true) == false)
                info.Normal = Scale(info.Normal, -1);
            info.RootPointMm = TransformPoint(mathUtility, component.Component, [p[3] * 1000.0, p[4] * 1000.0, p[5] * 1000.0]);
            info.PlaneOffsetMm = Dot(info.Normal, info.RootPointMm);
            AddCircularEdgeRadii(face, info);
        }
        else if (surface.IsCylinder())
        {
            var c = ToDoubleArray(GetComProperty(surface, "CylinderParams"));
            if (c.Length < 7)
                return null;

            info.SurfaceType = "cylinder";
            info.AxisPointMm = TransformPoint(mathUtility, component.Component, [c[0] * 1000.0, c[1] * 1000.0, c[2] * 1000.0]);
            info.Axis = Normalize(TransformVector(mathUtility, component.Component, [c[3], c[4], c[5]]));
            info.RadiusMm = c[6] * 1000.0;
            AddCylinderAxialRange(info);
        }
        else
        {
            info.SurfaceType = "other";
        }

        return info;
    }
    catch
    {
        return null;
    }
}

static BearingReport AnalyzeBearing(ComponentInfo bearing, List<ComponentInfo> components)
{
    var report = new BearingReport
    {
        ComponentName = bearing.Name,
        PartName = bearing.PartName,
        Path = bearing.Path,
        FaceCount = bearing.FaceCount
    };

    var cylinders = bearing.Faces
        .Where(f => f.SurfaceType == "cylinder" && f.RadiusMm.HasValue)
        .OrderByDescending(f => f.AreaMm2)
        .ToList();

    report.CylinderFaces = cylinders
        .Take(12)
        .Select(f => FaceSummary.From(f))
        .ToList();

    var axisCluster = SelectBearingAxisCluster(cylinders);
    if (axisCluster.Count == 0)
    {
        report.Status = "unknown";
        report.Message = "No usable bearing cylindrical faces found.";
        return report;
    }

    var outer = axisCluster.OrderByDescending(f => f.RadiusMm!.Value).First();
    var inner = axisCluster.OrderBy(f => f.RadiusMm!.Value).First();
    report.Axis = outer.Axis;
    report.AxisPointMm = outer.AxisPointMm;
    report.InnerRadiusMm = inner.RadiusMm;
    report.OuterRadiusMm = outer.RadiusMm;
    report.AxialMinMm = axisCluster.Min(f => f.AxialMinMm ?? Project(outer.AxisPointMm, outer));
    report.AxialMaxMm = axisCluster.Max(f => f.AxialMaxMm ?? Project(outer.AxisPointMm, outer));

    var planes = bearing.Faces
        .Where(f => f.SurfaceType == "plane")
        .Select(f => new
        {
            Face = f,
            Axial = Project(f.CenterMm, outer)
        })
        .Where(x => x.Axial >= report.AxialMinMm - 1.0 && x.Axial <= report.AxialMaxMm + 1.0)
        .ToList();

    report.EndFaces = planes
        .OrderBy(x => Math.Abs(x.Axial - report.AxialMinMm) < Math.Abs(x.Axial - report.AxialMaxMm) ? 0 : 1)
        .ThenByDescending(x => x.Face.AreaMm2)
        .Take(12)
        .Select(x => FaceSummary.From(x.Face))
        .ToList();

    var innerLeftCandidates = CollectAxialStopCandidates(bearing, components, outer, inner, report.AxialMinMm, -1, "inner");
    var innerRightCandidates = CollectAxialStopCandidates(bearing, components, outer, inner, report.AxialMaxMm, 1, "inner");
    var outerLeftCandidates = CollectAxialStopCandidates(bearing, components, outer, inner, report.AxialMinMm, -1, "outer");
    var outerRightCandidates = CollectAxialStopCandidates(bearing, components, outer, inner, report.AxialMaxMm, 1, "outer");

    report.StopCandidates.AddRange(innerLeftCandidates.Take(20));
    report.StopCandidates.AddRange(innerRightCandidates.Take(20));
    report.StopCandidates.AddRange(outerLeftCandidates.Take(20));
    report.StopCandidates.AddRange(outerRightCandidates.Take(20));

    report.InnerLeftStop = innerLeftCandidates.FirstOrDefault(c => c.Accepted);
    report.InnerRightStop = innerRightCandidates.FirstOrDefault(c => c.Accepted);
    report.OuterLeftStop = outerLeftCandidates.FirstOrDefault(c => c.Accepted);
    report.OuterRightStop = outerRightCandidates.FirstOrDefault(c => c.Accepted);

    report.RuleInterpretation = new RuleInterpretation
    {
        Principle = "One side inner ring fixed and the opposite side outer ring fixed is treated as correct for this experiment.",
        InnerLeft = report.InnerLeftStop != null,
        InnerRight = report.InnerRightStop != null,
        OuterLeft = report.OuterLeftStop != null,
        OuterRight = report.OuterRightStop != null
    };

    bool patternA = report.InnerLeftStop != null && report.OuterRightStop != null;
    bool patternB = report.InnerRightStop != null && report.OuterLeftStop != null;
    report.RuleInterpretation.PatternInnerLeftOuterRight = patternA;
    report.RuleInterpretation.PatternInnerRightOuterLeft = patternB;
    report.RuleInterpretation.ExperimentalPass = patternA || patternB;

    report.Status = report.RuleInterpretation.ExperimentalPass ? "pass" : "fail";
    report.Message = report.RuleInterpretation.ExperimentalPass
        ? "Detected opposite-side inner/outer axial stop evidence."
        : "Did not detect the required opposite-side inner/outer axial stop evidence.";

    return report;
}

static BearingReport AnalyzeBearingByFaceContact(ComponentInfo bearing, List<ComponentInfo> components, AxialFrame? frame)
{
    var report = new BearingReport
    {
        ComponentName = bearing.Name,
        PartName = bearing.PartName,
        Path = bearing.Path,
        FaceCount = bearing.FaceCount
    };

    if (frame == null)
    {
        report.Status = "unknown";
        report.Message = "No usable axial frame found for bearing.";
        return report;
    }

    report.Axis = frame.Axis;
    report.AxisPointMm = frame.AxisPointMm;
    report.AxialMinMm = frame.MinAxialMm;
    report.AxialMaxMm = frame.MaxAxialMm;
    report.InnerRadiusMm = frame.InnerRadiusMm;
    report.OuterRadiusMm = frame.OuterRadiusMm;
    report.CylinderFaces = bearing.Faces
        .Where(f => f.SurfaceType == "cylinder" && f.RadiusMm.HasValue)
        .OrderByDescending(f => f.AreaMm2)
        .Take(12)
        .Select(FaceSummary.From)
        .ToList();

    var contacts = CollectSideContacts(bearing, components, frame);
    report.SideContacts = contacts;

    var leftInner = contacts.Where(c => c.Accepted && c.Side == "left" && c.BearingRing == "inner").OrderBy(c => c.Score).FirstOrDefault();
    var leftOuter = contacts.Where(c => c.Accepted && c.Side == "left" && c.BearingRing == "outer").OrderBy(c => c.Score).FirstOrDefault();
    var rightInner = contacts.Where(c => c.Accepted && c.Side == "right" && c.BearingRing == "inner").OrderBy(c => c.Score).FirstOrDefault();
    var rightOuter = contacts.Where(c => c.Accepted && c.Side == "right" && c.BearingRing == "outer").OrderBy(c => c.Score).FirstOrDefault();

    report.InnerLeftStop = leftInner?.ToStopEvidence();
    report.InnerRightStop = rightInner?.ToStopEvidence();
    report.OuterLeftStop = leftOuter?.ToStopEvidence();
    report.OuterRightStop = rightOuter?.ToStopEvidence();

    bool bothSidesContact = contacts.Any(c => c.Side == "left" && c.Accepted) && contacts.Any(c => c.Side == "right" && c.Accepted);
    bool patternA = leftInner != null && rightOuter != null;
    bool patternB = rightInner != null && leftOuter != null;

    report.RuleInterpretation = new RuleInterpretation
    {
        Principle = "Bearing must have contacts on both axial sides; valid special bearing fixation requires one side on inner ring and the opposite side on outer ring.",
        InnerLeft = leftInner != null,
        InnerRight = rightInner != null,
        OuterLeft = leftOuter != null,
        OuterRight = rightOuter != null,
        PatternInnerLeftOuterRight = patternA,
        PatternInnerRightOuterLeft = patternB,
        ExperimentalPass = bothSidesContact && (patternA || patternB)
    };
    report.Status = report.RuleInterpretation.ExperimentalPass ? "pass" : "fail";
    report.Message = report.RuleInterpretation.ExperimentalPass
        ? "Both sides are contacted and opposite sides contact inner/outer rings."
        : "Bearing does not satisfy both-side contact plus opposite inner/outer ring contact.";

    return report;
}

static ComponentFixationReport AnalyzeComponentFixation(ComponentInfo target, List<ComponentInfo> components, AxialFrame frame)
{
    var contacts = CollectSideContacts(target, components, frame);
    bool left = contacts.Any(c => c.Side == "left" && c.Accepted);
    bool right = contacts.Any(c => c.Side == "right" && c.Accepted);
    return new ComponentFixationReport
    {
        ComponentName = target.Name,
        PartName = target.PartName,
        Category = target.Category,
        Axis = Util.Round(frame.Axis),
        AxisPointMm = Util.Round(frame.AxisPointMm),
        AxialMinMm = Util.Round(frame.MinAxialMm),
        AxialMaxMm = Util.Round(frame.MaxAxialMm),
        InnerRadiusMm = Util.Round(frame.InnerRadiusMm),
        OuterRadiusMm = Util.Round(frame.OuterRadiusMm),
        LeftFixed = left,
        RightFixed = right,
        FixedBothSides = left && right,
        Status = left && right ? "fixed_both_sides" : "not_fixed_both_sides",
        Contacts = contacts.Take(40).ToList()
    };
}

static AxialFrame? TryBuildAxialFrame(ComponentInfo component)
{
    var cylinders = component.Faces
        .Where(f => f.SurfaceType == "cylinder" && f.Axis != null && f.AxisPointMm != null && f.RadiusMm.HasValue)
        .OrderByDescending(f => f.AreaMm2)
        .ToList();

    if (cylinders.Count > 0)
    {
        var seed = cylinders.First();
        var cluster = cylinders
            .Where(c => AreParallel(c.Axis!, seed.Axis!, 0.98))
            .Where(c => DistancePointToLine(c.AxisPointMm!, seed.AxisPointMm!, seed.Axis!) < 1.0)
            .ToList();
        if (cluster.Count > 0)
        {
            double clusterMin = cluster.Min(f => f.AxialMinMm ?? Project(f.CenterMm, seed));
            double clusterMax = cluster.Max(f => f.AxialMaxMm ?? Project(f.CenterMm, seed));
            var radii = cluster.Select(c => c.RadiusMm!.Value).OrderBy(r => r).ToList();
            return new AxialFrame
            {
                Axis = seed.Axis!,
                AxisPointMm = seed.AxisPointMm!,
                MinAxialMm = clusterMin,
                MaxAxialMm = clusterMax,
                InnerRadiusMm = radii.First(),
                OuterRadiusMm = radii.Last()
            };
        }
    }

    var box = ComponentBBox(component);
    if (box == null)
        return null;

    double dx = box[3] - box[0];
    double dy = box[4] - box[1];
    double dz = box[5] - box[2];
    double[] axis;
    double min;
    double max;
    if (dx <= dy && dx <= dz)
    {
        axis = [1, 0, 0];
        min = box[0];
        max = box[3];
    }
    else if (dy <= dx && dy <= dz)
    {
        axis = [0, 1, 0];
        min = box[1];
        max = box[4];
    }
    else
    {
        axis = [0, 0, 1];
        min = box[2];
        max = box[5];
    }

    return new AxialFrame
    {
        Axis = axis,
        AxisPointMm = [(box[0] + box[3]) / 2.0, (box[1] + box[4]) / 2.0, (box[2] + box[5]) / 2.0],
        MinAxialMm = min,
        MaxAxialMm = max,
        InnerRadiusMm = 0,
        OuterRadiusMm = Math.Max(dx, Math.Max(dy, dz)) / 2.0
    };
}

static List<SideContactEvidence> CollectSideContacts(ComponentInfo target, List<ComponentInfo> components, AxialFrame frame)
{
    var result = new List<SideContactEvidence>();
    double sideToleranceMm = 1.5;
    double normalTolerance = 0.80;

    foreach (var other in components)
    {
        if (other.Name == target.Name)
            continue;

        foreach (var face in other.Faces.Where(f => f.SurfaceType == "plane" && f.Normal != null))
        {
            double axial = ProjectOnFrame(face.CenterMm, frame);
            string? side = null;
            double gap = 0;
            if (Math.Abs(axial - frame.MinAxialMm) <= sideToleranceMm)
            {
                side = "left";
                gap = Math.Abs(axial - frame.MinAxialMm);
            }
            else if (Math.Abs(axial - frame.MaxAxialMm) <= sideToleranceMm)
            {
                side = "right";
                gap = Math.Abs(axial - frame.MaxAxialMm);
            }
            else
            {
                continue;
            }

            double axisNormalAbs = Math.Abs(Dot(face.Normal!, frame.Axis));
            string ring = ClassifyBearingRingByCoverage(face, frame);
            var radialRange = FaceRadialRange(face, frame);
            bool overlap = RingCoverage(face, frame, ring);
            var reasons = new List<string>();
            if (axisNormalAbs < normalTolerance)
                reasons.Add($"plane is not perpendicular to axis enough: abs(dot)={Math.Round(axisNormalAbs, 4)}");
            if (!overlap)
                reasons.Add("face bbox does not overlap target radial envelope");

            result.Add(new SideContactEvidence
            {
                Side = side,
                ComponentName = other.Name,
                PartName = other.PartName,
                ComponentCategory = other.Category,
                FaceIndex = face.FaceIndex,
                CenterMm = Util.Round(face.CenterMm),
                Normal = Util.Round(face.Normal!),
                AxialPositionMm = Util.Round(axial),
                AxialGapMm = Util.Round(gap),
                AxisNormalAbs = Util.Round(axisNormalAbs),
                BearingRing = ring,
                FaceRadialMinMm = Util.Round(radialRange.Min),
                FaceRadialMaxMm = Util.Round(radialRange.Max),
                Accepted = reasons.Count == 0,
                RejectReasons = reasons,
                Score = Util.Round(gap * 10.0 + (1.0 - axisNormalAbs)),
                Reason = "plane at target axial side with radial overlap"
            });
        }
    }

    return result
        .OrderBy(c => c.Side)
        .ThenBy(c => c.Score)
        .ToList();
}

static string ClassifyBearingRingByCoverage(FaceInfo face, AxialFrame frame)
{
    var (minR, maxR) = FaceRadialRange(face, frame);
    double inner = frame.InnerRadiusMm;
    double outer = frame.OuterRadiusMm;

    bool coversInner = inner <= 0 || (minR <= inner + 3.0 && maxR >= Math.Max(0.0, inner - 3.0));
    bool coversOuter = maxR >= outer - 3.0 && minR <= outer + 3.0;
    if (coversInner && !coversOuter)
        return "inner";
    if (coversOuter && !coversInner)
        return "outer";
    if (coversInner && coversOuter)
        return "full";
    return "unknown";
}

static bool RingCoverage(FaceInfo face, AxialFrame frame, string ring)
{
    var (minR, maxR) = FaceRadialRange(face, frame);
    if (frame.OuterRadiusMm <= 0)
        return true;
    return maxR >= Math.Max(1.0, frame.InnerRadiusMm - 3.0) && minR <= frame.OuterRadiusMm + 3.0;
}

static (double Min, double Max) FaceRadialRange(FaceInfo face, AxialFrame frame)
{
    if (face.CircularEdgeRadiiMm.Count > 0)
    {
        var radii = face.CircularEdgeRadiiMm.OrderBy(r => r).ToList();
        double min = radii.Count == 1 ? 0.0 : radii.First();
        double max = radii.Last();
        return (min, max);
    }

    var radials = BBoxCorners(face.BBoxMm).Select(p => DistancePointToLine(p, frame.AxisPointMm, frame.Axis)).ToList();
    return (radials.Min(), radials.Max());
}

static double[]? ComponentBBox(ComponentInfo component)
{
    var boxes = component.Faces.Where(f => f.BBoxMm.Length >= 6).Select(f => f.BBoxMm).ToList();
    if (boxes.Count == 0)
        return null;
    return
    [
        boxes.Min(b => b[0]),
        boxes.Min(b => b[1]),
        boxes.Min(b => b[2]),
        boxes.Max(b => b[3]),
        boxes.Max(b => b[4]),
        boxes.Max(b => b[5])
    ];
}

static List<FaceInfo> SelectBearingAxisCluster(List<FaceInfo> cylinders)
{
    if (cylinders.Count == 0)
        return [];

    FaceInfo seed = cylinders.OrderByDescending(c => c.AreaMm2).First();
    return cylinders
        .Where(c => c.Axis != null && c.AxisPointMm != null && c.RadiusMm.HasValue)
        .Where(c => AreParallel(c.Axis!, seed.Axis!, 0.98))
        .Where(c => DistancePointToLine(c.AxisPointMm!, seed.AxisPointMm!, seed.Axis!) < 0.5)
        .ToList();
}

static List<StopEvidence> CollectAxialStopCandidates(
    ComponentInfo bearing,
    List<ComponentInfo> components,
    FaceInfo outer,
    FaceInfo inner,
    double axialEnd,
    int side,
    string ring)
{
    double innerRadius = inner.RadiusMm ?? 0.0;
    double outerRadius = outer.RadiusMm ?? 0.0;
    double targetRadius = ring == "inner" ? innerRadius : outerRadius;
    double radialTolerance = Math.Max(1.5, outerRadius * 0.08);
    double axialAcceptanceTolerance = 2.0;
    double axialDebugTolerance = 30.0;

    var candidates = new List<StopEvidence>();

    foreach (var component in components)
    {
        if (component.Name == bearing.Name)
            continue;
        if (component.Category is "bearing" or "other")
            continue;

        foreach (var face in component.Faces)
        {
            if (face.SurfaceType != "plane" || face.Normal == null)
                continue;

            double axial = Project(face.CenterMm, outer);
            double axialGap = Math.Abs(axial - axialEnd);
            if (axialGap > axialDebugTolerance)
                continue;

            double radial = DistancePointToLine(face.CenterMm, outer.AxisPointMm!, outer.Axis!);
            if (radial > outerRadius + 80.0)
                continue;

            var reasons = new List<string>();
            bool categoryOk = ring == "inner"
                ? component.Category is "shaft" or "spacer_or_ring" or "gear"
                : component.Category is "end_cap" or "housing" or "spacer_or_ring";
            if (!categoryOk)
                reasons.Add($"category {component.Category} is not valid for {ring} stop");

            if (axialGap > axialAcceptanceTolerance)
                reasons.Add($"axial gap {Math.Round(axialGap, 4)} mm exceeds {axialAcceptanceTolerance} mm");

            double axisNormalAbs = Math.Abs(Dot(face.Normal, outer.Axis!));
            if (axisNormalAbs < 0.85)
                reasons.Add($"plane normal is not axial enough, abs(dot)={Math.Round(axisNormalAbs, 4)}");

            if (!OverlapsProjectedBBox(face, outer, axialEnd, innerRadius, outerRadius, ring))
                reasons.Add("projected bbox does not cover the required ring region");

            double normalAlongAxis = Dot(face.Normal, outer.Axis!);
            double expectedSign = -side;
            double normalScore = Math.Abs(normalAlongAxis - expectedSign);

            var evidence = new StopEvidence
            {
                ComponentName = component.Name,
                PartName = component.PartName,
                ComponentCategory = component.Category,
                FaceIndex = face.FaceIndex,
                SurfaceType = face.SurfaceType,
                AreaMm2 = Util.Round(face.AreaMm2),
                CenterMm = Util.Round(face.CenterMm),
                Normal = Util.Round(face.Normal),
                AxialPositionMm = Util.Round(axial),
                AxialGapMm = Util.Round(Math.Abs(axial - axialEnd)),
                RadialDistanceMm = Util.Round(radial),
                Ring = ring,
                Side = side < 0 ? "left" : "right",
                Score = Util.Round(Math.Abs(axial - axialEnd) * 10.0 + normalScore + Math.Abs(radial - targetRadius) * (ring == "inner" ? 0.2 : 0.05)),
                Accepted = reasons.Count == 0,
                RejectReasons = reasons,
                AxisNormalAbs = Util.Round(axisNormalAbs),
                Reason = $"plane near bearing {ring} {(side < 0 ? "left" : "right")} end; component category={component.Category}"
            };
            candidates.Add(evidence);
        }
    }

    return candidates
        .OrderBy(c => c.Score)
        .ThenBy(c => c.AxialGapMm)
        .ToList();
}

static bool OverlapsProjectedBBox(FaceInfo face, FaceInfo bearingOuter, double axialEnd, double innerRadius, double outerRadius, string ring)
{
    var axis = bearingOuter.Axis!;
    var axisPoint = bearingOuter.AxisPointMm!;
    var corners = BBoxCorners(face.BBoxMm);
    var radials = corners.Select(c => DistancePointToLine(c, axisPoint, axis)).ToList();
    double minR = radials.Min();
    double maxR = radials.Max();
    if (ring == "inner")
        return minR <= innerRadius + 3.0 && maxR >= Math.Max(0.0, innerRadius - 3.0);
    return maxR >= innerRadius + 1.0 && minR <= outerRadius + 3.0;
}

static void AddLoopInfo(Face2 face, FaceInfo info)
{
    try
    {
        object loopsObj = face.GetLoops();
        var loops = ToObjectArray(loopsObj);
        info.LoopCount = loops.Length;
    }
    catch { }
}

static void AddCircularEdgeRadii(Face2 face, FaceInfo info)
{
    try
    {
        foreach (object edgeObj in ToObjectArray(face.GetEdges()))
        {
            if (edgeObj is not Edge edge)
                continue;

            var curve = edge.GetCurve() as Curve;
            if (curve == null || !Safe(() => curve.IsCircle(), false))
                continue;

            var cp = ToDoubleArray(GetComProperty(curve, "CircleParams"));
            if (cp.Length >= 7)
            {
                double radiusMm = Math.Abs(cp[6]) * 1000.0;
                if (radiusMm > 0.001 && !info.CircularEdgeRadiiMm.Any(r => Math.Abs(r - radiusMm) < 0.01))
                    info.CircularEdgeRadiiMm.Add(radiusMm);
            }
        }

        info.CircularEdgeRadiiMm.Sort();
    }
    catch { }
}

static void AddCylinderAxialRange(FaceInfo info)
{
    if (info.Axis == null || info.AxisPointMm == null || info.BBoxMm.Length < 6)
        return;

    var values = BBoxCorners(info.BBoxMm).Select(p => Project(p, info)).ToList();
    info.AxialMinMm = values.Min();
    info.AxialMaxMm = values.Max();
    info.AxialLengthMm = info.AxialMaxMm - info.AxialMinMm;
}

static double Project(double[] point, FaceInfo axisFace)
{
    return Dot(Sub(point, axisFace.AxisPointMm!), axisFace.Axis!);
}

static double ProjectOnFrame(double[] point, AxialFrame frame)
{
    return Dot(Sub(point, frame.AxisPointMm), frame.Axis);
}

static string BuildMarkdown(List<AssemblyReport> reports)
{
    var sb = new StringBuilder();
    sb.AppendLine("# Structural Fault Evidence");
    sb.AppendLine();
    sb.AppendLine("Experimental rule: one side inner ring fixed and the opposite side outer ring fixed is treated as correct.");
    sb.AppendLine();

    foreach (var report in reports)
    {
        sb.AppendLine($"## {report.AssemblyName}");
        sb.AppendLine();
        sb.AppendLine($"- Path: `{report.AssemblyPath}`");
        sb.AppendLine($"- Status: {(report.Ok ? "ok" : "failed")}");
        sb.AppendLine($"- Message: {report.Message}");
        sb.AppendLine($"- Components: {report.ComponentCount}, bearing candidates: {report.BearingCount}");
        sb.AppendLine();

        sb.AppendLine("### Component Side Fixation");
        sb.AppendLine();
        foreach (var fixation in report.ComponentFixations.OrderBy(f => f.Category).ThenBy(f => f.ComponentName, StringComparer.OrdinalIgnoreCase))
        {
            sb.AppendLine($"- `{fixation.Status}` {fixation.ComponentName}: left={fixation.LeftFixed}, right={fixation.RightFixed}");
            foreach (var contact in fixation.Contacts.Where(c => c.Accepted).Take(4))
                sb.AppendLine($"  - {contact.Side}: {contact.ComponentName} face#{contact.FaceIndex}, ring={contact.BearingRing}, gap={contact.AxialGapMm} mm");
        }
        sb.AppendLine();

        foreach (var bearing in report.Bearings)
        {
            sb.AppendLine($"### {bearing.ComponentName}");
            sb.AppendLine();
            sb.AppendLine($"- Diagnosis: `{bearing.Status}` - {bearing.Message}");
            sb.AppendLine($"- Inner radius: {Util.Round(bearing.InnerRadiusMm)} mm, outer radius: {Util.Round(bearing.OuterRadiusMm)} mm");
            sb.AppendLine($"- Axial range: {Util.Round(bearing.AxialMinMm)} to {Util.Round(bearing.AxialMaxMm)} mm");
            sb.AppendLine($"- Inner left stop: {FormatStop(bearing.InnerLeftStop)}");
            sb.AppendLine($"- Inner right stop: {FormatStop(bearing.InnerRightStop)}");
            sb.AppendLine($"- Outer left stop: {FormatStop(bearing.OuterLeftStop)}");
            sb.AppendLine($"- Outer right stop: {FormatStop(bearing.OuterRightStop)}");
            sb.AppendLine("- Accepted side contacts:");
            foreach (var contact in bearing.SideContacts.Where(c => c.Accepted).Take(10))
                sb.AppendLine($"  - {contact.Side}: {contact.ComponentName} face#{contact.FaceIndex}, ring={contact.BearingRing}, gap={contact.AxialGapMm} mm");
            sb.AppendLine();
        }
    }

    return sb.ToString();
}

static string FormatStop(StopEvidence? stop)
{
    if (stop == null)
        return "not detected";
    return $"{stop.ComponentName} face#{stop.FaceIndex}, category={stop.ComponentCategory}, gap={stop.AxialGapMm} mm, radial={stop.RadialDistanceMm} mm";
}

static bool ContainsAny(string text, params string[] needles)
{
    return needles.Any(n => text.Contains(n, StringComparison.OrdinalIgnoreCase));
}

static double[] TransformPoint(dynamic mathUtility, Component2 component, double[] pointMm)
{
    try
    {
        object pointObj = mathUtility.CreatePoint(new[] { pointMm[0] / 1000.0, pointMm[1] / 1000.0, pointMm[2] / 1000.0 });
        dynamic point = pointObj;
        object transformObj = component.Transform2;
        dynamic transformed = point.MultiplyTransform(transformObj);
        return ToDoubleArray(transformed.ArrayData).Take(3).Select(v => v * 1000.0).ToArray();
    }
    catch
    {
        return pointMm;
    }
}

static double[] TransformVector(dynamic mathUtility, Component2 component, double[] vector)
{
    try
    {
        object vectorObj = mathUtility.CreateVector(vector);
        dynamic mathVector = vectorObj;
        object transformObj = component.Transform2;
        dynamic transformed = mathVector.MultiplyTransform(transformObj);
        return Normalize(ToDoubleArray(transformed.ArrayData).Take(3).ToArray());
    }
    catch
    {
        return Normalize(vector);
    }
}

static double[] TransformBBox(dynamic mathUtility, Component2 component, double[] bboxMm)
{
    var corners = BBoxCorners(bboxMm).Select(p => TransformPoint(mathUtility, component, p)).ToList();
    return
    [
        corners.Min(p => p[0]),
        corners.Min(p => p[1]),
        corners.Min(p => p[2]),
        corners.Max(p => p[0]),
        corners.Max(p => p[1]),
        corners.Max(p => p[2])
    ];
}

static string StripInstanceSuffix(string value)
{
    int dash = value.LastIndexOf('-');
    return dash > 0 && int.TryParse(value[(dash + 1)..], out _) ? value[..dash] : value;
}

static object? GetComProperty(object obj, string propertyName)
{
    return obj.GetType().InvokeMember(propertyName, System.Reflection.BindingFlags.GetProperty, null, obj, null, CultureInfo.InvariantCulture);
}

static object[] ToObjectArray(object? value)
{
    if (value == null)
        return [];
    if (value is object[] objects)
        return objects;
    if (value is Array array)
    {
        var result = new object[array.Length];
        for (int i = 0; i < array.Length; i++)
            result[i] = array.GetValue(i)!;
        return result;
    }
    return [];
}

static double[] ToDoubleArray(object? value)
{
    if (value == null)
        return [];
    if (value is double[] doubles)
        return doubles;
    if (value is object[] objects)
        return objects.Select(o => Convert.ToDouble(o, CultureInfo.InvariantCulture)).ToArray();
    if (value is Array array)
    {
        var result = new double[array.Length];
        for (int i = 0; i < array.Length; i++)
            result[i] = Convert.ToDouble(array.GetValue(i), CultureInfo.InvariantCulture);
        return result;
    }
    return [];
}

static T Safe<T>(Func<T> fn, T fallback)
{
    try { return fn(); }
    catch { return fallback; }
}

static double Dot(double[] a, double[] b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
static double[] Sub(double[] a, double[] b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
static double[] Scale(double[] a, double s) => [a[0] * s, a[1] * s, a[2] * s];
static double Norm(double[] a) => Math.Sqrt(Dot(a, a));
static double[] Normalize(double[] a)
{
    double n = Norm(a);
    return n < 1e-12 ? [0, 0, 0] : [a[0] / n, a[1] / n, a[2] / n];
}

static bool AreParallel(double[] a, double[] b, double threshold) => Math.Abs(Dot(Normalize(a), Normalize(b))) >= threshold;
static double DistancePointToLine(double[] p, double[] linePoint, double[] lineDir)
{
    var d = Normalize(lineDir);
    var v = Sub(p, linePoint);
    var projected = Scale(d, Dot(v, d));
    return Norm(Sub(v, projected));
}

static List<double[]> BBoxCorners(double[] b)
{
    return
    [
        [b[0], b[1], b[2]], [b[0], b[1], b[5]], [b[0], b[4], b[2]], [b[0], b[4], b[5]],
        [b[3], b[1], b[2]], [b[3], b[1], b[5]], [b[3], b[4], b[2]], [b[3], b[4], b[5]]
    ];
}

sealed class ComponentInfo
{
    public required Component2 Component { get; init; }
    public string Name { get; init; } = "";
    public string PartName { get; init; } = "";
    public string Path { get; init; } = "";
    public string Category { get; init; } = "";
    public int FaceCount { get; set; }
    public List<FaceInfo> Faces { get; set; } = [];
}

sealed class FaceInfo
{
    public string ComponentName { get; set; } = "";
    public string ComponentCategory { get; set; } = "";
    public string PartName { get; set; } = "";
    public int FaceIndex { get; set; }
    public string SurfaceType { get; set; } = "";
    public double AreaMm2 { get; set; }
    public double[] BBoxMm { get; set; } = [];
    public double[] CenterMm { get; set; } = [];
    public int LoopCount { get; set; }
    public double[]? Normal { get; set; }
    public double[]? RootPointMm { get; set; }
    public double? PlaneOffsetMm { get; set; }
    public List<double> CircularEdgeRadiiMm { get; set; } = [];
    public double[]? AxisPointMm { get; set; }
    public double[]? Axis { get; set; }
    public double? RadiusMm { get; set; }
    public double? AxialMinMm { get; set; }
    public double? AxialMaxMm { get; set; }
    public double? AxialLengthMm { get; set; }
}

sealed class AssemblyReport
{
    public bool Ok { get; set; }
    public string Message { get; set; } = "";
    public string AssemblyName { get; set; } = "";
    public string AssemblyTitle { get; set; } = "";
    public string AssemblyPath { get; set; } = "";
    public int OpenErrors { get; set; }
    public int OpenWarnings { get; set; }
    public int ComponentCount { get; set; }
    public int BearingCount { get; set; }
    public List<ComponentSummary> Components { get; set; } = [];
    public List<ComponentFixationReport> ComponentFixations { get; set; } = [];
    public List<BearingReport> Bearings { get; set; } = [];
}

sealed class ComponentSummary
{
    public string ComponentName { get; set; } = "";
    public string PartName { get; set; } = "";
    public string Category { get; set; } = "";
    public string Path { get; set; } = "";
    public int FaceCount { get; set; }
    public List<FaceSummary> CylinderFaces { get; set; } = [];
}

sealed class BearingReport
{
    public string ComponentName { get; set; } = "";
    public string PartName { get; set; } = "";
    public string Path { get; set; } = "";
    public int FaceCount { get; set; }
    public string Status { get; set; } = "";
    public string Message { get; set; } = "";
    public double[]? Axis { get; set; }
    public double[]? AxisPointMm { get; set; }
    public double? InnerRadiusMm { get; set; }
    public double? OuterRadiusMm { get; set; }
    public double AxialMinMm { get; set; }
    public double AxialMaxMm { get; set; }
    public List<FaceSummary> CylinderFaces { get; set; } = [];
    public List<FaceSummary> EndFaces { get; set; } = [];
    public List<SideContactEvidence> SideContacts { get; set; } = [];
    public List<StopEvidence> StopCandidates { get; set; } = [];
    public StopEvidence? InnerLeftStop { get; set; }
    public StopEvidence? InnerRightStop { get; set; }
    public StopEvidence? OuterLeftStop { get; set; }
    public StopEvidence? OuterRightStop { get; set; }
    public RuleInterpretation? RuleInterpretation { get; set; }
}

sealed class AxialFrame
{
    public double[] Axis { get; set; } = [];
    public double[] AxisPointMm { get; set; } = [];
    public double MinAxialMm { get; set; }
    public double MaxAxialMm { get; set; }
    public double InnerRadiusMm { get; set; }
    public double OuterRadiusMm { get; set; }
}

sealed class ComponentFixationReport
{
    public string ComponentName { get; set; } = "";
    public string PartName { get; set; } = "";
    public string Category { get; set; } = "";
    public string Status { get; set; } = "";
    public double[] Axis { get; set; } = [];
    public double[] AxisPointMm { get; set; } = [];
    public double AxialMinMm { get; set; }
    public double AxialMaxMm { get; set; }
    public double? InnerRadiusMm { get; set; }
    public double? OuterRadiusMm { get; set; }
    public bool LeftFixed { get; set; }
    public bool RightFixed { get; set; }
    public bool FixedBothSides { get; set; }
    public List<SideContactEvidence> Contacts { get; set; } = [];
}

sealed class SideContactEvidence
{
    public string Side { get; set; } = "";
    public string ComponentName { get; set; } = "";
    public string PartName { get; set; } = "";
    public string ComponentCategory { get; set; } = "";
    public int FaceIndex { get; set; }
    public string BearingRing { get; set; } = "";
    public double FaceRadialMinMm { get; set; }
    public double FaceRadialMaxMm { get; set; }
    public double[] CenterMm { get; set; } = [];
    public double[] Normal { get; set; } = [];
    public double AxialPositionMm { get; set; }
    public double AxialGapMm { get; set; }
    public double AxisNormalAbs { get; set; }
    public double Score { get; set; }
    public bool Accepted { get; set; }
    public List<string> RejectReasons { get; set; } = [];
    public string Reason { get; set; } = "";

    public StopEvidence ToStopEvidence()
    {
        return new StopEvidence
        {
            ComponentName = ComponentName,
            PartName = PartName,
            ComponentCategory = ComponentCategory,
            FaceIndex = FaceIndex,
            SurfaceType = "plane",
            CenterMm = CenterMm,
            Normal = Normal,
            AxialPositionMm = AxialPositionMm,
            AxialGapMm = AxialGapMm,
            AxisNormalAbs = AxisNormalAbs,
            Ring = BearingRing,
            Side = Side,
            Score = Score,
            Accepted = Accepted,
            RejectReasons = RejectReasons,
            Reason = Reason
        };
    }
}

sealed class RuleInterpretation
{
    public string Principle { get; set; } = "";
    public bool InnerLeft { get; set; }
    public bool InnerRight { get; set; }
    public bool OuterLeft { get; set; }
    public bool OuterRight { get; set; }
    public bool PatternInnerLeftOuterRight { get; set; }
    public bool PatternInnerRightOuterLeft { get; set; }
    public bool ExperimentalPass { get; set; }
}

sealed class StopEvidence
{
    public string ComponentName { get; set; } = "";
    public string PartName { get; set; } = "";
    public string ComponentCategory { get; set; } = "";
    public int FaceIndex { get; set; }
    public string SurfaceType { get; set; } = "";
    public double AreaMm2 { get; set; }
    public double[] CenterMm { get; set; } = [];
    public double[] Normal { get; set; } = [];
    public double AxialPositionMm { get; set; }
    public double AxialGapMm { get; set; }
    public double RadialDistanceMm { get; set; }
    public double AxisNormalAbs { get; set; }
    public string Ring { get; set; } = "";
    public string Side { get; set; } = "";
    public double Score { get; set; }
    public bool Accepted { get; set; }
    public List<string> RejectReasons { get; set; } = [];
    public string Reason { get; set; } = "";
}

static class Util
{
    public static double? Round(double? value) => value.HasValue ? Math.Round(value.Value, 4) : null;
    public static double Round(double value) => Math.Round(value, 4);
    public static double[] Round(double[] value) => value.Select(v => Math.Round(v, 4)).ToArray();
}

sealed class FaceSummary
{
    public int FaceIndex { get; set; }
    public string SurfaceType { get; set; } = "";
    public double AreaMm2 { get; set; }
    public double[] CenterMm { get; set; } = [];
    public double[] BBoxMm { get; set; } = [];
    public int LoopCount { get; set; }
    public double? RadiusMm { get; set; }
    public double[]? AxisPointMm { get; set; }
    public double[]? Axis { get; set; }
    public double? AxialMinMm { get; set; }
    public double? AxialMaxMm { get; set; }

    public static FaceSummary From(FaceInfo face)
    {
        return new FaceSummary
        {
            FaceIndex = face.FaceIndex,
            SurfaceType = face.SurfaceType,
            AreaMm2 = Util.Round(face.AreaMm2),
            CenterMm = Util.Round(face.CenterMm),
            BBoxMm = Util.Round(face.BBoxMm),
            LoopCount = face.LoopCount,
            RadiusMm = Util.Round(face.RadiusMm),
            AxisPointMm = face.AxisPointMm == null ? null : Util.Round(face.AxisPointMm),
            Axis = face.Axis == null ? null : Util.Round(face.Axis),
            AxialMinMm = Util.Round(face.AxialMinMm),
            AxialMaxMm = Util.Round(face.AxialMaxMm)
        };
    }
}
