using SolidWorks.Interop.sldworks;

static class AssemblyModelProcessing
{
    public static List<ComponentHighlightImageRequest> BuildComponentHighlightImageRequests(SectionBrepReport report)
    {
        var componentsById = report.AssemblyComponents
            .ToDictionary(component => component.Id, StringComparer.OrdinalIgnoreCase);

        var designTargets = report.PartImagePlan
            .Where(item => !string.IsNullOrWhiteSpace(item.ComponentId))
            .Where(item => IsTopLevelAssemblyComponent(item.InstanceName))
            .GroupBy(item => item.ComponentId, StringComparer.OrdinalIgnoreCase)
            .Select(group => group.First())
            .Select(item => new ComponentHighlightImageRequest
            {
                PlanId = item.AssemblyContextImagePlanId,
                ComponentId = item.ComponentId,
                InstanceName = item.InstanceName,
                DisplayName = item.DisplayName,
                ComponentPath = item.ComponentPath,
                Views = ["best_oblique"],
                PreferredDirectionMm = componentsById.TryGetValue(item.ComponentId, out var component)
                    ? ChooseComponentHighlightDirection(component, report.AssemblyComponents)
                    : [],
                ShowComponents =
                [
                    BuildComponentImageRef(
                        componentsById.GetValueOrDefault(item.ComponentId),
                        item.InstanceName,
                        item.DisplayName,
                        item.ComponentPath)
                ]
            })
            .ToList();

        return designTargets;
    }

    static bool IsTopLevelAssemblyComponent(string instanceName)
    {
        return !string.IsNullOrWhiteSpace(instanceName) &&
               !instanceName.Contains("/", StringComparison.Ordinal) &&
               !instanceName.Contains("\\", StringComparison.Ordinal);
    }

    public static List<ComponentContextImageRequest> BuildComponentContextImageRequests(SectionBrepReport report)
    {
        var componentsById = report.AssemblyComponents
            .ToDictionary(component => component.Id, StringComparer.OrdinalIgnoreCase);

        return report.ComponentContextImagePlan
            .Where(item => ShouldExportComponentContext(item, report.ExternalInterfaceRoots))
            .Where(item => item.ShowComponents.Count > 0)
            .Select(item =>
            {
                var showComponents = item.ShowComponents
                    .Select(component => BuildComponentImageRef(component, componentsById))
                    .ToList();
                var preserveComponents = item.BoundaryComponents
                    .Select(component => BuildComponentImageRef(component, componentsById))
                    .ToList();
                var protectedIds = showComponents
                    .Concat(preserveComponents)
                    .Select(component => component.ComponentId)
                    .ToHashSet(StringComparer.OrdinalIgnoreCase);
                var hideComponents = item.MemberOcclusionAnalyses
                    .SelectMany(analysis => analysis.Directions)
                    .Where(direction => direction.Occluded)
                    .SelectMany(direction => direction.FirstHitComponents)
                    .Where(blocker => !protectedIds.Contains(blocker.ComponentId))
                    .DistinctBy(blocker => $"{blocker.InstanceName}::{blocker.ComponentPath}", StringComparer.OrdinalIgnoreCase)
                    .Select(blocker => new ComponentImageRef
                    {
                        ComponentId = blocker.ComponentId,
                        InstanceName = blocker.InstanceName,
                        DisplayName = blocker.DisplayName,
                        ComponentPath = blocker.ComponentPath,
                        BBoxMm = componentsById.TryGetValue(blocker.ComponentId, out var blockerSummary)
                            ? blockerSummary.BBoxMm
                            : []
                    })
                    .ToList();

                return new ComponentContextImageRequest
                {
                    PlanId = item.Id,
                    GroupId = item.GroupId,
                    TargetComponentId = item.TargetComponentId,
                    TargetInstanceName = item.TargetInstanceName,
                    TargetDisplayName = item.TargetDisplayName,
                    TargetComponentPath = item.TargetComponentPath,
                    Views = ["best_oblique"],
                    PreferredDirectionMm = componentsById.TryGetValue(item.TargetComponentId, out var component)
                        ? ChooseComponentHighlightDirection(component, report.AssemblyComponents)
                        : [],
                    ShowComponents = showComponents,
                    PreserveComponents = preserveComponents,
                    HideComponents = hideComponents
                };
            })
            .ToList();
    }

    static bool ShouldExportComponentContext(ComponentContextImagePlanItem item, IReadOnlyList<string> externalInterfaceRoots)
    {
        if (externalInterfaceRoots.Count == 0)
            return true;

        // Purchased-component hints are semantic hints, not a switch to disable
        // all component context images. But ordinary component-context images
        // should not expand the inside of a user-specified purchased module.
        // Those internal contacts are represented by physical-interface images
        // when they cross the purchased-module boundary.
        return !IsExternalInterfaceRootDescendant(item.TargetInstanceName, item.TargetComponentPath, externalInterfaceRoots);
    }

    static bool IsExternalInterfaceRootDescendant(string instanceName, string path, IReadOnlyList<string> roots)
    {
        var name = NormalizeRootMatchText(instanceName);
        var strippedName = NormalizeRootMatchText(StripInstanceSuffix(instanceName));
        var file = NormalizeRootMatchText(Path.GetFileNameWithoutExtension(path));
        foreach (var root in roots.Select(NormalizeRootMatchText).Where(root => root.Length > 0))
        {
            var exactRoot =
                name.Equals(root, StringComparison.OrdinalIgnoreCase) ||
                strippedName.Equals(root, StringComparison.OrdinalIgnoreCase) ||
                file.Equals(root, StringComparison.OrdinalIgnoreCase);
            if (exactRoot)
                return false;

            if (name.StartsWith(root + "-", StringComparison.OrdinalIgnoreCase) ||
                name.StartsWith(root + "/", StringComparison.OrdinalIgnoreCase) ||
                strippedName.StartsWith(root + "-", StringComparison.OrdinalIgnoreCase) ||
                strippedName.StartsWith(root + "/", StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }
        }

        return false;
    }

    public static List<InterfaceHighlightImageRequest> BuildInterfaceHighlightImageRequests(
        SectionBrepReport report,
        int maxRequests = 30,
        bool compactViews = false)
    {
        if (!report.DocumentKind.Equals("assembly", StringComparison.OrdinalIgnoreCase) ||
            report.AssemblyFaceContacts.Count == 0)
        {
            return [];
        }

        var components = report.AssemblyComponents;
        var groups = BuildPhysicalInterfaceGroups(report.AssemblyFaceContacts, report.ExternalInterfaceRoots)
            .OrderByDescending(group => group.AnalysisPriority)
            .ThenByDescending(group => group.Contacts.Max(contact => contact.Confidence))
            .Take(Math.Max(0, maxRequests))
            .ToList();
        var requests = new List<InterfaceHighlightImageRequest>();

        foreach (var group in groups)
        {
            var contact = group.Contacts[0];
            var componentA = FindComponentSummary(components, contact.ComponentA, contact.ComponentPathA);
            var componentB = FindComponentSummary(components, contact.ComponentB, contact.ComponentPathB);
            var refA = BuildComponentImageRef(componentA, contact.ComponentA, contact.ComponentDisplayNameA, contact.ComponentPathA);
            var refB = BuildComponentImageRef(componentB, contact.ComponentB, contact.ComponentDisplayNameB, contact.ComponentPathB);
            var axis = RepresentativeAxis(group.Contacts);
            var normal = RepresentativeNormal(group.Contacts);
            var interfaceBox = UnionBoxes(group.Contacts.SelectMany(item => new[] { item.BBoxA, item.BBoxB }));
            var isCylindrical = contact.ContactKind.Contains("cylindrical", StringComparison.OrdinalIgnoreCase);
            var verificationStatuses = group.Contacts
                .Select(item => item.VerificationStatus)
                .Where(status => !string.IsNullOrWhiteSpace(status))
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToList();

            requests.Add(new InterfaceHighlightImageRequest
            {
                PlanId = $"physical_interface_highlight_{SanitizeId(group.Id)}",
                InterfaceId = group.Id,
                ContactKind = contact.ContactKind,
                MateRole = contact.MateRole,
                ComponentA = refA,
                ComponentB = refB,
                FaceARef = group.FaceARefs[0],
                FaceBRef = group.FaceBRefs[0],
                FaceAIndex = group.FaceAIndices[0],
                FaceBIndex = group.FaceBIndices[0],
                FaceARefs = group.FaceARefs,
                FaceBRefs = group.FaceBRefs,
                FaceAIndices = group.FaceAIndices,
                FaceBIndices = group.FaceBIndices,
                RuntimeFaces = group.RuntimeFaces,
                SourceContactIds = group.Contacts.Select(item => item.Id).Distinct(StringComparer.OrdinalIgnoreCase).ToList(),
                Views = compactViews
                    ? ["assembled_oblique"]
                    : isCylindrical
                    ? ["assembled_oblique", "axis_end", "axis_side"]
                    : ["assembled_oblique", "normal_view"],
                PreferredDirectionMm = ChooseInterfaceHighlightDirection(contact, components),
                InterfaceAxisMm = axis,
                InterfaceNormalMm = normal,
                InterfaceCenterMm = AverageCenters(group.Contacts),
                InterfaceBBoxMm = interfaceBox,
                AnalysisPriority = group.AnalysisPriority,
                ConfidenceTier = group.ConfidenceTier,
                EvidencePurpose = "physical_interface_function_analysis",
                TrimmedPatchVerified = group.Contacts.All(item => item.TrimmedPatchVerified),
                VerificationStatus = verificationStatuses.Count == 1
                    ? verificationStatuses[0]
                    : "mixed_verified_interface",
                VerificationMethods = group.Contacts
                    .Select(item => item.VerificationMethod)
                    .Where(method => !string.IsNullOrWhiteSpace(method))
                    .Distinct(StringComparer.OrdinalIgnoreCase)
                    .ToList(),
                VerificationSampleHits = group.Contacts.Sum(item => item.VerificationSampleHits),
                VerificationMinDistanceMm = group.Contacts.Min(item => item.VerificationMinDistanceMm),
                DisplayMode = "actual_assembly_position_with_native_selected_interface_faces",
                ActualAssemblyPosition = true,
                NotActualClearance = true,
                ShowComponents = [refA, refB],
                PreserveComponents = []
            });
        }

        return requests;
    }

    static List<PhysicalInterfaceGroup> BuildPhysicalInterfaceGroups(
        IReadOnlyList<AssemblyFaceContactEvidence> contacts,
        IReadOnlyList<string> externalInterfaceRoots)
    {
        var planarProfilePairKeys = BuildPlanarProfilePairKeys(contacts);
        var groups = new List<PhysicalInterfaceGroup>();
        foreach (var contact in contacts
                     .Where(contact => contact.FaceA > 0 && contact.FaceB > 0)
                     .Where(IsConfirmedInterfaceContact)
                     .Where(contact => IsExternalPurchasedInterfaceContact(contact, externalInterfaceRoots))
                     .Where(contact => ShouldExportFitLikeInterface(contact, planarProfilePairKeys)))
        {
            var key = PhysicalInterfaceKey(contact);
            var group = groups.FirstOrDefault(candidate =>
                candidate.BaseKey.Equals(key, StringComparison.OrdinalIgnoreCase) &&
                candidate.Contacts.Any(existing => SamePhysicalInterfaceSegment(existing, contact)));
            if (group == null)
            {
                group = new PhysicalInterfaceGroup
                {
                    BaseKey = key,
                    Id = $"physical_interface_{groups.Count + 1:000}_{ShortInterfaceToken(contact.ComponentFileNameA, contact.ComponentA)}__{ShortInterfaceToken(contact.ComponentFileNameB, contact.ComponentB)}"
                };
                groups.Add(group);
            }

            // Keep every group oriented like its first contact so face-side identity remains stable.
            var first = group.Contacts.FirstOrDefault();
            var sameOrientation = first == null || SameComponentIdentity(first.ComponentA, first.ComponentPathA, contact.ComponentA, contact.ComponentPathA);
            group.Contacts.Add(contact);
            if (sameOrientation)
            {
                AddFace(group.FaceARefs, group.FaceAIndices, contact.ComponentA, contact.FaceA);
                AddFace(group.FaceBRefs, group.FaceBIndices, contact.ComponentB, contact.FaceB);
                AddRuntimeFace(group.RuntimeFaces, contact.RuntimeFaceA);
                AddRuntimeFace(group.RuntimeFaces, contact.RuntimeFaceB);
            }
            else
            {
                AddFace(group.FaceARefs, group.FaceAIndices, contact.ComponentB, contact.FaceB);
                AddFace(group.FaceBRefs, group.FaceBIndices, contact.ComponentA, contact.FaceA);
                AddRuntimeFace(group.RuntimeFaces, contact.RuntimeFaceB);
                AddRuntimeFace(group.RuntimeFaces, contact.RuntimeFaceA);
            }
        }

        foreach (var group in groups)
        {
            var text = string.Join(" ", group.Contacts.Select(contact =>
                $"{contact.ContactKind} {contact.MateRole} {contact.ComponentCategoryA} {contact.ComponentCategoryB} {contact.ComponentDisplayNameA} {contact.ComponentDisplayNameB}"));
            var containsKnownStandardPart = ContainsAny(text, "bearing", "轴承", "bolt", "screw", "nut", "pin", "key", "螺", "销", "键");
            var geometryConfidence = group.Contacts.Max(contact => contact.Confidence);
            group.AnalysisPriority = (containsKnownStandardPart ? 100 : 0)
                + (group.Contacts.Any(contact => contact.ContactKind.Contains("cylindrical", StringComparison.OrdinalIgnoreCase)) ? 30 : 10)
                + geometryConfidence * 20
                + Math.Min(10, group.Contacts.Count);
            group.ConfidenceTier = containsKnownStandardPart && geometryConfidence >= 0.75
                ? "anchor"
                : geometryConfidence >= 0.75 ? "high" : geometryConfidence >= 0.45 ? "medium" : "low";
        }

        return groups;
    }

    // Physical-interface images are boundary evidence only:
    // exactly one side must be a purchased/standard component and the other
    // side must be a top-level external non-purchased component. Internal
    // purchased-to-purchased or purchased-to-internal contacts remain in the
    // B-rep contact report but are not exported as physical-interface images.
    static bool IsExternalPurchasedInterfaceContact(AssemblyFaceContactEvidence contact, IReadOnlyList<string> externalInterfaceRoots)
    {
        if (externalInterfaceRoots.Count > 0)
        {
            var aIsRoot = IsExternalInterfaceRootMember(contact.ComponentA, contact.ComponentPathA, externalInterfaceRoots);
            var bIsRoot = IsExternalInterfaceRootMember(contact.ComponentB, contact.ComponentPathB, externalInterfaceRoots);
            return aIsRoot ^ bIsRoot;
        }

        var purchasedA = IsPurchasedInterfaceComponent(contact.ComponentCategoryA, contact.ComponentA, contact.ComponentPathA);
        var purchasedB = IsPurchasedInterfaceComponent(contact.ComponentCategoryB, contact.ComponentB, contact.ComponentPathB);
        if (purchasedA == purchasedB)
            return false;

        var externalOther = purchasedA
            ? contact.ComponentB
            : contact.ComponentA;
        var otherCategory = purchasedA
            ? contact.ComponentCategoryB
            : contact.ComponentCategoryA;
        var otherPath = purchasedA
            ? contact.ComponentPathB
            : contact.ComponentPathA;
        return !IsPurchasedInterfaceComponent(otherCategory, externalOther, otherPath) &&
               IsTopLevelAssemblyComponent(externalOther);
    }

    static bool IsExternalInterfaceRootMember(string instanceName, string path, IReadOnlyList<string> roots)
    {
        var name = NormalizeRootMatchText(instanceName);
        var strippedName = NormalizeRootMatchText(StripInstanceSuffix(instanceName));
        var file = NormalizeRootMatchText(Path.GetFileNameWithoutExtension(path));
        foreach (var root in roots.Select(NormalizeRootMatchText).Where(root => root.Length > 0))
        {
            if (name.Equals(root, StringComparison.OrdinalIgnoreCase) ||
                strippedName.Equals(root, StringComparison.OrdinalIgnoreCase) ||
                file.Equals(root, StringComparison.OrdinalIgnoreCase) ||
                name.StartsWith(root + "-", StringComparison.OrdinalIgnoreCase) ||
                name.StartsWith(root + "/", StringComparison.OrdinalIgnoreCase) ||
                strippedName.StartsWith(root + "-", StringComparison.OrdinalIgnoreCase) ||
                strippedName.StartsWith(root + "/", StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }
        }

        return false;
    }

    static string StripInstanceSuffix(string value) =>
        System.Text.RegularExpressions.Regex.Replace(value ?? "", @"-\d+(?=/|$)", "");

    static string NormalizeRootMatchText(string value) =>
        (value ?? "").Trim().Replace('\\', '/');

    static bool IsPurchasedInterfaceComponent(string category, string instanceName, string path)
    {
        if (category.Equals("bearing", StringComparison.OrdinalIgnoreCase) ||
            category.Equals("purchased_motor", StringComparison.OrdinalIgnoreCase) ||
            category.Equals("purchased_reducer", StringComparison.OrdinalIgnoreCase) ||
            category.Equals("purchased_motor_reducer_assembly", StringComparison.OrdinalIgnoreCase) ||
            category.Equals("user_purchased_component", StringComparison.OrdinalIgnoreCase))
        {
            return true;
        }

        var text = $"{category} {instanceName} {path}".ToLowerInvariant();
        if (LooksLikeDesignedConnectorOrHousing(text))
            return false;

        return ContainsAny(
            text,
            "gb", "t70", "t276", "bearing", "bolt", "screw",
            "csg", "shg", "mhc", "ec60", "purchased_motor", "purchased_reducer", "gearbox",
            "motor", "servo", "reducer", "gear motor",
            "\u8f74\u627f", "\u87ba\u9489", "\u87ba\u6813", "\u7535\u673a", "\u9a6c\u8fbe", "\u4f3a\u670d", "\u51cf\u901f\u5668", "\u51cf\u901f\u673a");
    }

    static bool LooksLikeDesignedConnectorOrHousing(string text)
    {
        return ContainsAny(
            text,
            "\u8fde\u63a5\u4ef6", "\u6cd5\u5170", "\u5916\u58f3", "\u58f3\u4f53", "\u652f\u67b6", "\u5ea7",
            "connector", "adapter", "bracket", "flange", "housing", "mount");
    }

    static bool IsConfirmedInterfaceContact(AssemblyFaceContactEvidence contact) =>
        contact.TrimmedPatchVerified &&
        (contact.VerificationStatus.Equals("trimmed_interface_confirmed", StringComparison.OrdinalIgnoreCase) ||
         contact.VerificationStatus.Equals("trimmed_cylindrical_domain_confirmed", StringComparison.OrdinalIgnoreCase) ||
         contact.VerificationStatus.Equals("analytic_cylindrical_domain_confirmed", StringComparison.OrdinalIgnoreCase));

    static string ShortInterfaceToken(string fileName, string componentName)
    {
        var token = SanitizeId(FirstNonEmpty(Path.GetFileNameWithoutExtension(fileName), componentName, "component"));
        return token.Length <= 24 ? token : token[..24];
    }

    static bool SamePhysicalInterfaceSegment(AssemblyFaceContactEvidence a, AssemblyFaceContactEvidence b)
    {
        if (!a.ContactKind.Contains("cylindrical", StringComparison.OrdinalIgnoreCase) ||
            !b.ContactKind.Contains("cylindrical", StringComparison.OrdinalIgnoreCase))
        {
            return true;
        }

        var axis = a.AxisA.Length >= 3 ? Vec.Normalize(a.AxisA) : Vec.Normalize(a.AxisB);
        if (axis.Length < 3)
            return true;
        var intervalA = ContactAxialInterval(a, axis);
        var intervalB = ContactAxialInterval(b, axis);
        if (!intervalA.HasValue || !intervalB.HasValue)
            return true;
        var radius = new[] { a.RadiusAmm, a.RadiusBmm, b.RadiusAmm, b.RadiusBmm }.Where(value => value > 0).DefaultIfEmpty(1).Average();
        var allowedGap = Math.Max(0.5, radius * 0.03);
        return intervalA.Value.Min <= intervalB.Value.Max + allowedGap &&
               intervalB.Value.Min <= intervalA.Value.Max + allowedGap;
    }

    static (double Min, double Max)? ContactAxialInterval(AssemblyFaceContactEvidence contact, double[] axis)
    {
        var boxes = new[] { contact.BBoxA, contact.BBoxB }.Where(box => box.Length >= 6).ToList();
        if (boxes.Count == 0)
            return null;
        var values = boxes.SelectMany(BoxCorners).Select(point => Vec.Dot(point, axis)).ToList();
        return (values.Min(), values.Max());
    }

    static string PhysicalInterfaceKey(AssemblyFaceContactEvidence contact)
    {
        var pair = ComponentPairKey(
            FirstNonEmpty(contact.ComponentPathA, contact.ComponentA),
            FirstNonEmpty(contact.ComponentPathB, contact.ComponentB));
        if (!contact.ContactKind.Contains("cylindrical", StringComparison.OrdinalIgnoreCase))
            return $"{pair}|planar_profile";

        var axis = contact.AxisA.Length >= 3 ? Vec.Normalize(contact.AxisA) : Vec.Normalize(contact.AxisB);
        axis = CanonicalDirection(axis);
        var radius = new[] { contact.RadiusAmm, contact.RadiusBmm }.Where(value => value > 0).DefaultIfEmpty(0).Average();
        var center = AverageVectors(contact.CenterA, contact.CenterB);
        var axisOffset = axis.Length >= 3 && center.Length >= 3
            ? Vec.Sub(center, Vec.Mul(axis, Vec.Dot(center, axis)))
            : [];
        return string.Join("|", new[]
        {
            pair,
            "cylindrical",
            VectorKey(axis, 0.02),
            VectorKey(axisOffset, Math.Max(0.1, radius * 0.01)),
            $"r{Math.Round(radius, 2):0.00}"
        });
    }

    static void AddFace(List<string> refs, List<int> indices, string component, int faceIndex)
    {
        if (faceIndex <= 0)
            return;
        var faceRef = $"{component}:face#{faceIndex}";
        if (refs.Contains(faceRef, StringComparer.OrdinalIgnoreCase))
            return;
        refs.Add(faceRef);
        indices.Add(faceIndex);
    }

    static void AddRuntimeFace(List<Face2> faces, Face2? face)
    {
        if (face != null && !faces.Any(existing => ReferenceEquals(existing, face)))
            faces.Add(face);
    }

    static bool SameComponentIdentity(string nameA, string pathA, string nameB, string pathB) =>
        !string.IsNullOrWhiteSpace(pathA) && !string.IsNullOrWhiteSpace(pathB)
            ? pathA.Equals(pathB, StringComparison.OrdinalIgnoreCase)
            : nameA.Equals(nameB, StringComparison.OrdinalIgnoreCase);

    static double[] RepresentativeAxis(IReadOnlyList<AssemblyFaceContactEvidence> contacts) =>
        contacts.Select(contact => contact.AxisA.Length >= 3 ? contact.AxisA : contact.AxisB)
            .FirstOrDefault(axis => axis.Length >= 3) is { Length: >= 3 } axis ? CanonicalDirection(Vec.Normalize(axis)) : [];

    static double[] RepresentativeNormal(IReadOnlyList<AssemblyFaceContactEvidence> contacts) =>
        contacts.Select(contact => contact.NormalA.Length >= 3 ? contact.NormalA : contact.NormalB)
            .FirstOrDefault(normal => normal.Length >= 3) is { Length: >= 3 } normal ? CanonicalDirection(Vec.Normalize(normal)) : [];

    static double[] AverageCenters(IReadOnlyList<AssemblyFaceContactEvidence> contacts)
    {
        var centers = contacts.Select(contact => AverageVectors(contact.CenterA, contact.CenterB)).Where(center => center.Length >= 3).ToList();
        return centers.Count == 0
            ? []
            : [centers.Average(center => center[0]), centers.Average(center => center[1]), centers.Average(center => center[2])];
    }

    static double[] CanonicalDirection(double[] direction)
    {
        if (direction.Length < 3)
            return [];
        var first = direction.FirstOrDefault(value => Math.Abs(value) > 1e-6);
        return first < 0 ? Vec.Mul(direction, -1) : direction;
    }

    static string VectorKey(double[] vector, double tolerance)
    {
        if (vector.Length < 3)
            return "none";
        tolerance = Math.Max(tolerance, 1e-6);
        return string.Join(",", vector.Take(3).Select(value => Math.Round(value / tolerance).ToString(System.Globalization.CultureInfo.InvariantCulture)));
    }

    static HashSet<string> BuildPlanarProfilePairKeys(IReadOnlyList<AssemblyFaceContactEvidence> contacts)
    {
        var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (var group in contacts
                     .Where(contact => contact.ContactKind.Equals("planar_face_contact_candidate", StringComparison.OrdinalIgnoreCase))
                     .GroupBy(contact => ComponentPairKey(contact.ComponentA, contact.ComponentB), StringComparer.OrdinalIgnoreCase))
        {
            var items = group.ToList();
            if (items.Count < 2)
                continue;

            var normalFamilies = new List<double[]>();
            foreach (var contact in items)
            {
                var normal = contact.NormalA.Length >= 3 ? Vec.Normalize(contact.NormalA) : [];
                if (normal.Length < 3)
                    continue;

                var existing = normalFamilies.Any(family => Math.Abs(Vec.Dot(family, normal)) >= 0.92);
                if (!existing)
                    normalFamilies.Add(normal);
            }

            // Multi-direction planar contacts indicate prismatic/profile fits such as square shafts,
            // rectangular bosses, slots, or keyed interfaces. Same-direction multi-face groups are
            // kept as weaker profile candidates; ordinary single flat mounting contacts are excluded.
            if (normalFamilies.Count >= 2 || items.Count >= 3)
                result.Add(group.Key);
        }

        return result;
    }

    static bool ShouldExportFitLikeInterface(AssemblyFaceContactEvidence contact, IReadOnlySet<string> planarProfilePairKeys)
    {
        if (contact.ContactKind.Equals("cylindrical_coaxial_fit_candidate", StringComparison.OrdinalIgnoreCase))
            return true;

        if (!contact.ContactKind.Equals("planar_face_contact_candidate", StringComparison.OrdinalIgnoreCase))
            return false;

        return planarProfilePairKeys.Contains(ComponentPairKey(contact.ComponentA, contact.ComponentB));
    }

    static ComponentImageRef BuildComponentImageRef(
        ComponentContextMember component,
        IReadOnlyDictionary<string, AssemblyComponentSummary> componentsById)
    {
        return new ComponentImageRef
        {
            ComponentId = component.ComponentId,
            InstanceName = component.InstanceName,
            DisplayName = component.DisplayName,
            ComponentPath = component.ComponentPath,
            BBoxMm = componentsById.TryGetValue(component.ComponentId, out var summary)
                ? summary.BBoxMm
                : []
        };
    }

    static ComponentImageRef BuildComponentImageRef(
        AssemblyComponentSummary? component,
        string instanceName,
        string displayName,
        string componentPath)
    {
        return new ComponentImageRef
        {
            ComponentId = component?.Id ?? SanitizeId(FirstNonEmpty(instanceName, displayName, Path.GetFileNameWithoutExtension(componentPath), "component")),
            InstanceName = component?.InstanceName ?? instanceName,
            DisplayName = component?.DisplayName ?? displayName,
            ComponentPath = component?.Path ?? componentPath,
            BBoxMm = component?.BBoxMm ?? []
        };
    }

    static AssemblyComponentSummary? FindComponentSummary(
        IReadOnlyList<AssemblyComponentSummary> components,
        string instanceName,
        string componentPath)
    {
        return components.FirstOrDefault(component =>
                   !string.IsNullOrWhiteSpace(instanceName) &&
                   component.InstanceName.Equals(instanceName, StringComparison.OrdinalIgnoreCase) &&
                   (string.IsNullOrWhiteSpace(componentPath) ||
                    string.IsNullOrWhiteSpace(component.Path) ||
                    component.Path.Equals(componentPath, StringComparison.OrdinalIgnoreCase)))
               ?? components.FirstOrDefault(component =>
                   !string.IsNullOrWhiteSpace(componentPath) &&
                   !string.IsNullOrWhiteSpace(component.Path) &&
                   component.Path.Equals(componentPath, StringComparison.OrdinalIgnoreCase));
    }

    static double ContactPriority(AssemblyFaceContactEvidence contact)
    {
        var text = $"{contact.ContactKind} {contact.MateRole} {contact.FaceRoleA} {contact.FaceRoleB}".ToLowerInvariant();
        if (text.Contains("cylindrical") || text.Contains("bearing") || text.Contains("hinge") || text.Contains("fit"))
            return 3.0;
        if (text.Contains("planar") || text.Contains("profile") || text.Contains("slot") || text.Contains("key"))
            return 1.0;
        return 0.0;
    }

    static string ComponentPairKey(string a, string b) =>
        string.Compare(a, b, StringComparison.OrdinalIgnoreCase) <= 0
            ? $"{a}||{b}"
            : $"{b}||{a}";

    static double[] ChooseInterfaceHighlightDirection(
        AssemblyFaceContactEvidence contact,
        IReadOnlyList<AssemblyComponentSummary> components)
    {
        var center = AverageVectors(contact.CenterA, contact.CenterB);
        var assemblyBox = UnionBoxes(components.Select(component => component.BBoxMm));
        var outward = Array.Empty<double>();
        if (center.Length >= 3 && assemblyBox.Length >= 6)
        {
            var away = Vec.Sub(center, BoxCenter(assemblyBox));
            if (Vec.Norm(away) > 1.0)
                outward = Vec.Normalize(away);
        }

        var axis = contact.AxisA.Length >= 3 ? Vec.Normalize(contact.AxisA) :
            contact.AxisB.Length >= 3 ? Vec.Normalize(contact.AxisB) : [];
        var normal = contact.NormalA.Length >= 3 ? contact.NormalA : contact.NormalB;
        normal = normal.Length >= 3 ? Vec.Normalize(normal) : [];
        var isCylindrical = contact.ContactKind.Contains("cylindrical", StringComparison.OrdinalIgnoreCase);
        var componentA = FindComponentSummary(components, contact.ComponentA, contact.ComponentPathA);
        var componentB = FindComponentSummary(components, contact.ComponentB, contact.ComponentPathB);
        var boxA = componentA?.BBoxMm is { Length: >= 6 } ? componentA.BBoxMm : contact.BBoxA;
        var boxB = componentB?.BBoxMm is { Length: >= 6 } ? componentB.BBoxMm : contact.BBoxB;

        return InterfaceObliqueCandidateDirections()
            .OrderByDescending(direction => ScoreInterfaceViewDirection(direction, boxA, boxB, outward, axis, normal, isCylindrical))
            .First();
    }

    static double ScoreInterfaceViewDirection(double[] direction, double[] boxA, double[] boxB, double[] outward, double[] axis, double[] normal, bool isCylindrical)
    {
        var visibilityScore = ScorePairProjectedVisibility(boxA, boxB, direction);

        var geometryPreference = 0.0;
        if (outward.Length >= 3)
            geometryPreference += 0.8 * Math.Max(0.0, Vec.Dot(direction, outward));

        if (isCylindrical && axis.Length >= 3)
            geometryPreference += 0.7 * (1.0 - Math.Abs(Vec.Dot(direction, axis)));

        if (!isCylindrical && normal.Length >= 3)
            geometryPreference += 0.7 * Math.Abs(Vec.Dot(direction, normal));

        return visibilityScore * (1.0 + geometryPreference);
    }

    static double ScorePairProjectedVisibility(double[] boxA, double[] boxB, double[] direction)
    {
        if (boxA.Length < 6 || boxB.Length < 6)
            return 1.0;

        var projectionA = ProjectBoxForInterface(boxA, direction);
        var projectionB = ProjectBoxForInterface(boxB, direction);
        var areaA = projectionA.Area;
        var areaB = projectionB.Area;
        var overlap = OverlapArea(projectionA, projectionB);
        var unionArea = Math.Max(1.0, areaA + areaB - overlap);
        var smallerArea = Math.Max(1.0, Math.Min(areaA, areaB));
        var largerArea = Math.Max(smallerArea, Math.Max(areaA, areaB));
        var overlapRatio = Math.Min(1.0, overlap / smallerArea);
        var balance = smallerArea / largerArea;

        // Contact faces are usually hidden; favor views where both components expose
        // enough surrounding exterior area instead of one box covering the other.
        return unionArea * (1.0 - overlapRatio * 0.85) * (0.75 + balance * 0.25);
    }

    static InterfaceProjectionRect ProjectBoxForInterface(double[] box, double[] direction)
    {
        var d = Vec.Normalize(direction);
        var reference = Math.Abs(Vec.Dot(d, [0.0, 1.0, 0.0])) > 0.92
            ? new[] { 0.0, 0.0, 1.0 }
            : [0.0, 1.0, 0.0];
        var u = Vec.Normalize(Vec.Cross(reference, d));
        var v = Vec.Normalize(Vec.Cross(d, u));
        var corners = BoxCorners(box);
        var uRange = ProjectionRange(corners, u);
        var vRange = ProjectionRange(corners, v);
        return new InterfaceProjectionRect(uRange.Min, uRange.Max, vRange.Min, vRange.Max);
    }

    static double OverlapArea(InterfaceProjectionRect a, InterfaceProjectionRect b)
    {
        var u = Math.Max(0.0, Math.Min(a.UMax, b.UMax) - Math.Max(a.UMin, b.UMin));
        var v = Math.Max(0.0, Math.Min(a.VMax, b.VMax) - Math.Max(a.VMin, b.VMin));
        return u * v;
    }

    static List<double[]> BoxCorners(double[] box) =>
    [
        [box[0], box[1], box[2]],
        [box[0], box[1], box[5]],
        [box[0], box[4], box[2]],
        [box[0], box[4], box[5]],
        [box[3], box[1], box[2]],
        [box[3], box[1], box[5]],
        [box[3], box[4], box[2]],
        [box[3], box[4], box[5]]
    ];

    static (double Min, double Max) ProjectionRange(IReadOnlyList<double[]> points, double[] axis)
    {
        var values = points.Select(point => Vec.Dot(point, axis)).ToList();
        return (values.Min(), values.Max());
    }

    static List<double[]> InterfaceObliqueCandidateDirections() =>
    [
        Vec.Normalize([1.0, 1.0, 1.0]),
        Vec.Normalize([1.0, 1.0, -1.0]),
        Vec.Normalize([1.0, -1.0, 1.0]),
        Vec.Normalize([1.0, -1.0, -1.0]),
        Vec.Normalize([-1.0, 1.0, 1.0]),
        Vec.Normalize([-1.0, 1.0, -1.0]),
        Vec.Normalize([-1.0, -1.0, 1.0]),
        Vec.Normalize([-1.0, -1.0, -1.0])
    ];

    static double[] AverageVectors(double[] a, double[] b)
    {
        if (a.Length < 3 && b.Length < 3)
            return [];
        if (a.Length < 3)
            return b.Take(3).ToArray();
        if (b.Length < 3)
            return a.Take(3).ToArray();
        return [(a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0, (a[2] + b[2]) / 2.0];
    }

    static bool ContainsAny(string value, params string[] tokens) =>
        tokens.Any(token => value.Contains(token, StringComparison.OrdinalIgnoreCase));

    static double[] ChooseComponentHighlightDirection(
        AssemblyComponentSummary component,
        IReadOnlyList<AssemblyComponentSummary> allComponents)
    {
        if (component.BBoxMm.Length < 6)
            return [1.0, 1.0, 1.0];

        var assemblyBox = UnionBoxes(allComponents.Select(item => item.BBoxMm));
        if (assemblyBox.Length < 6)
            return [1.0, 1.0, 1.0];

        var componentCenter = BoxCenter(component.BBoxMm);
        var assemblyCenter = BoxCenter(assemblyBox);
        var delta = componentCenter.Zip(assemblyCenter, (a, b) => a - b).ToArray();

        double Axis(double value, int index)
        {
            if (Math.Abs(value) > 1.0)
                return Math.Sign(value);

            var componentSpan = Math.Abs(component.BBoxMm[index + 3] - component.BBoxMm[index]);
            var assemblySpan = Math.Abs(assemblyBox[index + 3] - assemblyBox[index]);
            return componentSpan >= assemblySpan * 0.45 ? 1.0 : (index == 1 ? 1.0 : -1.0);
        }

        return Vec.Normalize([Axis(delta[0], 0), Axis(delta[1], 1), Axis(delta[2], 2)]);
    }

    static double[] UnionBoxes(IEnumerable<double[]> boxes)
    {
        var valid = boxes.Where(box => box.Length >= 6).ToList();
        if (valid.Count == 0)
            return [];

        return
        [
            valid.Min(box => box[0]),
            valid.Min(box => box[1]),
            valid.Min(box => box[2]),
            valid.Max(box => box[3]),
            valid.Max(box => box[4]),
            valid.Max(box => box[5])
        ];
    }

    static double[] BoxCenter(double[] box) =>
        box.Length >= 6
            ? [(box[0] + box[3]) / 2.0, (box[1] + box[4]) / 2.0, (box[2] + box[5]) / 2.0]
            : [];

    static string SanitizeId(string value)
    {
        var chars = value.Select(ch => char.IsLetterOrDigit(ch) ? ch : '_').ToArray();
        return new string(chars).Trim('_');
    }

    static string FirstNonEmpty(params string[] values) =>
        values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)) ?? "";
}

readonly record struct InterfaceProjectionRect(double UMin, double UMax, double VMin, double VMax)
{
    public double Area => Math.Max(1.0, (UMax - UMin) * (VMax - VMin));
}

sealed class PhysicalInterfaceGroup
{
    public string BaseKey { get; set; } = "";
    public string Id { get; set; } = "";
    public List<AssemblyFaceContactEvidence> Contacts { get; set; } = [];
    public List<string> FaceARefs { get; set; } = [];
    public List<string> FaceBRefs { get; set; } = [];
    public List<int> FaceAIndices { get; set; } = [];
    public List<int> FaceBIndices { get; set; } = [];
    public List<Face2> RuntimeFaces { get; set; } = [];
    public double AnalysisPriority { get; set; }
    public string ConfidenceTier { get; set; } = "";
}
