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

namespace AssemblyKnowledgeAudit;

internal class Program
{
    const int Asm = 2, Silent = 1, ReadOnly = 2;
    static string StandardPartSourceRoot => Environment.GetEnvironmentVariable("AGENT4_REDUCER_MODEL_DIR") ?? "";

    [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 readonly JsonSerializerOptions JsonOptions = new()
    {
        WriteIndented = true
    };
    static string DiagnosticLogPath = "";
    static readonly Dictionary<string, List<Dictionary<string, object>>> ComponentCylinderSignatureCache = new(StringComparer.OrdinalIgnoreCase);

    [STAThread]
    static void Main(string[] args)
    {
        try
        {
            string asmPath = args.Length > 0
                ? args[0]
                : throw new ArgumentException("Provide an assembly path or set AGENT4_REDUCER_MODEL_DIR.");
            if (!File.Exists(asmPath) || !string.Equals(Path.GetExtension(asmPath), ".SLDASM", StringComparison.OrdinalIgnoreCase))
                throw new Exception("Please input a valid .SLDASM assembly path.");

            Console.WriteLine("姝ｅ湪杩炴帴 SolidWorks...");
            var sw = Connect() ?? throw new Exception("鏃犳硶杩炴帴 SolidWorks");

            string fileBase = MakeSafeFileName(Path.GetFileNameWithoutExtension(asmPath));
            string outPath = Path.Combine(Path.GetDirectoryName(asmPath) ?? System.Environment.CurrentDirectory, $"{fileBase}_knowledge_skillflow.json");
            var report = BuildAssemblyKnowledge(sw, Path.GetFullPath(asmPath));
            File.WriteAllText(outPath, JsonSerializer.Serialize(report, JsonOptions));

            Console.WriteLine("瀹屾垚锛佺粺涓€鐭ヨ瘑 JSON 宸插鍑猴細" + outPath);
        }
        catch (Exception ex)
        {
            Console.WriteLine("Program error: " + ex.Message);
            Console.WriteLine(ex);
        }

        Console.WriteLine("Press Enter to exit...");
        if (System.Environment.UserInteractive && args.Length == 0)
            Console.ReadLine();
    }

    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
        {
            try
            {
                var t = Type.GetTypeFromProgID("SldWorks.Application");
                if (t == null) return null;
                var sw = (SldWorks)Activator.CreateInstance(t);
                if (sw != null) sw.Visible = true;
                return sw;
            }
            catch { return null; }
        }
    }

    static object BuildAssemblyKnowledge(SldWorks sw, string asmPath)
    {
        InitDiagnosticLog(asmPath);
        Log("[assembly] BuildAssemblyKnowledge started");
        string extractionArtifactDir = Path.GetDirectoryName(asmPath) ?? System.Environment.CurrentDirectory;
        int errors = 0, warnings = 0;
        var doc = sw.OpenDoc6(asmPath, Asm, Silent | ReadOnly, "", ref errors, ref warnings) as ModelDoc2;
        if (doc is not AssemblyDoc assy)
            throw new Exception($"OpenDoc6 澶辫触 errors={errors}, warnings={warnings}, path={asmPath}");

        string assemblyName = Safe(() => doc.GetTitle(), Path.GetFileName(asmPath));
        Console.WriteLine("Extracting assembly knowledge: " + assemblyName);

        Log("[assembly] reading components...");
        var components = ReadComponents(assy, assemblyName);
        var componentIdMap = NormalizeComponentInstanceIds(components);
        Log("[assembly] extracting assembly component patterns...");
        var componentPatterns = ReadAssemblyComponentPatterns(doc, componentIdMap);
        AnnotatePatternGeneratedComponents(components, componentPatterns);
        Log($"[assembly] components={components.Count}");
        var uniqueParts = components
            .Where(c => string.Equals(c.DocumentType, "part", StringComparison.OrdinalIgnoreCase))
            .GroupBy(c => c.FilePath, StringComparer.OrdinalIgnoreCase)
            .Select(g => new
            {
                FilePath = g.Key,
                PartName = Path.GetFileNameWithoutExtension(g.Key),
                InstanceCount = g.Count(),
                Instances = g.Select(x => x.ComponentName).ToList(),
                OriginalInstances = g.Select(x => x.OriginalComponentName).Where(x => !string.IsNullOrWhiteSpace(x)).ToList(),
                IsStandardPart = g.Any(x => x.IsStandardPart) || IsGbStandardPart(Path.GetFileNameWithoutExtension(g.Key), g.Key),
                StandardRule = "name_contains_GB"
            })
            .OrderBy(x => x.PartName)
            .ToList();
        Log($"[assembly] unique_parts={uniqueParts.Count}, gb_standard_parts={uniqueParts.Count(x => x.IsStandardPart)}");

        var parts = new List<PartKnowledge>();
        foreach (var part in uniqueParts)
        {
            if (part.IsStandardPart)
            {
                Log($"[assembly] GB standard part: skip modeling and insert directly: {part.PartName}");
                string standardPartPath = ResolveStandardPartSourcePath(part.PartName, part.FilePath, part.Instances);
                parts.Add(CreateStandardPartKnowledge(part.FilePath, standardPartPath, part.PartName, part.InstanceCount, part.Instances, part.OriginalInstances, part.StandardRule));
                continue;
            }

            Console.WriteLine($"  -> extracting/reusing part skill flow: {part.PartName}");
            Log($"[assembly] extracting/reusing part skill flow: {part.PartName}");
            parts.Add(ExtractPartKnowledge(part.FilePath, part.PartName, part.InstanceCount, part.Instances, part.OriginalInstances, extractionArtifactDir));
        }

        Log("[assembly] extracting mates...");
        var mates = ExtractMates(doc);
        NormalizeMateComponentIds(mates, componentIdMap);
        Log($"[assembly] mates={mates.Count}");
        Log("[assembly] building functional face repository...");
        var functionalFaces = BuildFunctionalFaceRepository(mates);
        Log($"[assembly] functional_faces={functionalFaces.Count}");
        Log("[assembly] normalizing standard part functional face paths...");
        NormalizeStandardPartFunctionalFacePaths(parts, functionalFaces);
        Log("[assembly] backfilling part functional knowledge...");
        BackfillPartFunctionalKnowledge(parts, functionalFaces);
        Log("[assembly] persisting enriched part modeling plans...");
        PersistEnrichedPartModelingPlans(parts, assemblyName, asmPath);
        string workflowId = "assembly-knowledge-skillflow-" + MakeAssemblyStepFileBase(assemblyName);
        Log("[assembly] building executable assembly steps...");
        var executableSteps = BuildExecutableSteps(workflowId, assemblyName, components, parts, componentPatterns, mates, functionalFaces);
        Log($"[assembly] executable_steps={executableSteps.Count}");

        return new
        {
            schemaVersion = "assembly_knowledge_skillflow.v1",
            workflowId,
            sourceAgent = "assembly_knowledge_audit",
            approvedPlanText = "Generated from extracted part modeling plans, assembly mates, and functional face repository.",
            generatedAt = DateTime.Now,
            modelingPlan = new
            {
                planId = workflowId + "-plan",
                targetAgent = "solidworks",
                units = "mm",
                summary = $"Rebuild assembly from extracted part skill flows and functional-face mates. steps={executableSteps.Count}",
                steps = executableSteps
            },
            knowledge = new
            {
                sourceAssembly = new
                {
                    name = assemblyName,
                    path = asmPath
                },
                parts,
                functionalFaceRepository = functionalFaces,
                assembly = new
                {
                    components,
                    componentPatterns,
                    mates,
                    matePorts = BuildMatePorts(mates)
                }
            }
        };
    }

    sealed class ComponentInfo
    {
        public string AssemblyName { get; set; } = "";
        public string ParentComponent { get; set; } = "";
        public string OriginalParentComponent { get; set; } = "";
        public string ComponentName { get; set; } = "";
        public string OriginalComponentName { get; set; } = "";
        public string PartName { get; set; } = "";
        public string FilePath { get; set; } = "";
        public string DocumentType { get; set; } = "";
        public string SourceKind { get; set; } = "direct_seed";
        public bool IsPatternInstance { get; set; }
        public bool IsMirrored { get; set; }
        public string SourcePattern { get; set; } = "";
        public string SourcePatternType { get; set; } = "";
        public string SourceSeedComponent { get; set; } = "";
        public bool IsStandardPart { get; set; }
        public string StandardRule { get; set; } = "";
        public double X { get; set; }
        public double Y { get; set; }
        public double Z { get; set; }
        public double[] Transform { get; set; } = Array.Empty<double>();
    }

    sealed class MateFeatureSnapshot
    {
        public Feature Feature { get; set; }
        public Mate2 Mate { get; set; }
        public string MateName { get; set; } = "";
        public int ScannedFeatureIndex { get; set; }
    }

    sealed class PartKnowledge
    {
        public string PartName { get; set; } = "";
        public string FilePath { get; set; } = "";
        public int InstanceCount { get; set; }
        public List<string> Instances { get; set; } = new();
        public List<string> OriginalInstances { get; set; } = new();
        public object Extraction { get; set; } = new();
        public string SkillFlowPath { get; set; } = "";
        public string ModelingPlanPath { get; set; } = "";
        public string ValidationPath { get; set; } = "";
        public string RebuiltOutputPath { get; set; } = "";
        public bool IsStandardPart { get; set; }
        public string StandardRule { get; set; } = "";
        public string DirectInsertPath { get; set; } = "";
        public object SkillFlow { get; set; }
        public object ModelingPlan { get; set; }
        public object Validation { get; set; }
        public List<Dictionary<string, object>> FunctionalFaces { get; set; } = new();
        public List<Dictionary<string, object>> MatePorts { get; set; } = new();
    }

    sealed class AssemblyComponentPatternInfo
    {
        public string PatternName { get; set; } = "";
        public string PatternType { get; set; } = "";
        public string FeatureTypeName { get; set; } = "";
        public List<string> SeedComponents { get; set; } = new();
        public List<string> GeneratedComponents { get; set; } = new();
        public List<string> SkippedItems { get; set; } = new();
        public Dictionary<string, object> Parameters { get; set; } = new();
    }

    static List<ComponentInfo> ReadComponents(AssemblyDoc assy, string assemblyName)
    {
        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 path = Safe(() => comp.GetPathName());
            if (string.IsNullOrWhiteSpace(path)) continue;
            string ext = Path.GetExtension(path).ToLowerInvariant();
            string componentName = Safe(() => comp.Name2);
            string partName = Path.GetFileNameWithoutExtension(path);
            bool isStandardPart = IsGbStandardPart(componentName, partName, path);

            var transform = ComponentTransform(comp);
            result.Add(new ComponentInfo
            {
                AssemblyName = assemblyName,
                ParentComponent = ParentName(comp),
                OriginalParentComponent = ParentName(comp),
                ComponentName = componentName,
                OriginalComponentName = componentName,
                PartName = partName,
                FilePath = Path.GetFullPath(path),
                DocumentType = ext == ".sldprt" ? "part" : ext == ".sldasm" ? "assembly" : ext.TrimStart('.'),
                IsPatternInstance = SafeBool(() => comp.IsPatternInstance()),
                IsMirrored = SafeBool(() => comp.IsMirrored()),
                IsStandardPart = isStandardPart,
                StandardRule = isStandardPart ? "name_contains_GB" : "",
                X = transform.Length >= 12 ? transform[9] : 0.0,
                Y = transform.Length >= 12 ? transform[10] : 0.0,
                Z = transform.Length >= 12 ? transform[11] : 0.0,
                Transform = transform
            });
        }
        return result;
    }

    static Dictionary<string, string> NormalizeComponentInstanceIds(List<ComponentInfo> components)
    {
        var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        var counters = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);

        foreach (var component in components
            .OrderBy(c => ComponentIdBaseName(c), StringComparer.OrdinalIgnoreCase)
            .ThenBy(c => IsGeneratedAssemblyComponent(c) ? 1 : 0)
            .ThenBy(c => OriginalInstanceNumber(c))
            .ThenBy(c => c.OriginalComponentName, StringComparer.OrdinalIgnoreCase))
        {
            string originalName = string.IsNullOrWhiteSpace(component.OriginalComponentName)
                ? component.ComponentName
                : component.OriginalComponentName;
            component.OriginalComponentName = originalName;

            string baseName = ComponentIdBaseName(component);
            if (string.IsNullOrWhiteSpace(baseName))
                continue;

            counters.TryGetValue(baseName, out int count);
            count++;
            counters[baseName] = count;

            string normalizedName = $"{baseName}-{count}";
            component.ComponentName = normalizedName;

            if (!string.IsNullOrWhiteSpace(originalName) && !map.ContainsKey(originalName))
                map[originalName] = normalizedName;
        }

        foreach (var component in components)
        {
            if (string.IsNullOrWhiteSpace(component.OriginalParentComponent))
                component.OriginalParentComponent = component.ParentComponent;

            if (!string.IsNullOrWhiteSpace(component.ParentComponent) &&
                map.TryGetValue(component.ParentComponent, out string normalizedParent))
            {
                component.ParentComponent = normalizedParent;
            }
        }

        Log($"[assembly] normalized_component_ids={map.Count}");
        return map;
    }

    static bool IsGeneratedAssemblyComponent(ComponentInfo component)
    {
        if (component.IsPatternInstance || component.IsMirrored)
            return true;
        return string.Equals(component.SourceKind, "generated_by_pattern", StringComparison.OrdinalIgnoreCase) ||
               string.Equals(component.SourceKind, "generated_by_mirror", StringComparison.OrdinalIgnoreCase);
    }

    static int OriginalInstanceNumber(ComponentInfo component)
    {
        string value = string.IsNullOrWhiteSpace(component.OriginalComponentName)
            ? component.ComponentName
            : component.OriginalComponentName;
        if (string.IsNullOrWhiteSpace(value))
            return int.MaxValue;

        int dash = value.LastIndexOf('-');
        if (dash < 0 || dash >= value.Length - 1)
            return int.MaxValue;

        return int.TryParse(value[(dash + 1)..], NumberStyles.Integer, CultureInfo.InvariantCulture, out int number)
            ? number
            : int.MaxValue;
    }

    static string ComponentIdBaseName(ComponentInfo component)
    {
        string baseName = StripInstanceSuffix(component.PartName);
        if (string.IsNullOrWhiteSpace(baseName))
            baseName = StripInstanceSuffix(component.OriginalComponentName);
        if (string.IsNullOrWhiteSpace(baseName))
            baseName = StripInstanceSuffix(component.ComponentName);
        return baseName.Trim();
    }

    static List<AssemblyComponentPatternInfo> ReadAssemblyComponentPatterns(ModelDoc2 doc, IReadOnlyDictionary<string, string> componentIdMap)
    {
        var result = new List<AssemblyComponentPatternInfo>();
        foreach (var feat in WalkFeatures(doc.FirstFeature() as Feature))
        {
            string featureName = Safe(() => feat.Name);
            string typeName = Safe(() => feat.GetTypeName2());
            string patternType = AssemblyPatternType(typeName, featureName);
            if (string.IsNullOrWhiteSpace(patternType))
                continue;

            var pattern = new AssemblyComponentPatternInfo
            {
                PatternName = featureName,
                PatternType = patternType,
                FeatureTypeName = typeName
            };

            object specific = SafeObj(() => feat.GetDefinition()) ?? SafeObj(() => feat.GetSpecificFeature2());
            if (specific != null)
            {
                AccessPatternSelections(specific, doc, pattern.Parameters);
                ReadPatternComponents(specific, "SeedComponentArray", pattern.SeedComponents, componentIdMap);
                ReadPatternComponents(specific, "ComponentsToInstanceAlignToComponentOrigin", pattern.SeedComponents, componentIdMap);
                ReadPatternComponents(specific, "ComponentsToInstanceAlignToSelection", pattern.SeedComponents, componentIdMap);
                ReadPatternComponents(specific, "OppositeHandComponents", pattern.SeedComponents, componentIdMap);
                ReadPatternComponentGroup(specific, "SeedComponentArray", pattern.Parameters, componentIdMap);
                ReadPatternComponentGroup(specific, "ComponentsToInstanceAlignToComponentOrigin", pattern.Parameters, componentIdMap);
                ReadPatternComponentGroup(specific, "ComponentsToInstanceAlignToSelection", pattern.Parameters, componentIdMap);
                ReadPatternComponentGroup(specific, "OppositeHandComponents", pattern.Parameters, componentIdMap);
                ReadPatternValueArray(specific, pattern.Parameters, "ComponentOrientationsAlignToComponentOrigin");
                ReadPatternValueArray(specific, pattern.Parameters, "ComponentOrientationsAlignToSelection");
                ReadPatternValueArray(specific, pattern.Parameters, "FlipDirections");
                ReadPatternValueArray(specific, pattern.Parameters, "MirroredComponentFilenames");
                ReadPatternComponents(specific, "SkippedItemArray", pattern.SkippedItems, componentIdMap);
                ReadPatternScalar(specific, pattern.Parameters, "D1TotalInstances");
                ReadPatternScalar(specific, pattern.Parameters, "D1Spacing");
                ReadPatternScalar(specific, pattern.Parameters, "D1ReverseDirection");
                ReadPatternScalar(specific, pattern.Parameters, "D2TotalInstances");
                ReadPatternScalar(specific, pattern.Parameters, "D2Spacing");
                ReadPatternScalar(specific, pattern.Parameters, "D2ReverseDirection");
                ReadPatternScalar(specific, pattern.Parameters, "TotalInstances");
                ReadPatternScalar(specific, pattern.Parameters, "TotalInstances2");
                ReadPatternScalar(specific, pattern.Parameters, "Spacing");
                ReadPatternScalar(specific, pattern.Parameters, "Spacing2");
                ReadPatternScalar(specific, pattern.Parameters, "EqualSpacing");
                ReadPatternScalar(specific, pattern.Parameters, "EqualSpacing2");
                ReadPatternScalar(specific, pattern.Parameters, "ReverseDirection");
                ReadPatternScalar(specific, pattern.Parameters, "Direction2");
                ReadPatternScalar(specific, pattern.Parameters, "Symmetric");
                ReadPatternScalar(specific, pattern.Parameters, "SynchronizeFlexibleComponents");
                ReadPatternScalar(specific, pattern.Parameters, "ForceUseSeedConfiguration");
                ReadPatternScalar(specific, pattern.Parameters, "MirrorType");
                ReadPatternScalar(specific, pattern.Parameters, "NameModifierType");
                ReadPatternScalar(specific, pattern.Parameters, "NameModifier");
                ReadPatternScalar(specific, pattern.Parameters, "MirrorTransferOptions");
                ReadPatternScalar(specific, pattern.Parameters, "BreakLinksToOriginalPart");
                ReadPatternScalar(specific, pattern.Parameters, "CreateDerivedConfigurations");
                ReadPatternScalar(specific, pattern.Parameters, "PreserveZAxis");
                ReadPatternScalar(specific, pattern.Parameters, "SyncFlexibleSubAssemblies");
                ReadPatternScalar(specific, pattern.Parameters, "MirrorComponentsFolderLocation");
                ReadPatternReference(specific, pattern.Parameters, "D1Axis");
                ReadPatternReference(specific, pattern.Parameters, "D2Axis");
                ReadPatternReference(specific, pattern.Parameters, "RotationAxis");
                ReadPatternReference(specific, pattern.Parameters, "Axis");
                ReadPatternReference(specific, pattern.Parameters, "MirrorPlane");
                ReadPatternMethodScalar(specific, pattern.Parameters, "GetAxisType", "AxisType");
                ReadPatternMethodScalar(specific, pattern.Parameters, "GetD1AxisType", "D1AxisType");
                ReadPatternMethodScalar(specific, pattern.Parameters, "GetD2AxisType", "D2AxisType");
                ReadTypedPatternData(specific, pattern.Parameters);
                ReadPatternSelectionReferences(doc, pattern.Parameters);
                PromotePatternReferenceFromSelections(pattern.PatternType, pattern.Parameters);
                NormalizePatternReferenceComponentIds(pattern.Parameters, componentIdMap);
                if (string.Equals(pattern.PatternType, "mirror_component", StringComparison.OrdinalIgnoreCase))
                    NormalizeMirrorComponentParameters(pattern.Parameters);
                ReleasePatternSelections(specific);
            }

            ReadPatternGeneratedComponents(feat, pattern.GeneratedComponents, componentIdMap);

            result.Add(pattern);
        }

        Log($"[assembly] component_patterns={result.Count}");
        return result;
    }

    static string AssemblyPatternType(string typeName, string featureName)
    {
        string text = $"{typeName} {featureName}";
        if (ContainsAny(text, "LocalLPattern", "局部线性阵列"))
            return "local_linear_pattern";
        if (ContainsAny(text, "LocalCirPattern", "局部圆周阵列"))
            return "local_circular_pattern";
        if (ContainsAny(text, "MirrorComponent", "镜向零部件"))
            return "mirror_component";
        return "";
    }

    static void ReadPatternComponents(object featureData, string propertyName, List<string> target, IReadOnlyDictionary<string, string> componentIdMap)
    {
        object value = GetComProperty(featureData, propertyName);
        foreach (object item in ToObjectArray(value))
        {
            string name = "";
            if (item is Component2 comp)
                name = Safe(() => comp.Name2);
            else if (item is Feature feature)
                name = Safe(() => feature.Name);

            AddPatternComponentName(target, name, componentIdMap);
        }
    }

    static void ReadPatternGeneratedComponents(Feature patternFeature, List<string> target, IReadOnlyDictionary<string, string> componentIdMap)
    {
        Feature sub = SafeObj(() => patternFeature.GetFirstSubFeature()) as Feature;
        while (sub != null)
        {
            string type = Safe(() => sub.GetTypeName2());
            if (ContainsAny(type, "ReferencePattern"))
                AddPatternComponentName(target, Safe(() => sub.Name), componentIdMap);

            sub = SafeObj(() => sub.GetNextSubFeature()) as Feature;
        }
    }

    static void AddPatternComponentName(List<string> target, string name, IReadOnlyDictionary<string, string> componentIdMap)
    {
        if (string.IsNullOrWhiteSpace(name))
            return;

        if (componentIdMap.TryGetValue(name, out string normalized))
            name = normalized;
        if (!string.IsNullOrWhiteSpace(name) && !target.Contains(name, StringComparer.OrdinalIgnoreCase))
            target.Add(name);
    }

    static void NormalizePatternReferenceComponentIds(Dictionary<string, object> parameters, IReadOnlyDictionary<string, string> componentIdMap)
    {
        if (parameters.Count == 0 || componentIdMap.Count == 0)
            return;

        foreach (var value in parameters.Values)
            NormalizeReferenceComponentIds(value, componentIdMap);
    }

    static void NormalizeReferenceComponentIds(object value, IReadOnlyDictionary<string, string> componentIdMap)
    {
        if (value is Dictionary<string, object> dict)
        {
            NormalizeReferenceComponentId(dict, "owning_component", componentIdMap);
            foreach (var nested in dict.Values)
                NormalizeReferenceComponentIds(nested, componentIdMap);
            return;
        }

        if (value is List<Dictionary<string, object>> dictList)
        {
            foreach (var item in dictList)
                NormalizeReferenceComponentIds(item, componentIdMap);
            return;
        }

        if (value is IEnumerable<object> items && value is not string)
        {
            foreach (var item in items)
                NormalizeReferenceComponentIds(item, componentIdMap);
        }
    }

    static void NormalizeReferenceComponentId(Dictionary<string, object> dict, string key, IReadOnlyDictionary<string, string> componentIdMap)
    {
        if (!dict.TryGetValue(key, out var value) || value == null)
            return;

        string original = Convert.ToString(value, CultureInfo.InvariantCulture) ?? "";
        if (string.IsNullOrWhiteSpace(original))
            return;

        if (componentIdMap.TryGetValue(original, out string normalized) && !string.IsNullOrWhiteSpace(normalized))
        {
            dict[key] = normalized;
            if (!dict.ContainsKey("original_" + key))
                dict["original_" + key] = original;
        }
    }

    static void ReadPatternComponentGroup(object featureData, string propertyName, Dictionary<string, object> parameters, IReadOnlyDictionary<string, string> componentIdMap)
    {
        var names = new List<string>();
        ReadPatternComponents(featureData, propertyName, names, componentIdMap);
        if (names.Count > 0)
            parameters[propertyName + "Components"] = names;
    }

    static void ReadPatternValueArray(object featureData, Dictionary<string, object> parameters, string propertyName)
    {
        object value = GetComProperty(featureData, propertyName);
        if (value == null)
            return;

        var values = new List<object>();
        foreach (object item in ToObjectArray(value))
        {
            if (item == null)
                continue;

            if (item is bool or int or double or float or decimal or string)
            {
                values.Add(item);
                continue;
            }

            try
            {
                values.Add(Convert.ToInt32(item));
            }
            catch
            {
                string text = Safe(() => item.ToString() ?? "");
                if (!string.IsNullOrWhiteSpace(text))
                    values.Add(text);
            }
        }

        if (values.Count > 0)
            parameters[propertyName] = values;
    }

    static void ReadPatternScalar(object featureData, Dictionary<string, object> parameters, string propertyName)
    {
        object value = GetComProperty(featureData, propertyName);
        if (value == null)
            return;

        if (value is string s)
        {
            if (!string.IsNullOrWhiteSpace(s))
                parameters[propertyName] = s;
            return;
        }

        if (value is bool or int or double or float or decimal)
            parameters[propertyName] = value;
    }

    static void ReadPatternReference(object featureData, Dictionary<string, object> parameters, string propertyName)
    {
        object value = GetComProperty(featureData, propertyName);
        if (value == null)
            return;

        var reference = DescribeReference(value);
        if (reference.Count > 0)
            parameters[propertyName + "Reference"] = reference;
    }

    static void ReadPatternMethodScalar(object featureData, Dictionary<string, object> parameters, string methodName, string parameterName)
    {
        object value = InvokeIfExists(featureData, methodName);
        if (value == null)
            return;

        if (value is bool or int or double or float or decimal)
        {
            parameters[parameterName] = value;
            return;
        }

        try
        {
            parameters[parameterName] = Convert.ToInt32(value, CultureInfo.InvariantCulture);
        }
        catch { }
    }

    static void AccessPatternSelections(object featureData, ModelDoc2 doc, Dictionary<string, object> parameters)
    {
        bool accessed = false;
        if (featureData is ILocalCircularPatternFeatureData circular)
            accessed = SafeBool(() => circular.IAccessSelections2(doc, null)) ||
                       SafeBool(() => circular.AccessSelections(doc, null));
        else if (featureData is ILocalLinearPatternFeatureData linear)
            accessed = SafeBool(() => linear.IAccessSelections2(doc, null)) ||
                       SafeBool(() => linear.AccessSelections(doc, null));
        else if (featureData is IMirrorComponentFeatureData mirror)
            accessed = SafeBool(() => mirror.AccessSelections(doc, null));
        else
            accessed = SafeBool(() => Convert.ToBoolean(InvokeIfExists(featureData, "AccessSelections", doc, null), CultureInfo.InvariantCulture));

        parameters["AccessSelectionsOk"] = accessed;
    }

    static void ReleasePatternSelections(object featureData)
    {
        if (featureData is ILocalCircularPatternFeatureData circular)
            SafeAction(() => circular.ReleaseSelectionAccess());
        else if (featureData is ILocalLinearPatternFeatureData linear)
            SafeAction(() => linear.ReleaseSelectionAccess());
        else if (featureData is IMirrorComponentFeatureData mirror)
            SafeAction(() => mirror.ReleaseSelectionAccess());
        else
            SafeAction(() => InvokeIfExists(featureData, "ReleaseSelectionAccess"));
    }

    static void ReadTypedPatternData(object featureData, Dictionary<string, object> parameters)
    {
        if (featureData is ILocalCircularPatternFeatureData circular)
        {
            int axisType = SafeInt(() => circular.GetAxisType(), -1);
            parameters["AxisType"] = axisType;
            parameters["AxisTypeName"] = LocalCircularAxisTypeName(axisType);
            parameters["SeedComponentCount"] = SafeInt(() => circular.GetSeedComponentCount(), 0);
            parameters["SkippedItemCount"] = SafeInt(() => circular.GetSkippedItemCount(), 0);
            var axisRef = DescribeReference(SafeObj(() => circular.Axis));
            if (axisRef.Count > 0)
            {
                axisRef["axis_type"] = axisType;
                axisRef["axis_type_name"] = LocalCircularAxisTypeName(axisType);
                axisRef["reference_source"] = "ILocalCircularPatternFeatureData.Axis";
                parameters["AxisReference"] = axisRef;
            }
            ReadPatternTransforms(
                parameters,
                "InstanceTransforms",
                SafeInt(() => circular.TotalInstances, ReadIntParameter(parameters, "TotalInstances", 0)),
                index => SafeObj(() => circular.GetTransform(index)));
        }
        else if (featureData is ILocalLinearPatternFeatureData linear)
        {
            parameters["D1AxisType"] = SafeInt(() => linear.GetD1AxisType(), -1);
            parameters["D2AxisType"] = SafeInt(() => linear.GetD2AxisType(), -1);
            parameters["SeedComponentCount"] = SafeInt(() => linear.GetSeedComponentCount(), 0);
            parameters["SkippedItemCount"] = SafeInt(() => linear.GetSkippedItemCount(), 0);
            var d1Ref = DescribeReference(SafeObj(() => linear.D1Axis));
            if (d1Ref.Count > 0)
                parameters["D1AxisReference"] = d1Ref;
            var d2Ref = DescribeReference(SafeObj(() => linear.D2Axis));
            if (d2Ref.Count > 0)
                parameters["D2AxisReference"] = d2Ref;
            int d1 = SafeInt(() => linear.D1TotalInstances, ReadIntParameter(parameters, "D1TotalInstances", 0));
            int d2 = SafeInt(() => linear.D2TotalInstances, ReadIntParameter(parameters, "D2TotalInstances", 0));
            ReadPatternTransforms(parameters, "InstanceTransforms", Math.Max(d1, d1 * Math.Max(1, d2)), index => SafeObj(() => linear.GetTransform(index)));
        }
    }

    static string LocalCircularAxisTypeName(int axisType)
    {
        return axisType switch
        {
            0 => "reference_axis",
            1 => "edge",
            2 => "dimension",
            _ => "unknown"
        };
    }

    static void ReadPatternTransforms(Dictionary<string, object> parameters, string key, int totalInstances, Func<int, object> getTransform)
    {
        if (totalInstances <= 0)
            return;

        var transforms = new List<Dictionary<string, object>>();
        for (int i = 0; i <= totalInstances; i++)
        {
            object transform = getTransform(i);
            var values = MathTransformArray(transform);
            if (values.Length == 0)
                continue;

            transforms.Add(new Dictionary<string, object>
            {
                ["instance_index"] = i,
                ["array_data"] = values,
                ["coordinate_system"] = "solidworks_math_transform"
            });
        }

        if (transforms.Count > 0)
            parameters[key] = transforms;
    }

    static double[] MathTransformArray(object transform)
    {
        if (transform == null)
            return Array.Empty<double>();
        object raw = GetComProperty(transform, "ArrayData");
        var values = ToDoubleArray(raw);
        return values.Length > 0 ? values.Select(R).ToArray() : Array.Empty<double>();
    }

    static void ReadPatternSelectionReferences(ModelDoc2 doc, Dictionary<string, object> parameters)
    {
        var selections = new List<Dictionary<string, object>>();
        try
        {
            if (doc?.SelectionManager is not SelectionMgr selMgr)
                return;

            int count = SafeInt(() => selMgr.GetSelectedObjectCount2(-1), 0);
            for (int i = 1; i <= count; i++)
            {
                object selected = SafeObj(() => selMgr.GetSelectedObject6(i, -1));
                if (selected == null)
                    continue;

                var reference = DescribeReference(selected);
                if (reference.Count == 0)
                    continue;

                int selectionType = SafeInt(() => selMgr.GetSelectedObjectType3(i, -1), 0);
                int selectionMark = SafeInt(() => Convert.ToInt32(InvokeIfExists(selMgr, "GetSelectedObjectMark", i), CultureInfo.InvariantCulture), 0);
                reference["selection_index"] = i;
                reference["selection_object_type"] = selectionType;
                reference["selection_object_type_name"] = SelectionTypeName(selectionType);
                reference["selection_mark"] = selectionMark;
                selections.Add(reference);
            }
        }
        catch { }

        if (selections.Count > 0)
            parameters["SelectionReferences"] = selections;
    }

    static void PromotePatternReferenceFromSelections(string patternType, Dictionary<string, object> parameters)
    {
        if (!string.Equals(patternType, "local_circular_pattern", StringComparison.OrdinalIgnoreCase))
            return;
        if (parameters.ContainsKey("AxisReference"))
            return;
        if (!parameters.TryGetValue("SelectionReferences", out object value) || value is not List<Dictionary<string, object>> selections)
            return;

        var explicitAxisCandidates = selections
            .Where(IsAxisLikeSelectionReference)
            .ToList();
        if (explicitAxisCandidates.Count == 1)
        {
            explicitAxisCandidates[0]["reference_source"] = "AccessSelections axis-like selection";
            parameters["AxisReference"] = explicitAxisCandidates[0];
            parameters["AxisReferenceSource"] = "feature_data_access_selections";
        }
    }

    static bool IsAxisLikeSelectionReference(Dictionary<string, object> reference)
    {
        string kind = ReadDictString(reference, "reference_kind");
        string selectionType = ReadDictString(reference, "selection_type");
        string selectionObjectType = ReadDictString(reference, "selection_object_type_name");
        string featureType = ReadDictString(reference, "feature_type_name");
        string curveType = "";
        if (reference.TryGetValue("edge_signature", out object edgeObj) && edgeObj is Dictionary<string, object> edgeSig)
            curveType = ReadDictString(edgeSig, "curve_type");

        if (string.Equals(kind, "feature", StringComparison.OrdinalIgnoreCase) &&
            featureType.Contains("RefAxis", StringComparison.OrdinalIgnoreCase))
            return true;
        if (selectionType.Contains("AXIS", StringComparison.OrdinalIgnoreCase) ||
            selectionObjectType.Contains("AXIS", StringComparison.OrdinalIgnoreCase) ||
            selectionObjectType.Contains("DATUMAXES", StringComparison.OrdinalIgnoreCase))
            return true;
        if (string.Equals(kind, "edge", StringComparison.OrdinalIgnoreCase) &&
            (string.Equals(curveType, "circle", StringComparison.OrdinalIgnoreCase) ||
             string.Equals(curveType, "line", StringComparison.OrdinalIgnoreCase)))
            return true;
        if (string.Equals(kind, "face", StringComparison.OrdinalIgnoreCase) &&
            reference.TryGetValue("face_signature", out object faceObj) &&
            faceObj is Dictionary<string, object> faceSig &&
            string.Equals(ReadDictString(faceSig, "surface_type"), "cylinder", StringComparison.OrdinalIgnoreCase))
            return true;

        return false;
    }

    static string SelectionTypeName(int selectionType)
    {
        try
        {
            string name = Enum.GetName(typeof(swSelectType_e), selectionType);
            return name ?? selectionType.ToString(CultureInfo.InvariantCulture);
        }
        catch
        {
            return selectionType.ToString(CultureInfo.InvariantCulture);
        }
    }

    static void NormalizeMirrorComponentParameters(Dictionary<string, object> parameters)
    {
        var originComponents = ReadObjectList(parameters, "ComponentsToInstanceAlignToComponentOriginComponents");
        var selectionComponents = ReadObjectList(parameters, "ComponentsToInstanceAlignToSelectionComponents");
        var oppositeComponents = ReadObjectList(parameters, "OppositeHandComponentsComponents");
        var originOrientations = ReadObjectList(parameters, "ComponentOrientationsAlignToComponentOrigin");
        var selectionOrientations = ReadObjectList(parameters, "ComponentOrientationsAlignToSelection");

        var instanceComponents = new List<object>();
        instanceComponents.AddRange(originComponents);
        instanceComponents.AddRange(selectionComponents);

        var componentOrientations = new List<object>();
        componentOrientations.AddRange(originOrientations);
        componentOrientations.AddRange(selectionOrientations);

        parameters["MirrorComponentsApi"] = "IAssemblyDoc.MirrorComponents3";
        parameters["ComponentsToInstanceComponents"] = instanceComponents;
        parameters["ComponentOrientations"] = componentOrientations;
        parameters["OrientAboutCenterOfMass"] = selectionComponents.Count > 0;
        parameters["ComponentsToMirrorComponents"] = oppositeComponents;

        EnsureParameter(parameters, "CreateDerivedConfigurations", false);
        EnsureParameter(parameters, "MirroredComponentFilenames", new List<object>());
        EnsureParameter(parameters, "NameModifierType", 0);
        EnsureParameter(parameters, "NameModifier", "");
        EnsureParameter(parameters, "MirrorTransferOptions", 0);
        EnsureParameter(parameters, "BreakLinksToOriginalPart", false);
        EnsureParameter(parameters, "PreserveZAxis", false);
        EnsureParameter(parameters, "SyncFlexibleSubAssemblies", false);
        EnsureParameter(parameters, "MirrorComponentsFolderLocation", "");

        parameters["MirroredComponentFileLocation"] = ReadStringParameter(parameters, "MirrorComponentsFolderLocation");
        parameters["ImportOptions"] = ReadIntParameter(parameters, "MirrorTransferOptions", 0);
        parameters["BreakLinks"] = ReadBoolParameter(parameters, "BreakLinksToOriginalPart", false);
    }

    static List<object> ReadObjectList(Dictionary<string, object> values, string key)
    {
        if (!values.TryGetValue(key, out object value) || value == null)
            return new List<object>();

        if (value is IEnumerable<object> objectItems)
            return objectItems.Where(x => x != null).ToList();

        if (value is System.Collections.IEnumerable enumerable && value is not string)
        {
            var result = new List<object>();
            foreach (object item in enumerable)
            {
                if (item != null)
                    result.Add(item);
            }
            return result;
        }

        return new List<object> { value };
    }

    static void EnsureParameter(Dictionary<string, object> values, string key, object defaultValue)
    {
        if (!values.ContainsKey(key) || values[key] == null)
            values[key] = defaultValue;
    }

    static string ReadStringParameter(Dictionary<string, object> values, string key)
    {
        return values.TryGetValue(key, out object value) ? value?.ToString() ?? "" : "";
    }

    static int ReadIntParameter(Dictionary<string, object> values, string key, int defaultValue)
    {
        if (!values.TryGetValue(key, out object value) || value == null)
            return defaultValue;
        try { return Convert.ToInt32(value, CultureInfo.InvariantCulture); }
        catch { return defaultValue; }
    }

    static bool ReadBoolParameter(Dictionary<string, object> values, string key, bool defaultValue)
    {
        if (!values.TryGetValue(key, out object value) || value == null)
            return defaultValue;
        if (value is bool b)
            return b;
        try { return Convert.ToBoolean(value, CultureInfo.InvariantCulture); }
        catch { return defaultValue; }
    }

    static Dictionary<string, object> DescribeReference(object reference)
    {
        var result = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
        try { result["runtime_type"] = reference.GetType().Name; } catch { }

        if (reference is Component2 component)
        {
            result["reference_kind"] = "component";
            string name = Safe(() => component.Name2);
            string path = Safe(() => component.GetPathName());
            if (!string.IsNullOrWhiteSpace(name))
                result["component_name"] = name;
            if (!string.IsNullOrWhiteSpace(path))
                result["component_path"] = path;
        }
        else if (reference is Feature feature)
        {
            result["reference_kind"] = "feature";
            string type = "";
            string selectionName = Safe(() => feature.GetNameForSelection(out type));
            string name = Safe(() => feature.Name);
            string featureType = Safe(() => feature.GetTypeName2());
            if (!string.IsNullOrWhiteSpace(name))
                result["name"] = name;
            if (!string.IsNullOrWhiteSpace(featureType))
                result["feature_type_name"] = featureType;
            if (!string.IsNullOrWhiteSpace(selectionName))
                result["selection_name"] = selectionName;
            if (!string.IsNullOrWhiteSpace(type))
                result["selection_type"] = type;
        }
        else if (reference is Edge edge)
        {
            result["reference_kind"] = "edge";
            result["edge_signature"] = EdgeSignature(edge);
            if (reference is Entity edgeEntity)
                DescribeEntity(edgeEntity, result);
        }
        else if (reference is Face2 face)
        {
            result["reference_kind"] = "face";
            result["face_signature"] = FaceSignature(face);
            if (reference is Entity faceEntity)
                DescribeEntity(faceEntity, result);
        }
        else if (reference is Entity entity)
        {
            result["reference_kind"] = "entity";
            DescribeEntity(entity, result);
        }

        return result;
    }

    static void DescribeEntity(Entity entity, Dictionary<string, object> result)
    {
        string modelName = Safe(() => entity.ModelName);
        int type = SafeInt(() => entity.GetType(), 0);
        if (!string.IsNullOrWhiteSpace(modelName))
            result["model_name"] = modelName;
        if (type != 0)
            result["entity_type"] = type;

        if (SafeObj(() => entity.GetComponent()) is Component2 component)
        {
            string componentName = Safe(() => component.Name2);
            string componentPath = Safe(() => component.GetPathName());
            if (!string.IsNullOrWhiteSpace(componentName))
                result["owning_component"] = componentName;
            if (!string.IsNullOrWhiteSpace(componentPath))
                result["owning_component_path"] = componentPath;
            result["reference_coordinate_system"] = "component_model";
        }
    }

    static void AnnotatePatternGeneratedComponents(List<ComponentInfo> components, List<AssemblyComponentPatternInfo> patterns)
    {
        foreach (var component in components)
        {
            if (!component.IsPatternInstance && !component.IsMirrored)
                continue;

            component.SourceKind = component.IsMirrored ? "generated_by_mirror" : "generated_by_pattern";
            var pattern = FindLikelySourcePattern(component, patterns);
            if (pattern != null)
            {
                component.SourcePattern = pattern.PatternName;
                component.SourcePatternType = pattern.PatternType;
                component.SourceSeedComponent = pattern.SeedComponents.FirstOrDefault() ?? "";
                if (!pattern.GeneratedComponents.Contains(component.ComponentName, StringComparer.OrdinalIgnoreCase))
                    pattern.GeneratedComponents.Add(component.ComponentName);
            }
        }

        foreach (var pattern in patterns)
        {
            pattern.SeedComponents = pattern.SeedComponents
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToList();
            pattern.GeneratedComponents = pattern.GeneratedComponents
                .Where(item => !pattern.SeedComponents.Contains(item, StringComparer.OrdinalIgnoreCase))
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToList();
        }

        int generatedCount = components.Count(c => !IsDirectInsertComponent(c));
        Log($"[assembly] pattern_generated_components={generatedCount}");
    }

    static AssemblyComponentPatternInfo FindLikelySourcePattern(ComponentInfo component, List<AssemblyComponentPatternInfo> patterns)
    {
        var candidates = patterns
            .Where(pattern => component.IsMirrored
                ? string.Equals(pattern.PatternType, "mirror_component", StringComparison.OrdinalIgnoreCase)
                : !string.Equals(pattern.PatternType, "mirror_component", StringComparison.OrdinalIgnoreCase))
            .Where(pattern => pattern.SeedComponents.Any(seed =>
                seed.StartsWith(component.PartName + "-", StringComparison.OrdinalIgnoreCase) ||
                string.Equals(ComponentBaseName(seed), component.PartName, StringComparison.OrdinalIgnoreCase)))
            .ToList();

        return candidates.Count == 1 ? candidates[0] : null;
    }

    static bool IsDirectInsertComponent(ComponentInfo component)
    {
        return string.Equals(component.DocumentType, "part", StringComparison.OrdinalIgnoreCase)
            && !string.Equals(component.SourceKind, "generated_by_pattern", StringComparison.OrdinalIgnoreCase)
            && !string.Equals(component.SourceKind, "generated_by_mirror", StringComparison.OrdinalIgnoreCase);
    }

    static string StripInstanceSuffix(string value)
    {
        if (string.IsNullOrWhiteSpace(value))
            return "";

        string name = value.Trim();
        if (LooksLikePathOrSolidWorksFile(name))
            name = Path.GetFileNameWithoutExtension(name);
        int dash = name.LastIndexOf('-');
        if (dash > 0 && dash < name.Length - 1 && name[(dash + 1)..].All(char.IsDigit))
            name = name[..dash];
        return name.Trim();
    }

    static bool LooksLikePathOrSolidWorksFile(string value)
    {
        if (string.IsNullOrWhiteSpace(value))
            return false;
        if (value.Contains('\\') || value.Contains('/'))
            return true;

        string ext = Path.GetExtension(value);
        return string.Equals(ext, ".SLDPRT", StringComparison.OrdinalIgnoreCase) ||
               string.Equals(ext, ".SLDASM", StringComparison.OrdinalIgnoreCase);
    }

    static double[] ComponentTransform(Component2 comp)
    {
        try
        {
            if (comp.Transform2 is MathTransform transform)
            {
                var data = ToDoubleArray(transform.ArrayData);
                if (data.Length >= 16)
                    return data.Take(16).ToArray();
            }
        }
        catch { }

        return new double[]
        {
            1, 0, 0,
            0, 1, 0,
            0, 0, 1,
            0, 0, 0,
            1, 0, 0, 0
        };
    }

    static PartKnowledge CreateStandardPartKnowledge(string partPath, string directInsertPath, string partName, int instanceCount, List<string> instances, List<string> originalInstances, string standardRule)
    {
        return new PartKnowledge
        {
            PartName = partName,
            FilePath = partPath,
            InstanceCount = instanceCount,
            Instances = instances,
            OriginalInstances = originalInstances,
            IsStandardPart = true,
            StandardRule = standardRule,
            DirectInsertPath = directInsertPath,
            Extraction = new
            {
                ok = true,
                skippedModeling = true,
                reason = "standard_part_name_contains_GB",
                standardPartSourceRoot = StandardPartSourceRoot,
                originalComponentPath = partPath,
                directCallPath = directInsertPath
            }
        };
    }

    static PartKnowledge ExtractPartKnowledge(string partPath, string partName, int instanceCount, List<string> instances, List<string> originalInstances, string extractionArtifactDir)
    {
        string dir = string.IsNullOrWhiteSpace(extractionArtifactDir)
            ? Path.GetDirectoryName(partPath) ?? System.Environment.CurrentDirectory
            : extractionArtifactDir;
        string fileBase = MakeSafeFileName(partName);
        string skillFlowPath = PreferredOrLatestFile(dir, $"{fileBase}_skill_flow.json", $"{fileBase}_skill_flow_*.json");
        string modelingPlanPath = PreferredOrLatestFile(dir, $"{fileBase}_modeling_plan.json", $"{fileBase}_modeling_plan_*.json");
        string validationPath = PreferredOrLatestFile(dir, $"{fileBase}_skill_flow_validation.json", $"{fileBase}_skill_flow_validation_*.json");
        string rebuiltOutputPath = LatestRebuiltPartOutput(partName);
        bool forcePartExtraction = IsForcePartExtractionEnabled();
        bool hasExistingExtraction = File.Exists(skillFlowPath) && File.Exists(modelingPlanPath);
        object result = hasExistingExtraction && !forcePartExtraction
            ? new
            {
                ok = true,
                reusedExistingExtraction = true,
                skillFlowPath,
                modelingPlanPath,
                validationPath,
                rebuiltOutputPath
            }
            : RunPartFeatureAudit(partPath, dir);

        if (!hasExistingExtraction || forcePartExtraction)
        {
            skillFlowPath = PreferredOrLatestFile(dir, $"{fileBase}_skill_flow.json", $"{fileBase}_skill_flow_*.json");
            modelingPlanPath = PreferredOrLatestFile(dir, $"{fileBase}_modeling_plan.json", $"{fileBase}_modeling_plan_*.json");
            validationPath = PreferredOrLatestFile(dir, $"{fileBase}_skill_flow_validation.json", $"{fileBase}_skill_flow_validation_*.json");
            rebuiltOutputPath = LatestRebuiltPartOutput(partName);
        }

        return new PartKnowledge
        {
            PartName = partName,
            FilePath = partPath,
            InstanceCount = instanceCount,
            Instances = instances,
            OriginalInstances = originalInstances,
            IsStandardPart = false,
            StandardRule = "",
            DirectInsertPath = "",
            Extraction = result,
            SkillFlowPath = skillFlowPath,
            ModelingPlanPath = modelingPlanPath,
            ValidationPath = validationPath,
            RebuiltOutputPath = rebuiltOutputPath,
            SkillFlow = ReadJsonOrNull(skillFlowPath),
            ModelingPlan = ReadJsonOrNull(modelingPlanPath),
            Validation = ReadJsonOrNull(validationPath)
        };
    }

    static object RunPartFeatureAudit(string partPath, string outputDir)
    {
        string exe = FindPartFeatureAuditExe();
        if (string.IsNullOrWhiteSpace(exe) || !File.Exists(exe))
        {
            return new
            {
                ok = false,
                message = "鏈壘鍒?PartFeatureAudit.exe锛岃烦杩囬浂浠跺缓妯?skill flow 鎻愬彇"
            };
        }

        try
        {
            var psi = new ProcessStartInfo
            {
                FileName = exe,
                Arguments = Quote(partPath) + " --output-dir " + Quote(outputDir),
                UseShellExecute = false,
                RedirectStandardInput = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                CreateNoWindow = true
            };
            using var p = Process.Start(psi);
            if (p == null)
                return new { ok = false, message = "鍚姩 PartFeatureAudit 澶辫触" };

            p.StandardInput.WriteLine();
            p.StandardInput.Close();
            var stdoutTask = p.StandardOutput.ReadToEndAsync();
            var stderrTask = p.StandardError.ReadToEndAsync();
            bool exited = p.WaitForExit(180_000);
            if (!exited)
            {
                try { p.Kill(true); } catch { }
                try { p.WaitForExit(10_000); } catch { }
            }
            string stdout = SafeTaskResult(stdoutTask);
            string stderr = SafeTaskResult(stderrTask);

            return new
            {
                ok = exited && p.ExitCode == 0,
                timedOut = !exited,
                exitCode = exited ? p.ExitCode : -1,
                stdoutTail = Tail(stdout, 4000),
                stderrTail = Tail(stderr, 2000)
            };
        }
        catch (Exception ex)
        {
            return new { ok = false, message = ex.Message };
        }
    }

    static string FindPartFeatureAuditExe()
    {
        var candidates = new[]
        {
            Path.Combine(System.Environment.CurrentDirectory, "1", "1", "PartFeatureAudit", "bin", "Release", "net10.0", "PartFeatureAudit.exe"),
            Path.Combine(System.Environment.CurrentDirectory, "1", "1", "PartFeatureAudit", "bin", "Debug", "net10.0", "PartFeatureAudit.exe"),
            Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "PartFeatureAudit", "bin", "Release", "net10.0", "PartFeatureAudit.exe")),
            Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "PartFeatureAudit", "bin", "Debug", "net10.0", "PartFeatureAudit.exe")),
            Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "PartFeatureAudit", "bin", "Release", "net10.0", "PartFeatureAudit.exe")),
            Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "PartFeatureAudit", "bin", "Debug", "net10.0", "PartFeatureAudit.exe"))
        };
        return candidates.FirstOrDefault(File.Exists) ?? "";
    }

    static List<object> BuildExecutableSteps(
        string workflowId,
        string assemblyName,
        List<ComponentInfo> components,
        List<PartKnowledge> parts,
        List<AssemblyComponentPatternInfo> componentPatterns,
        List<object> mates,
        List<Dictionary<string, object>> functionalFaces)
    {
        var steps = new List<object>();
        int generatedIndex = 1;

        AddGeneratedStep(
            steps,
            ref generatedIndex,
            "clear_functional_faces",
            "clear_functional_faces",
            "寮€濮嬭閰嶅墠娓呯┖涓存椂鍔熻兘闈㈠瓨鍌ㄥ簱",
            new Dictionary<string, object>());

        foreach (var part in parts)
        {
            if (part.IsStandardPart)
                continue;

            foreach (var step in ExtractModelingSteps(part.ModelingPlan))
                steps.Add(step);

            AddGeneratedStep(
                steps,
                ref generatedIndex,
                "save_part_to_ai_folder",
                part.PartName,
                "Save rebuilt part for assembly insertion",
                new Dictionary<string, object>
                {
                    ["file_base_name"] = SyntheticPartFileBase(part.PartName, workflowId)
                });
        }

        AddGeneratedStep(
            steps,
            ref generatedIndex,
            "create_new_assembly",
            assemblyName,
            "Create new assembly",
            new Dictionary<string, object>());

        foreach (var component in components
            .Where(IsDirectInsertComponent)
            .OrderBy(c => ComponentIdBaseName(c), StringComparer.OrdinalIgnoreCase)
            .ThenBy(c => OriginalInstanceNumber(c))
            .ThenBy(c => c.ComponentName, StringComparer.OrdinalIgnoreCase))
        {
            var part = parts.FirstOrDefault(p => string.Equals(p.FilePath, component.FilePath, StringComparison.OrdinalIgnoreCase));
            if (part == null) continue;
            string partPath = ResolveInsertPartPath(part, workflowId);

            AddGeneratedStep(
                steps,
                ref generatedIndex,
                "insert_part_into_active_assembly",
                component.ComponentName,
                "鎻掑叆閲嶅缓闆朵欢鍒板綋鍓嶈閰嶄綋",
                new Dictionary<string, object>
                {
                    ["part_path"] = partPath,
                    ["desired_component_id"] = component.ComponentName,
                    ["x"] = component.X,
                    ["y"] = component.Y,
                    ["z"] = component.Z
                });

            AddGeneratedStep(
                steps,
                ref generatedIndex,
                "set_component_transform",
                component.ComponentName,
                "鎸夊師瑁呴厤浣?Transform2 鎭㈠缁勪欢瀹屾暣浣嶅Э",
                new Dictionary<string, object>
                {
                    ["component_id"] = component.ComponentName,
                    ["transform"] = component.Transform
                });
        }

        foreach (var pattern in componentPatterns)
        {
            string skill = PatternSkillName(pattern.PatternType);
            if (string.IsNullOrWhiteSpace(skill))
                continue;

            AddGeneratedStep(
                steps,
                ref generatedIndex,
                skill,
                pattern.PatternName,
                $"Create assembly component pattern/mirror from extracted {pattern.PatternType}",
                BuildPatternStepArgs(pattern));
        }

        foreach (var face in functionalFaces)
        {
            AddGeneratedStep(
                steps,
                ref generatedIndex,
                "register_functional_face",
                face.TryGetValue("role", out var role) ? role?.ToString() ?? "functional_face" : "functional_face",
                "Register functional face inferred from assembly mate",
                new Dictionary<string, object>(face));
        }

        var faceBySource = functionalFaces
            .Where(face => face.TryGetValue("source_feature", out var src) && src != null)
            .ToDictionary(face => face["source_feature"].ToString() ?? "", face => face, StringComparer.OrdinalIgnoreCase);

        foreach (dynamic mate in mates)
        {
            string mateName = Convert.ToString(mate.mateName) ?? "";
            string mateType = MateTypeForSkill(Convert.ToString(mate.mateTypeName) ?? "");
            string alignment = AlignmentForSkill(Convert.ToString(mate.alignmentName) ?? "");
            if (string.IsNullOrWhiteSpace(mateType))
                continue;

            var entities = new List<Dictionary<string, object>>();
            foreach (Dictionary<string, object> entity in mate.entities)
                entities.Add(entity);

            if (entities.Count != 2)
                continue;

            string sourceA = $"mate:{mateName}:0";
            string sourceB = $"mate:{mateName}:1";
            if (!faceBySource.TryGetValue(sourceA, out var faceA) || !faceBySource.TryGetValue(sourceB, out var faceB))
                continue;

            if (string.Equals(mateType, "Gear", StringComparison.OrdinalIgnoreCase))
            {
                if (!TryReadGearMateInfo(mate, out double ratioNumerator, out double ratioDenominator, out bool reverse))
                {
                    Log($"[mates] skip gear mate without extracted IGearMateFeatureData ratio: {mateName}");
                    continue;
                }

                var gearArgs = new Dictionary<string, object>
                {
                    ["mate_type"] = mateType,
                    ["mate_name"] = mateName,
                    ["component_a"] = ReadDictString(faceA, "component_id"),
                    ["axis_point_a_mm"] = ReadDictDoubleArray(faceA, "mate_point_mm"),
                    ["axis_a"] = ReadDictDoubleArray(faceA, "axis"),
                    ["radius_a_mm"] = ReadDictDouble(faceA, "radius_mm"),
                    ["source_feature_a"] = sourceA,
                    ["mate_entity_index_a"] = ReadDictInt(faceA, "mate_entity_index", 0),
                    ["component_b"] = ReadDictString(faceB, "component_id"),
                    ["axis_point_b_mm"] = ReadDictDoubleArray(faceB, "mate_point_mm"),
                    ["axis_b"] = ReadDictDoubleArray(faceB, "axis"),
                    ["radius_b_mm"] = ReadDictDouble(faceB, "radius_mm"),
                    ["source_feature_b"] = sourceB,
                    ["mate_entity_index_b"] = ReadDictInt(faceB, "mate_entity_index", 1),
                    ["ratio_numerator"] = ratioNumerator,
                    ["ratio_denominator"] = ratioDenominator,
                    ["reverse"] = reverse
                };

                AddGeneratedStep(
                    steps,
                    ref generatedIndex,
                    "create_gear_mate_by_axis_signature",
                    mateName,
                    "Create gear mate from extracted IGearMateFeatureData and mate-entity axis signatures",
                    gearArgs);
                continue;
            }

            var mateArgs = new Dictionary<string, object>
            {
                ["mate_type"] = mateType,
                ["component_a"] = faceA.TryGetValue("component_id", out var compA) ? compA?.ToString() ?? "" : "",
                ["face_role_a"] = faceA.TryGetValue("role", out var roleA) ? roleA?.ToString() ?? "" : "",
                ["source_feature_a"] = sourceA,
                ["mate_entity_index_a"] = ReadDictInt(faceA, "mate_entity_index", 0),
                ["side_tag_a"] = ReadDictString(faceA, "side_tag"),
                ["port_role_a"] = ReadDictString(faceA, "port_role"),
                ["counterpart_face_role_a"] = ReadDictString(faceA, "counterpart_role"),
                ["counterpart_side_tag_a"] = ReadDictString(faceA, "counterpart_side_tag"),
                ["component_b"] = faceB.TryGetValue("component_id", out var compB) ? compB?.ToString() ?? "" : "",
                ["face_role_b"] = faceB.TryGetValue("role", out var roleB) ? roleB?.ToString() ?? "" : "",
                ["source_feature_b"] = sourceB,
                ["mate_entity_index_b"] = ReadDictInt(faceB, "mate_entity_index", 1),
                ["side_tag_b"] = ReadDictString(faceB, "side_tag"),
                ["port_role_b"] = ReadDictString(faceB, "port_role"),
                ["counterpart_face_role_b"] = ReadDictString(faceB, "counterpart_role"),
                ["counterpart_side_tag_b"] = ReadDictString(faceB, "counterpart_side_tag"),
                ["alignment"] = alignment
            };

            if (string.Equals(mateType, "Distance", StringComparison.OrdinalIgnoreCase))
            {
                if (!TryReadMateDistanceMm(mate, out double distanceMm))
                {
                    Log($"[mates] skip distance mate without extracted distance: {mateName}");
                    continue;
                }

                mateArgs["distance_mm"] = distanceMm;
                if (TryReadMateDistanceFlipDimension(mate, out bool flipDimension))
                    mateArgs["flip_dimension"] = flipDimension;
            }

            AddGeneratedStep(
                steps,
                ref generatedIndex,
                "create_mate_by_functional_face",
                mateName,
                "鎸夊姛鑳介潰鍒涘缓瑁呴厤閰嶅悎",
                mateArgs);
        }

        AddGeneratedStep(
            steps,
            ref generatedIndex,
            "save_step_to_ai_folder",
            assemblyName,
            "瀵煎嚭閲嶅缓瑁呴厤浣?STEP 渚?ANSYS 浠跨湡浣跨敤",
            new Dictionary<string, object>
            {
                ["file_base_name"] = MakeAssemblyStepFileBase(assemblyName)
            });

        return steps;
    }

    static string PatternSkillName(string patternType)
    {
        if (string.Equals(patternType, "local_linear_pattern", StringComparison.OrdinalIgnoreCase))
            return "create_local_linear_component_pattern";
        if (string.Equals(patternType, "local_circular_pattern", StringComparison.OrdinalIgnoreCase))
            return "create_local_circular_component_pattern";
        if (string.Equals(patternType, "mirror_component", StringComparison.OrdinalIgnoreCase))
            return "create_mirror_components";
        return "";
    }

    static Dictionary<string, object> BuildPatternStepArgs(AssemblyComponentPatternInfo pattern)
    {
        var args = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
        {
            ["pattern_name"] = pattern.PatternName,
            ["pattern_type"] = pattern.PatternType,
            ["feature_type_name"] = pattern.FeatureTypeName,
            ["seed_components"] = pattern.SeedComponents,
            ["generated_components"] = pattern.GeneratedComponents,
            ["skipped_items"] = pattern.SkippedItems,
            ["parameters"] = pattern.Parameters
        };

        foreach (var kv in pattern.Parameters)
            args[ToSnakeCase(kv.Key)] = kv.Value;

        return args;
    }

    static string ToSnakeCase(string value)
    {
        if (string.IsNullOrWhiteSpace(value))
            return "";

        var sb = new StringBuilder();
        for (int i = 0; i < value.Length; i++)
        {
            char c = value[i];
            if (char.IsUpper(c) && i > 0 && value[i - 1] != '_')
                sb.Append('_');
            sb.Append(char.ToLowerInvariant(c));
        }
        return sb.ToString();
    }

    static List<Dictionary<string, object>> BuildFunctionalFaceRepository(List<object> mates)
    {
        var faces = new List<Dictionary<string, object>>();
        foreach (dynamic mate in mates)
        {
            try
            {
                var entities = new List<Dictionary<string, object>>();
                foreach (var entity in mate.entities)
                    if (entity is Dictionary<string, object> dict)
                        entities.Add(dict);

                if (entities.Count == 0)
                    continue;

                var mateFaces = new List<Dictionary<string, object>>();
                for (int i = 0; i < entities.Count; i++)
                {
                    var entity = entities[i];
                    var counterpart = entities.Count == 2 ? entities[1 - i] : null;
                    string entityKind = ReadDictString(entity, "entity_kind");
                    bool hasFace = entity.TryGetValue("face_signature", out var sigObj) && sigObj is Dictionary<string, object>;
                    bool hasEdge = entity.TryGetValue("edge_signature", out var edgeObj) && edgeObj is Dictionary<string, object>;
                    if (!hasFace && !hasEdge)
                        continue;

                    var roles = ToStringList(entity.TryGetValue("role_candidates", out var rolesObj) ? rolesObj : null);
                    string role = ChooseVerifiedFaceRole(entity, counterpart, SafeInt(() => Convert.ToInt32(mate.mateType)), roles);
                    if (string.IsNullOrWhiteSpace(role))
                        continue;

                    string sideTag = InferSideTag(entity);
                    var face = new Dictionary<string, object>
                    {
                        ["source_mate"] = Convert.ToString(mate.mateName) ?? "",
                        ["source_feature"] = $"mate:{Convert.ToString(mate.mateName) ?? ""}:{i}",
                        ["mate_type"] = MateTypeForSkill(Convert.ToString(mate.mateTypeName) ?? ""),
                        ["alignment"] = AlignmentForSkill(Convert.ToString(mate.alignmentName) ?? ""),
                        ["mate_entity_index"] = ReadDictInt(entity, "mate_entity_index", i),
                        ["component_id"] = ReadDictString(entity, "component"),
                        ["part_name"] = ReadDictString(entity, "part_name"),
                        ["part_path"] = ReadDictString(entity, "component_path"),
                        ["entity_kind"] = entityKind,
                        ["role"] = role,
                        ["side_tag"] = sideTag,
                        ["port_role"] = string.IsNullOrWhiteSpace(sideTag) ? role : $"{role}:{sideTag}",
                        ["source"] = "verified_from_assembly_mate",
                        ["verified"] = true,
                        ["confidence"] = 1.0,
                        ["geometry_coordinate_system"] = "part_model",
                        ["evidence"] = entity.TryGetValue("role_evidence", out var evidence) ? evidence : Array.Empty<string>()
                    };
                    CopyIfExists(entity, face, "original_component", "original_component_id");

                    CopySignature(entity, face, hasFace ? "face_signature" : "edge_signature");

                    if (counterpart != null)
                    {
                        var counterpartRoles = ToStringList(counterpart.TryGetValue("role_candidates", out var counterpartRolesObj) ? counterpartRolesObj : null);
                        string counterpartRole = ChooseVerifiedFaceRole(counterpart, entity, SafeInt(() => Convert.ToInt32(mate.mateType)), counterpartRoles);
                        string counterpartSideTag = InferSideTag(counterpart);
                        face["counterpart_component_id"] = ReadDictString(counterpart, "component");
                        CopyIfExists(counterpart, face, "original_component", "original_counterpart_component_id");
                        face["counterpart_part_name"] = ReadDictString(counterpart, "part_name");
                        face["counterpart_part_path"] = ReadDictString(counterpart, "component_path");
                        face["counterpart_entity_kind"] = ReadDictString(counterpart, "entity_kind");
                        face["counterpart_role"] = counterpartRole;
                        face["counterpart_side_tag"] = counterpartSideTag;
                        face["counterpart_port_role"] = string.IsNullOrWhiteSpace(counterpartSideTag) ? counterpartRole : $"{counterpartRole}:{counterpartSideTag}";
                    }

                    if (TryReadMateDistanceMm(mate, out double distanceMm))
                        face["distance_mm"] = distanceMm;
                    if (TryReadMateDistanceFlipDimension(mate, out bool flipDimension))
                        face["flip_dimension"] = flipDimension;

                    mateFaces.Add(face);
                }

                HarmonizeMateFaceSides(mateFaces);
                faces.AddRange(mateFaces);
            }
            catch { }
        }

        return DeduplicateDictionaries(faces, FunctionalFaceKey);
    }

    static void CopySignature(Dictionary<string, object> entity, Dictionary<string, object> face, string signatureKey)
    {
        if (!entity.TryGetValue(signatureKey, out var sigObj) || sigObj is not Dictionary<string, object> sig)
            return;

        CopyIfExists(sig, face, "surface_type", "surface_type");
        CopyIfExists(sig, face, "curve_type", "curve_type");
        CopyIfExists(sig, face, "area_mm2", "area_mm2");
        CopyIfExists(sig, face, "center_mm", "center_mm");
        CopyIfExists(sig, face, "start_mm", "start_mm");
        CopyIfExists(sig, face, "end_mm", "end_mm");
        CopyIfExists(sig, face, "line_point_mm", "line_point_mm");
        CopyIfExists(sig, face, "line_direction", "line_direction");
        CopyIfExists(sig, face, "length_mm", "length_mm");
        CopyIfExists(sig, face, "box_mm", "bbox_mm");
        CopyIfExists(sig, face, "axis", "axis");
        CopyIfExists(sig, face, "normal", "normal");
        CopyIfExists(sig, face, "origin_mm", "origin_mm");
        CopyIfExists(sig, face, "axis_point_mm", "axis_point_mm");
        CopyIfExists(sig, face, "axis_line_point_mm", "axis_line_point_mm");
        CopyIfExists(sig, face, "axial_center_mm", "axial_center_mm");
        CopyIfExists(sig, face, "axial_min_mm", "axial_min_mm");
        CopyIfExists(sig, face, "axial_max_mm", "axial_max_mm");
        CopyIfExists(sig, face, "radius_mm", "radius_mm");
        CopyIfExists(sig, face, "source_reference", "source_reference");
        CopyIfExists(sig, face, "source_feature", "solidworks_source_feature");

        if (sig.TryGetValue("radius_mm", out var radiusObj))
        {
            try
            {
                face["diameter_mm"] = R(Convert.ToDouble(radiusObj) * 2.0);
            }
            catch { }
        }
    }

    static string FunctionalFaceKey(Dictionary<string, object> face)
    {
        return string.Join("|",
            ReadDictString(face, "source_mate"),
            ReadDictString(face, "mate_entity_index"),
            ReadDictString(face, "component_id"),
            ReadDictString(face, "part_path"),
            ReadDictString(face, "role"),
            ReadDictString(face, "side_tag"),
            ReadDictString(face, "surface_type"),
            ReadDictString(face, "curve_type"),
            ReadDictString(face, "area_mm2"),
            ReadDictString(face, "center_mm"),
            ReadDictString(face, "axis"),
            ReadDictString(face, "normal"));
    }

    static void BackfillPartFunctionalKnowledge(List<PartKnowledge> parts, List<Dictionary<string, object>> functionalFaces)
    {
        foreach (var part in parts)
        {
            var related = functionalFaces
                .Where(face => SamePath(ReadDictString(face, "part_path"), part.FilePath)
                    || string.Equals(ReadDictString(face, "part_name"), part.PartName, StringComparison.OrdinalIgnoreCase))
                .ToList();

            part.FunctionalFaces = DeduplicateDictionaries(related.Select(face =>
            {
                var result = new Dictionary<string, object>
                {
                    ["face_role"] = ReadDictString(face, "role"),
                    ["surface_type"] = ReadDictString(face, "surface_type"),
                    ["side_tag"] = ReadDictString(face, "side_tag"),
                    ["source"] = "verified_from_assembly_mate",
                    ["verified"] = true,
                    ["confidence"] = 1.0,
                    ["source_assembly_component"] = ReadDictString(face, "component_id"),
                    ["source_mate"] = ReadDictString(face, "source_mate"),
                    ["mate_entity_index"] = ReadDictInt(face, "mate_entity_index", -1),
                    ["source_feature"] = ReadDictString(face, "source_feature"),
                    ["solidworks_source_feature"] = ReadDictString(face, "solidworks_source_feature")
                };
                CopyIfExists(face, result, "original_component_id", "original_source_assembly_component");

                CopyIfExists(face, result, "radius_mm", "radius_mm");
                CopyIfExists(face, result, "diameter_mm", "diameter_mm");
                CopyIfExists(face, result, "area_mm2", "area_mm2");
                CopyIfExists(face, result, "center_mm", "center_mm");
                CopyIfExists(face, result, "mate_point_mm", "mate_point_mm");
                CopyIfExists(face, result, "distance_mm", "distance_mm");
                CopyIfExists(face, result, "flip_dimension", "flip_dimension");
                CopyIfExists(face, result, "axis", "axis");
                CopyIfExists(face, result, "normal", "normal");
                CopyIfExists(face, result, "origin_mm", "origin_mm");
                CopyIfExists(face, result, "axis_point_mm", "axis_point_mm");
                CopyIfExists(face, result, "axis_line_point_mm", "axis_line_point_mm");
                CopyIfExists(face, result, "axial_center_mm", "axial_center_mm");
                CopyIfExists(face, result, "axial_min_mm", "axial_min_mm");
                CopyIfExists(face, result, "axial_max_mm", "axial_max_mm");
                CopyIfExists(face, result, "bbox_mm", "bbox_mm");
                CopyIfExists(face, result, "geometry_coordinate_system", "geometry_coordinate_system");
                CopyIfExists(face, result, "source_reference", "source_reference");
                CopyIfExists(face, result, "evidence", "evidence");
                return result;
            }), PartFaceKey);

            part.MatePorts = DeduplicateDictionaries(related.Select(face =>
            {
                var port = new Dictionary<string, object>
                {
                    ["port_role"] = ReadDictString(face, "port_role"),
                    ["local_face_role"] = ReadDictString(face, "role"),
                    ["local_side_tag"] = ReadDictString(face, "side_tag"),
                    ["mate_type"] = ReadDictString(face, "mate_type"),
                    ["alignment"] = ReadDictString(face, "alignment"),
                    ["counterpart_part_name"] = ReadDictString(face, "counterpart_part_name"),
                    ["counterpart_part_path"] = ReadDictString(face, "counterpart_part_path"),
                    ["counterpart_face_role"] = ReadDictString(face, "counterpart_role"),
                    ["counterpart_side_tag"] = ReadDictString(face, "counterpart_side_tag"),
                    ["counterpart_port_role"] = ReadDictString(face, "counterpart_port_role"),
                    ["source"] = "verified_from_assembly_mate",
                    ["verified"] = true,
                    ["confidence"] = 1.0,
                    ["source_assembly_component"] = ReadDictString(face, "component_id"),
                    ["source_mate"] = ReadDictString(face, "source_mate"),
                    ["mate_entity_index"] = ReadDictInt(face, "mate_entity_index", -1)
                };
                CopyIfExists(face, port, "original_component_id", "original_source_assembly_component");
                CopyIfExists(face, port, "distance_mm", "distance_mm");
                CopyIfExists(face, port, "flip_dimension", "flip_dimension");
                return port;
            }), PartPortKey);
        }
    }

    static void NormalizeStandardPartFunctionalFacePaths(List<PartKnowledge> parts, List<Dictionary<string, object>> functionalFaces)
    {
        foreach (var face in functionalFaces)
        {
            var part = FindPartForFace(parts, face, "part_path", "part_name");
            if (part?.IsStandardPart == true)
            {
                string originalPath = ReadDictString(face, "part_path");
                string insertPath = StandardPartInsertPath(part);
                if (!string.IsNullOrWhiteSpace(originalPath) && !SamePath(originalPath, insertPath))
                    face["original_part_path"] = originalPath;
                face["part_path"] = insertPath;
                face["standard_part"] = true;
                face["standard_rule"] = part.StandardRule;
                face["direct_insert_path"] = insertPath;
            }

            var counterpart = FindPartForFace(parts, face, "counterpart_part_path", "counterpart_part_name");
            if (counterpart?.IsStandardPart == true)
            {
                string originalPath = ReadDictString(face, "counterpart_part_path");
                string insertPath = StandardPartInsertPath(counterpart);
                if (!string.IsNullOrWhiteSpace(originalPath) && !SamePath(originalPath, insertPath))
                    face["original_counterpart_part_path"] = originalPath;
                face["counterpart_part_path"] = insertPath;
                face["counterpart_standard_part"] = true;
                face["counterpart_direct_insert_path"] = insertPath;
            }
        }
    }

    static PartKnowledge FindPartForFace(List<PartKnowledge> parts, Dictionary<string, object> face, string pathKey, string nameKey)
    {
        string path = ReadDictString(face, pathKey);
        string name = ReadDictString(face, nameKey);
        return parts.FirstOrDefault(part =>
            SamePath(path, part.FilePath) ||
            SamePath(path, part.DirectInsertPath) ||
            string.Equals(name, part.PartName, StringComparison.OrdinalIgnoreCase));
    }

    static void PersistEnrichedPartModelingPlans(List<PartKnowledge> parts, string assemblyName, string asmPath)
    {
        foreach (var part in parts)
        {
            if (string.IsNullOrWhiteSpace(part.ModelingPlanPath) || !File.Exists(part.ModelingPlanPath))
                continue;

            var enriched = new
            {
                schemaVersion = "part_modeling_plan_with_functional_faces.v1",
                partName = part.PartName,
                partPath = part.FilePath,
                sourceAssembly = new
                {
                    name = assemblyName,
                    path = asmPath
                },
                modelingPlan = ExtractModelingPlanPayload(part.ModelingPlan),
                functionalFaces = part.FunctionalFaces,
                matePorts = part.MatePorts,
                note = "functionalFaces and matePorts are verified from assembly mates and written back to the executable part modeling plan for knowledge-base reuse."
            };

            File.WriteAllText(part.ModelingPlanPath, JsonSerializer.Serialize(enriched, JsonOptions));
            part.ModelingPlan = ReadJsonOrNull(part.ModelingPlanPath);
        }
    }

    static object ExtractModelingPlanPayload(object modelingPlan)
    {
        if (modelingPlan is JsonElement root &&
            root.ValueKind == JsonValueKind.Object &&
            root.TryGetProperty("modelingPlan", out var plan) &&
            plan.ValueKind == JsonValueKind.Object)
        {
            return plan.Clone();
        }

        return modelingPlan;
    }

    static void AddGeneratedStep(List<object> steps, ref int index, string skillName, string name, string description, Dictionary<string, object> args)
    {
        string id = $"assembly-generated-{index:D4}";
        index++;
        steps.Add(new
        {
            id,
            targetAgent = "solidworks",
            skillName,
            name,
            description,
            arguments = args,
            dependencies = Array.Empty<string>(),
            acceptance = new Dictionary<string, object>()
        });
    }

    static List<object> ExtractModelingSteps(object modelingPlan)
    {
        var steps = new List<object>();
        if (modelingPlan is not JsonElement root)
            return steps;

        if (!root.TryGetProperty("modelingPlan", out var plan) || plan.ValueKind != JsonValueKind.Object)
            return steps;

        if (!plan.TryGetProperty("steps", out var stepArray) || stepArray.ValueKind != JsonValueKind.Array)
            return steps;

        var createdRefPlanes = CollectCreatedRefPlaneNames(stepArray);
        foreach (var step in stepArray.EnumerateArray())
            steps.Add(NormalizeExecutablePartStep(step, createdRefPlanes));

        return steps;
    }

    static HashSet<string> CollectCreatedRefPlaneNames(JsonElement stepArray)
    {
        var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        if (stepArray.ValueKind != JsonValueKind.Array)
            return names;

        foreach (var step in stepArray.EnumerateArray())
        {
            string skillName = step.TryGetProperty("skillName", out var skill) ? skill.GetString() ?? "" : "";
            if (!string.Equals(skillName, "create_offset_plane_mm", StringComparison.OrdinalIgnoreCase) &&
                !string.Equals(skillName, "create_angle_plane_by_edge_and_face", StringComparison.OrdinalIgnoreCase))
                continue;

            if (step.TryGetProperty("arguments", out var args) &&
                args.ValueKind == JsonValueKind.Object &&
                args.TryGetProperty("plane_name", out var plane))
            {
                string planeName = plane.GetString() ?? "";
                if (!string.IsNullOrWhiteSpace(planeName))
                    names.Add(planeName);
            }
        }

        return names;
    }

    static object NormalizeExecutablePartStep(JsonElement step, HashSet<string> createdRefPlanes)
    {
        string skillName = step.TryGetProperty("skillName", out var skill) ? skill.GetString() ?? "" : "";
        if (!string.Equals(skillName, "create_ref_plane_sketch_by_name", StringComparison.OrdinalIgnoreCase))
            return step.Clone();

        string planeName = "";
        if (step.TryGetProperty("arguments", out var args) &&
            args.ValueKind == JsonValueKind.Object &&
            args.TryGetProperty("plane_name", out var plane))
        {
            planeName = plane.GetString() ?? "";
        }

        return step.Clone();
    }

    static void CopyFaceSignature(Dictionary<string, object> entity, Dictionary<string, object> target)
    {
        if (!entity.TryGetValue("face_signature", out var sigObj) || sigObj is not Dictionary<string, object> sig)
            return;

        CopyIfExists(sig, target, "surface_type", "surface_type");
        CopyIfExists(sig, target, "area_mm2", "area_mm2");
        CopyIfExists(sig, target, "center_mm", "center_mm");
        CopyIfExists(sig, target, "axis", "axis");
        CopyIfExists(sig, target, "normal", "normal");
        CopyIfExists(sig, target, "origin_mm", "origin_mm");
        CopyIfExists(sig, target, "axis_point_mm", "axis_point_mm");
        CopyIfExists(sig, target, "axis_line_point_mm", "axis_line_point_mm");
        CopyIfExists(sig, target, "axial_center_mm", "axial_center_mm");
        CopyIfExists(sig, target, "axial_min_mm", "axial_min_mm");
        CopyIfExists(sig, target, "axial_max_mm", "axial_max_mm");
        CopyIfExists(sig, target, "radius_mm", "radius_mm");
        CopyIfExists(sig, target, "box_mm", "bbox_mm");
        CopyIfExists(sig, target, "geometry_coordinate_system", "geometry_coordinate_system");
        CopyIfExists(sig, target, "source_reference", "source_reference");
        CopyIfExists(sig, target, "source_feature", "solidworks_source_feature");
    }

    static void CopyIfExists(Dictionary<string, object> source, Dictionary<string, object> target, string sourceKey, string targetKey)
    {
        if (source.TryGetValue(sourceKey, out var value) && value != null)
            target[targetKey] = value;
    }

    static string ChoosePrimaryRole(List<string> roles)
    {
        string[] preferred =
        {
            "oil_ring_mount_face",
            "oil_ring_axial_stop_face",
            "bearing_mount_face",
            "key_contact_or_keyway_face",
            "key_mate_entity",
            "distance_mate_face",
            "gear_mate_axis",
            "cylindrical_mate_face",
            "planar_contact_face",
            "coincident_mate_entity"
        };

        foreach (string role in preferred)
            if (roles.Any(item => string.Equals(item, role, StringComparison.OrdinalIgnoreCase)))
                return role;

        return roles.FirstOrDefault() ?? "";
    }

    static string ChooseVerifiedFaceRole(Dictionary<string, object> entity, Dictionary<string, object> counterpart, int mateType, List<string> roles)
    {
        bool thisIsKey = IsKeyLike(entity);
        bool otherIsKey = counterpart != null && IsKeyLike(counterpart);
        bool thisIsOilRing = IsOilRingLike(entity);
        bool otherIsOilRing = counterpart != null && IsOilRingLike(counterpart);

        if (mateType == (int)swMateType_e.swMateCONCENTRIC && (thisIsKey || otherIsKey))
            return thisIsKey ? "key_end_arc_face" : "keyway_end_arc_face";

        if (mateType == (int)swMateType_e.swMateCOINCIDENT && (thisIsKey || otherIsKey))
            return thisIsKey ? KeyPlaneRole(entity) : KeywayPlaneRole(entity);

        if (mateType == (int)swMateType_e.swMateCONCENTRIC && (thisIsOilRing || otherIsOilRing))
            return thisIsOilRing ? "oil_ring_inner_cylindrical_face" : "oil_ring_mount_face";

        if (mateType == (int)swMateType_e.swMateCOINCIDENT && (thisIsOilRing || otherIsOilRing))
            return thisIsOilRing ? "oil_ring_planar_contact_face" : "oil_ring_axial_stop_face";

        if (mateType == (int)swMateType_e.swMateDISTANCE)
            return "distance_mate_face";

        if (mateType == (int)swMateType_e.swMateGEAR)
            return "gear_mate_axis";

        return ChoosePrimaryRole(roles);
    }

    static string InferSideTag(Dictionary<string, object> entity)
    {
        var center = SignatureDoubleArray(entity, "center_mm");
        if (center.Length < 3)
            return "";

        if (string.Equals(SignatureString(entity, "surface_type"), "plane", StringComparison.OrdinalIgnoreCase))
        {
            int planeAxis = PlanarConstantAxis(entity);
            if (planeAxis >= 0)
                return SideName(planeAxis, center[planeAxis]);

            int normalAxis = DominantNormalAxis(entity);
            if (normalAxis >= 0)
                return SideName(normalAxis, center[normalAxis]);
        }

        int axis = 0;
        double best = Math.Abs(center[0]);
        for (int i = 1; i < 3; i++)
        {
            double value = Math.Abs(center[i]);
            if (value > best)
            {
                best = value;
                axis = i;
            }
        }

        if (best < 1e-6)
            return "";

        double signed = center[axis];
        return SideName(axis, signed);
    }

    static int DominantNormalAxis(Dictionary<string, object> entity)
    {
        var normal = SignatureDoubleArray(entity, "normal");
        if (normal.Length < 3)
            return -1;

        int axis = 0;
        double best = Math.Abs(normal[0]);
        for (int i = 1; i < 3; i++)
        {
            double value = Math.Abs(normal[i]);
            if (value > best)
            {
                best = value;
                axis = i;
            }
        }

        return best >= 0.7 ? axis : -1;
    }

    static string KeyPlaneRole(Dictionary<string, object> entity)
    {
        int axis = PlanarConstantAxis(entity);
        return axis switch
        {
            1 => "key_side_contact_face",
            2 => "key_seating_face",
            _ => "key_planar_contact_face"
        };
    }

    static string KeywayPlaneRole(Dictionary<string, object> entity)
    {
        int axis = PlanarConstantAxis(entity);
        return axis switch
        {
            0 => "keyway_side_contact_face",
            1 => "keyway_bottom_contact_face",
            2 => "keyway_depth_contact_face",
            _ => "keyway_planar_contact_face"
        };
    }

    static string SideName(int axis, double signed)
    {
        return axis switch
        {
            0 => signed >= 0 ? "right" : "left",
            1 => signed >= 0 ? "front" : "back",
            2 => signed >= 0 ? "top" : "bottom",
            _ => ""
        };
    }

    static int PlanarConstantAxis(Dictionary<string, object> entity)
    {
        var box = SignatureDoubleArray(entity, "box_mm");
        if (box.Length < 6)
            return -1;

        var extents = new[]
        {
            Math.Abs(box[3] - box[0]),
            Math.Abs(box[4] - box[1]),
            Math.Abs(box[5] - box[2])
        };

        int axis = 0;
        double best = extents[0];
        for (int i = 1; i < 3; i++)
        {
            if (extents[i] < best)
            {
                best = extents[i];
                axis = i;
            }
        }

        return best <= 0.05 ? axis : -1;
    }

    static void HarmonizeMateFaceSides(List<Dictionary<string, object>> mateFaces)
    {
        if (mateFaces.Count != 2)
            return;

        string sideA = ReadDictString(mateFaces[0], "side_tag");
        string sideB = ReadDictString(mateFaces[1], "side_tag");
        string roleA = ReadDictString(mateFaces[0], "role");
        string roleB = ReadDictString(mateFaces[1], "role");
        HarmonizeSideTags(roleA, roleB, ref sideA, ref sideB);
        SetSideTag(mateFaces[0], roleA, sideA);
        SetSideTag(mateFaces[1], roleB, sideB);
    }

    static void HarmonizeSideTags(string roleA, string roleB, ref string sideA, ref string sideB)
    {
        bool keyArcA = string.Equals(roleA, "key_end_arc_face", StringComparison.OrdinalIgnoreCase);
        bool keyArcB = string.Equals(roleB, "key_end_arc_face", StringComparison.OrdinalIgnoreCase);
        bool keywayArcA = string.Equals(roleA, "keyway_end_arc_face", StringComparison.OrdinalIgnoreCase);
        bool keywayArcB = string.Equals(roleB, "keyway_end_arc_face", StringComparison.OrdinalIgnoreCase);

        if (keyArcA && keywayArcB && IsLeftRight(sideA))
            sideB = sideA;
        else if (keyArcB && keywayArcA && IsLeftRight(sideB))
            sideA = sideB;
    }

    static bool IsLeftRight(string value)
    {
        return string.Equals(value, "left", StringComparison.OrdinalIgnoreCase)
            || string.Equals(value, "right", StringComparison.OrdinalIgnoreCase);
    }

    static void SetSideTag(Dictionary<string, object> face, string role, string sideTag)
    {
        if (string.IsNullOrWhiteSpace(sideTag))
        {
            face.Remove("side_tag");
            face["port_role"] = role;
            return;
        }

        face["side_tag"] = sideTag;
        face["port_role"] = $"{role}:{sideTag}";
    }

    static double[] SignatureDoubleArray(Dictionary<string, object> entity, string key)
    {
        if (!entity.TryGetValue("face_signature", out var sigObj) || sigObj is not Dictionary<string, object> sig)
        {
            if (!entity.TryGetValue("edge_signature", out sigObj) || sigObj is not Dictionary<string, object> edgeSig)
                return Array.Empty<double>();
            else
            {
                sig = edgeSig;
            }
        }

        return sig.TryGetValue(key, out var value) ? ToDoubleArray(value) : Array.Empty<double>();
    }

    static string SignatureString(Dictionary<string, object> entity, string key)
    {
        if (!entity.TryGetValue("face_signature", out var sigObj) || sigObj is not Dictionary<string, object> sig)
        {
            if (!entity.TryGetValue("edge_signature", out sigObj) || sigObj is not Dictionary<string, object> edgeSig)
                return "";
            else
            {
                sig = edgeSig;
            }
        }

        return sig.TryGetValue(key, out var value) ? value?.ToString() ?? "" : "";
    }

    static bool IsKeyLike(Dictionary<string, object> entity)
    {
        string text = EntityText(entity);
        return ContainsAny(text, "\u952e", "key", "1096");
    }

    static bool IsOilRingLike(Dictionary<string, object> entity)
    {
        string text = EntityText(entity);
        return ContainsAny(text, "\u6321\u6cb9\u73af", "oil", "ring");
    }

    static string EntityText(Dictionary<string, object> entity)
    {
        return $"{ReadDictString(entity, "component")} {ReadDictString(entity, "part_name")} {ReadDictString(entity, "component_path")}";
    }

    static string ReadDictString(Dictionary<string, object> dict, string key)
    {
        if (dict == null || !dict.TryGetValue(key, out var value) || value == null)
            return "";

        return value.ToString() ?? "";
    }

    static int ReadDictInt(Dictionary<string, object> dict, string key, int fallback = -1)
    {
        if (dict == null || !dict.TryGetValue(key, out var value) || value == null)
            return fallback;
        try
        {
            return Convert.ToInt32(value);
        }
        catch
        {
            return int.TryParse(value.ToString(), out int parsed) ? parsed : fallback;
        }
    }

    static double ReadDictDouble(Dictionary<string, object> dict, string key, double fallback = 0.0)
    {
        if (dict == null || !dict.TryGetValue(key, out var value) || value == null)
            return fallback;

        try
        {
            if (value is double d) return d;
            if (value is float f) return f;
            if (value is int i) return i;
            if (value is long l) return l;
            if (value is decimal m) return (double)m;
            return double.TryParse(value.ToString(), out double parsed) ? parsed : fallback;
        }
        catch
        {
            return fallback;
        }
    }

    static double[] ReadDictDoubleArray(Dictionary<string, object> dict, string key)
    {
        if (dict == null || !dict.TryGetValue(key, out var value) || value == null)
            return Array.Empty<double>();

        try
        {
            if (value is double[] doubles)
                return doubles;
            if (value is IEnumerable<double> doubleEnumerable)
                return doubleEnumerable.ToArray();
            if (value is Array array)
                return array.Cast<object>()
                    .Select(item => item == null ? (double?)null : double.TryParse(item.ToString(), out double parsed) ? parsed : null)
                    .Where(item => item.HasValue)
                    .Select(item => item!.Value)
                    .ToArray();
            if (value is IEnumerable<object> objectEnumerable)
                return objectEnumerable
                    .Select(item => item == null ? (double?)null : double.TryParse(item.ToString(), out double parsed) ? parsed : null)
                    .Where(item => item.HasValue)
                    .Select(item => item!.Value)
                    .ToArray();
        }
        catch { }

        return Array.Empty<double>();
    }

    static bool TryReadMateDistanceMm(object mate, out double distanceMm)
    {
        distanceMm = 0.0;
        if (mate == null)
            return false;

        try
        {
            object value = mate.GetType().GetProperty("distanceMm")?.GetValue(mate);
            if (value == null)
                return false;

            if (value is double direct)
            {
                distanceMm = direct;
                return !double.IsNaN(distanceMm) && !double.IsInfinity(distanceMm);
            }

            distanceMm = Convert.ToDouble(value);
            return !double.IsNaN(distanceMm) && !double.IsInfinity(distanceMm);
        }
        catch
        {
            return false;
        }
    }

    static bool TryReadMateDistanceFlipDimension(object mate, out bool flipDimension)
    {
        flipDimension = false;
        if (mate == null)
            return false;

        try
        {
            object value =
                mate.GetType().GetProperty("distanceFlipDimension")?.GetValue(mate) ??
                mate.GetType().GetProperty("flipDimension")?.GetValue(mate);
            if (value == null)
                return false;

            if (value is bool direct)
            {
                flipDimension = direct;
                return true;
            }

            if (value is int i)
            {
                flipDimension = i != 0;
                return true;
            }

            if (value is long l)
            {
                flipDimension = l != 0;
                return true;
            }

            if (value is double d)
            {
                flipDimension = Math.Abs(d) > 1e-12;
                return !double.IsNaN(d) && !double.IsInfinity(d);
            }

            if (bool.TryParse(value.ToString(), out bool parsedBool))
            {
                flipDimension = parsedBool;
                return true;
            }

            if (double.TryParse(value.ToString(), out double parsedNumber))
            {
                flipDimension = Math.Abs(parsedNumber) > 1e-12;
                return !double.IsNaN(parsedNumber) && !double.IsInfinity(parsedNumber);
            }
        }
        catch
        {
        }

        return false;
    }

    static bool TryReadGearMateInfo(object mate, out double ratioNumerator, out double ratioDenominator, out bool reverse)
    {
        ratioNumerator = 0.0;
        ratioDenominator = 0.0;
        reverse = false;

        if (mate == null)
            return false;

        try
        {
            object gearObj = mate.GetType().GetProperty("gearMate")?.GetValue(mate);
            if (gearObj is not Dictionary<string, object> gear || gear.Count == 0)
                return false;

            ratioNumerator = ReadDictDouble(gear, "ratio_numerator");
            ratioDenominator = ReadDictDouble(gear, "ratio_denominator");
            reverse = ReadDictBool(gear, "reverse");

            return !double.IsNaN(ratioNumerator)
                && !double.IsNaN(ratioDenominator)
                && !double.IsInfinity(ratioNumerator)
                && !double.IsInfinity(ratioDenominator)
                && Math.Abs(ratioNumerator) > 1e-9
                && Math.Abs(ratioDenominator) > 1e-9;
        }
        catch
        {
            return false;
        }
    }

    static bool ReadDictBool(Dictionary<string, object> dict, string key, bool fallback = false)
    {
        if (dict == null || !dict.TryGetValue(key, out var value) || value == null)
            return fallback;

        try
        {
            if (value is bool b) return b;
            if (value is int i) return i != 0;
            if (value is long l) return l != 0;
            if (value is double d) return Math.Abs(d) > 1e-12;
            return bool.TryParse(value.ToString(), out bool parsed) ? parsed : fallback;
        }
        catch
        {
            return fallback;
        }
    }

    static bool SamePath(string a, string b)
    {
        if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b))
            return false;

        try
        {
            return string.Equals(Path.GetFullPath(a), Path.GetFullPath(b), StringComparison.OrdinalIgnoreCase);
        }
        catch
        {
            return string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
        }
    }

    static List<Dictionary<string, object>> DeduplicateDictionaries(IEnumerable<Dictionary<string, object>> items, Func<Dictionary<string, object>, string> keySelector)
    {
        var result = new List<Dictionary<string, object>>();
        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (var item in items)
        {
            string key = keySelector(item);
            if (seen.Add(key))
                result.Add(item);
        }

        return result;
    }

    static string PartFaceKey(Dictionary<string, object> face)
    {
        return string.Join("|",
            ReadDictString(face, "face_role"),
            ReadDictString(face, "side_tag"),
            ReadDictString(face, "surface_type"),
            ReadDictString(face, "radius_mm"),
            ReadDictString(face, "center_mm"),
            ReadDictString(face, "source_mate"),
            ReadDictString(face, "mate_entity_index"));
    }

    static string PartPortKey(Dictionary<string, object> port)
    {
        return string.Join("|",
            ReadDictString(port, "local_face_role"),
            ReadDictString(port, "local_side_tag"),
            ReadDictString(port, "mate_type"),
            ReadDictString(port, "counterpart_part_name"),
            ReadDictString(port, "counterpart_face_role"),
            ReadDictString(port, "counterpart_side_tag"),
            ReadDictString(port, "source_mate"),
            ReadDictString(port, "mate_entity_index"));
    }

    static List<string> ToStringList(object value)
    {
        if (value == null) return new List<string>();
        if (value is List<string> typed) return typed;
        if (value is IEnumerable<string> strings) return strings.ToList();
        if (value is Array array) return array.Cast<object>().Select(item => item?.ToString() ?? "").Where(item => item.Length > 0).ToList();
        return new List<string> { value.ToString() ?? "" }.Where(item => item.Length > 0).ToList();
    }

    static string MateTypeForSkill(string mateTypeName)
    {
        if (mateTypeName.Contains("CONCENTRIC", StringComparison.OrdinalIgnoreCase)) return "Concentric";
        if (mateTypeName.Contains("COINCIDENT", StringComparison.OrdinalIgnoreCase)) return "Coincident";
        if (mateTypeName.Contains("DISTANCE", StringComparison.OrdinalIgnoreCase)) return "Distance";
        if (mateTypeName.Contains("GEAR", StringComparison.OrdinalIgnoreCase)) return "Gear";
        if (mateTypeName.Contains("PARALLEL", StringComparison.OrdinalIgnoreCase)) return "Parallel";
        if (mateTypeName.Contains("PERPENDICULAR", StringComparison.OrdinalIgnoreCase)) return "Perpendicular";
        if (mateTypeName.Contains("TANGENT", StringComparison.OrdinalIgnoreCase)) return "Tangent";
        return "";
    }

    static string AlignmentForSkill(string alignmentName)
    {
        if (alignmentName.Contains("ANTI", StringComparison.OrdinalIgnoreCase)) return "AntiAligned";
        if (alignmentName.Contains("CLOSEST", StringComparison.OrdinalIgnoreCase)) return "Closest";
        return "Aligned";
    }

    static string RebuiltPartPath(string partName)
    {
        return Path.Combine(GetSwagentOutputRoot(), MakeSafeFileName(partName) + ".SLDPRT");
    }

    static string ResolveInsertPartPath(PartKnowledge part, string workflowId)
    {
        if (part.IsStandardPart)
            return StandardPartInsertPath(part);

        if (!string.IsNullOrWhiteSpace(part.RebuiltOutputPath) && File.Exists(part.RebuiltOutputPath))
            return part.RebuiltOutputPath;

        string existing = LatestRebuiltPartOutput(part.PartName);
        if (!string.IsNullOrWhiteSpace(existing))
            return existing;

        return SyntheticRebuiltPartPath(part.PartName, workflowId);
    }

    static string LatestRebuiltPartOutput(string partName)
    {
        try
        {
            string outputRoot = GetSwagentOutputRoot();
            if (!Directory.Exists(outputRoot))
                return "";

            string fileBase = MakeSafeFileName(partName);
            return Directory.GetFiles(outputRoot, $"{fileBase}_*.SLDPRT")
                .Concat(Directory.GetFiles(outputRoot, $"{fileBase}.SLDPRT"))
                .OrderByDescending(path => File.GetLastWriteTime(path))
                .FirstOrDefault(path => File.Exists(path)) ?? "";
        }
        catch
        {
            return "";
        }
    }

    static string SyntheticPartFileBase(string partName, string workflowId)
    {
        return MakeSafeFileName(partName);
    }

    static string SyntheticRebuiltPartPath(string partName, string workflowId)
    {
        return Path.Combine(GetSwagentOutputRoot(), SyntheticPartFileBase(partName, workflowId) + ".SLDPRT");
    }

    static string GetSwagentOutputRoot()
    {
        string dir = System.Environment.CurrentDirectory;
        for (int i = 0; i < 8 && !string.IsNullOrWhiteSpace(dir); i++)
        {
            if (File.Exists(Path.Combine(dir, "project_paths.json")))
                return Path.Combine(dir, "SWagent", "runtime", "outputs");

            dir = Directory.GetParent(dir)?.FullName ?? "";
        }

        return Path.Combine(System.Environment.CurrentDirectory, "SWagent", "runtime", "outputs");
    }

    static List<object> ExtractMates(ModelDoc2 doc)
    {
        var mates = new List<object>();
        var mateSnapshots = CollectMateFeatureSnapshots(doc);
        int estimatedTotal = mateSnapshots.Count;
        var estimatedMateNames = mateSnapshots.Select(item => item.MateName).ToList();
        string totalText = estimatedTotal > 0 ? estimatedTotal.ToString() : "?";
        Log($"[mates] estimated_total={totalText}");

        int extractedMates = 0;
        int skippedDuplicateMates = 0;
        var seenMateKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        var stopwatch = Stopwatch.StartNew();
        foreach (var snapshot in mateSnapshots)
        {
            var feat = snapshot.Feature;
            var mate = snapshot.Mate;
            string mateName = snapshot.MateName;
            int mateType = SafeInt(() => mate.Type);
            int alignment = SafeInt(() => mate.Alignment);
            double? distanceMm = ExtractMateDistanceMm(feat, mateType);
            bool? distanceFlipDimension = ExtractMateDistanceFlipDimension(feat, mateType);
            var gearMate = ExtractGearMateInfo(feat, mateType);
            var entities = new List<Dictionary<string, object>>();

            string mateKey = FeatureIdentityKey(feat);
            if (!seenMateKeys.Add(mateKey))
            {
                skippedDuplicateMates++;
                if (skippedDuplicateMates % 10 == 1)
                    Log($"[mates] skip_duplicate count={skippedDuplicateMates}, current={mateName}, scanned_features={snapshot.ScannedFeatureIndex}");
                continue;
            }

            int count = SafeInt(() => mate.GetMateEntityCount());
            for (int i = 0; i < count; i++)
            {
                object entityObj = SafeObj(() => mate.MateEntity(i));
                if (entityObj is not MateEntity2 mateEntity) continue;
                var entity = ReadMateEntity(mateEntity);
                entity["mate_entity_index"] = i;
                entities.Add(entity);
            }

            AnnotateMateEntityRoles(entities, mateType);

            extractedMates++;
            Log($"[mates] {ProgressBar(extractedMates, estimatedTotal)} extracted={extractedMates}/{totalText}, current={mateName}, type={MateTypeName(mateType)}, entities={entities.Count}, scanned_features={snapshot.ScannedFeatureIndex}, elapsed={stopwatch.Elapsed:mm\\:ss}");

            mates.Add(new
            {
                mateName,
                mateType,
                mateTypeName = MateTypeName(mateType),
                distanceMm,
                distanceFlipDimension,
                gearMate,
                alignment,
                alignmentName = AlignmentName(alignment),
                entities
            });
        }
        var extractedNames = new HashSet<string>(
            mates.Select(item => Safe(() => Convert.ToString(item.GetType().GetProperty("mateName")?.GetValue(item)) ?? "")),
            StringComparer.OrdinalIgnoreCase);
        var missingNames = estimatedMateNames.Where(name => !extractedNames.Contains(name)).ToList();
        if (missingNames.Count > 0)
            Log("[mates] missing_estimated_names=" + string.Join(", ", missingNames));
        int scannedFeatures = mateSnapshots.Count == 0 ? 0 : mateSnapshots.Max(item => item.ScannedFeatureIndex);
        Log($"[mates] completed extracted={mates.Count}/{totalText}, skipped_duplicates={skippedDuplicateMates}, scanned_features={scannedFeatures}, elapsed={stopwatch.Elapsed:mm\\:ss}");
        return mates;
    }

    static List<MateFeatureSnapshot> CollectMateFeatureSnapshots(ModelDoc2 doc)
    {
        var snapshots = new List<MateFeatureSnapshot>();
        int scannedFeatures = 0;
        foreach (var feat in WalkFeatures(doc.FirstFeature() as Feature))
        {
            scannedFeatures++;
            if (scannedFeatures % 100 == 0)
                Log($"[mates] scanning_features={scannedFeatures}, collected={snapshots.Count}");

            object specific = SafeObj(() => feat.GetSpecificFeature2());
            if (specific is not Mate2 mate)
                continue;

            snapshots.Add(new MateFeatureSnapshot
            {
                Feature = feat,
                Mate = mate,
                MateName = Safe(() => feat.Name),
                ScannedFeatureIndex = scannedFeatures
            });
        }

        Log($"[mates] collected_snapshot_count={snapshots.Count}, scanned_features={scannedFeatures}");
        return snapshots;
    }

    static double? ExtractMateDistanceMm(Feature feat, int mateType)
    {
        if (mateType != (int)swMateType_e.swMateDISTANCE)
            return null;

        var values = new List<double>();
        try
        {
            for (var displayDimension = SafeObj(() => feat.GetFirstDisplayDimension()) as DisplayDimension;
                 displayDimension != null;
                 displayDimension = SafeObj(() => feat.GetNextDisplayDimension(displayDimension)) as DisplayDimension)
            {
                var dimension = SafeObj(() => displayDimension.GetDimension()) as Dimension;
                if (dimension == null)
                    continue;

                if (!TryReadComDouble(dimension, "SystemValue", out double valueM))
                    continue;

                if (double.IsNaN(valueM) || double.IsInfinity(valueM))
                    continue;

                values.Add(Math.Abs(valueM) * 1000.0);
            }
        }
        catch { }

        if (values.Count == 0)
            return null;

        return R(values[0]);
    }

    static bool? ExtractMateDistanceFlipDimension(Feature feat, int mateType)
    {
        if (mateType != (int)swMateType_e.swMateDISTANCE)
            return null;

        try
        {
            object definitionObj = SafeObj(() => feat.GetDefinition());
            if (definitionObj == null)
                return null;

            foreach (string name in new[] { "FlipDimension", "Flip", "ReverseDimension", "ReverseDirection" })
            {
                if (TryReadComBool(definitionObj, name, out bool value))
                    return value;
            }
        }
        catch { }

        return null;
    }

    static Dictionary<string, object> ExtractGearMateInfo(Feature feat, int mateType)
    {
        var result = new Dictionary<string, object>();
        if (mateType != (int)swMateType_e.swMateGEAR)
            return result;

        try
        {
            object definitionObj = SafeObj(() => feat.GetDefinition());
            if (definitionObj is IGearMateFeatureData gearData)
            {
                double numerator = SafeDouble(() => gearData.GearRatioNumerator, double.NaN);
                double denominator = SafeDouble(() => gearData.GearRatioDenominator, double.NaN);
                if (!double.IsNaN(numerator) && !double.IsNaN(denominator) &&
                    !double.IsInfinity(numerator) && !double.IsInfinity(denominator) &&
                    Math.Abs(numerator) > 1e-9 && Math.Abs(denominator) > 1e-9)
                {
                    result["ratio_numerator"] = R(numerator);
                    result["ratio_denominator"] = R(denominator);
                    result["reverse"] = SafeBool(() => gearData.Reverse);
                    result["source"] = "IGearMateFeatureData";
                }
            }
        }
        catch { }

        return result;
    }

    static void NormalizeMateComponentIds(List<object> mates, IReadOnlyDictionary<string, string> componentIdMap)
    {
        if (mates.Count == 0 || componentIdMap.Count == 0)
            return;

        int updated = 0;
        int missing = 0;
        foreach (dynamic mate in mates)
        {
            try
            {
                foreach (var entityObj in mate.entities)
                {
                    if (entityObj is not Dictionary<string, object> entity)
                        continue;

                    string original = ReadDictString(entity, "component");
                    if (string.IsNullOrWhiteSpace(original))
                        continue;

                    if (!componentIdMap.TryGetValue(original, out string normalized) || string.IsNullOrWhiteSpace(normalized))
                    {
                        missing++;
                        continue;
                    }

                    if (!entity.ContainsKey("original_component"))
                        entity["original_component"] = original;
                    entity["component"] = normalized;
                    updated++;
                }
            }
            catch { }
        }

        Log($"[assembly] normalized_mate_entities={updated}, unmatched_mate_entities={missing}");
    }

    static int EstimateMateCount(ModelDoc2 doc)
    {
        try
        {
            var keys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            foreach (var feat in WalkFeatures(doc.FirstFeature() as Feature))
            {
                object specific = SafeObj(() => feat.GetSpecificFeature2());
                if (specific is not Mate2 mate)
                    continue;

                string mateName = Safe(() => feat.Name);
                keys.Add(mateName + "|" + MateIdentityKey(mate));
            }

            return keys.Count;
        }
        catch (Exception ex)
        {
            Log("[mates] estimate_total_failed=" + ex.Message);
            return 0;
        }
    }

    static List<string> EstimateMateNames(ModelDoc2 doc)
    {
        var names = new List<string>();
        try
        {
            var keys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            foreach (var feat in WalkFeatures(doc.FirstFeature() as Feature))
            {
                object specific = SafeObj(() => feat.GetSpecificFeature2());
                if (specific is not Mate2 mate)
                    continue;

                string mateName = Safe(() => feat.Name);
                string key = mateName + "|" + MateIdentityKey(mate);
                if (keys.Add(key))
                    names.Add(mateName);
            }
        }
        catch (Exception ex)
        {
            Log("[mates] estimate_names_failed=" + ex.Message);
        }

        return names;
    }

    static string MateIdentityKey(Mate2 mate)
    {
        var parts = new List<string>
        {
            SafeInt(() => mate.Type).ToString(),
            SafeInt(() => mate.Alignment).ToString()
        };

        int count = SafeInt(() => mate.GetMateEntityCount());
        for (int i = 0; i < count; i++)
        {
            object entityObj = SafeObj(() => mate.MateEntity(i));
            if (entityObj is not MateEntity2 entity)
                continue;

            var comp = SafeObj(() => entity.ReferenceComponent) as Component2;
            parts.Add($"{Safe(() => comp?.Name2 ?? "")}:{Safe(() => comp?.GetPathName() ?? "")}:{SafeInt(() => entity.ReferenceType)}:{SafeInt(() => entity.ReferenceType2)}");
        }

        return string.Join("|", parts.OrderBy(x => x, StringComparer.OrdinalIgnoreCase));
    }

    static object[] ToObjectArray(object value)
    {
        if (value == null)
            return Array.Empty<object>();
        if (value is object[] objects)
            return objects;
        if (value is Array array)
            return array.Cast<object>().ToArray();
        return new[] { value };
    }

    static string ProgressBar(int current, int total)
    {
        if (total <= 0)
            return "[????????????????????]";

        const int width = 20;
        double percent = Math.Min(100.0, Math.Max(0.0, current * 100.0 / total));
        int filled = Math.Max(0, Math.Min(width, (int)Math.Round(percent * width / 100.0)));
        string suffix = current > total ? "+" : "";
        return "[" + new string('#', filled) + new string('-', width - filled) + $"] {percent:0.0}%{suffix}";
    }

    static IEnumerable<Feature> WalkFeatures(Feature feature)
    {
        var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (var item in WalkFeatures(feature, visited))
            yield return item;
    }

    static IEnumerable<Feature> WalkFeatures(Feature feature, HashSet<string> visited)
    {
        for (Feature f = feature; f != null; f = SafeObj(() => f.GetNextFeature()) as Feature)
        {
            string key = FeatureIdentityKey(f);
            if (!visited.Add(key))
                continue;

            yield return f;
            for (Feature sub = SafeObj(() => f.GetFirstSubFeature()) as Feature; sub != null; sub = SafeObj(() => sub.GetNextSubFeature()) as Feature)
                foreach (var nested in WalkFeatures(sub, visited))
                    yield return nested;
        }
    }

    static string FeatureIdentityKey(Feature feature)
    {
        try
        {
            IntPtr ptr = Marshal.GetIUnknownForObject(feature);
            try
            {
                if (ptr != IntPtr.Zero)
                    return ptr.ToString();
            }
            finally
            {
                if (ptr != IntPtr.Zero)
                    Marshal.Release(ptr);
            }
        }
        catch { }

        return $"{Safe(() => feature.GetTypeName2())}|{Safe(() => feature.Name)}";
    }

    static Dictionary<string, object> ReadMateEntity(MateEntity2 mateEntity)
    {
        var comp = SafeObj(() => mateEntity.ReferenceComponent) as Component2;
        object reference = SafeObj(() => mateEntity.Reference);

        var result = new Dictionary<string, object>
        {
            ["component"] = comp == null ? "" : Safe(() => comp.Name2),
            ["component_path"] = comp == null ? "" : Safe(() => comp.GetPathName()),
            ["part_name"] = comp == null ? "" : Path.GetFileNameWithoutExtension(Safe(() => comp.GetPathName())),
            ["reference_type"] = SafeInt(() => mateEntity.ReferenceType),
            ["reference_type2"] = SafeInt(() => mateEntity.ReferenceType2),
            ["entity_kind"] = EntityKind(reference)
        };

        if (reference is Face2 face)
            result["face_signature"] = FaceSignature(face);
        else if (reference is Edge edge)
            result["edge_signature"] = EdgeSignature(edge);
        else
        {
            result["reference_runtime_type"] = reference?.GetType().FullName ?? "";
            result["reference_resolution"] = "unresolved_without_face_or_edge";
        }

        return result;
    }

    static List<Dictionary<string, object>> GetComponentCylinderSignatures(Component2 comp)
    {
        string key = $"{Safe(() => comp.Name2)}|{Safe(() => comp.GetPathName())}";
        if (ComponentCylinderSignatureCache.TryGetValue(key, out var cached))
            return cached;

        var result = new List<Dictionary<string, object>>();
        try
        {
            if (comp.GetModelDoc2() is PartDoc partDoc)
            {
                foreach (var face in EnumeratePartFaces(partDoc))
                {
                    var sig = FaceSignature(face);
                    if (string.Equals(ReadDictString(sig, "surface_type"), "cylinder", StringComparison.OrdinalIgnoreCase))
                        result.Add(sig);
                }
            }
        }
        catch { }

        ComponentCylinderSignatureCache[key] = result;
        return result;
    }

    static IEnumerable<Face2> EnumeratePartFaces(PartDoc partDoc)
    {
        object bodiesObj = partDoc.GetBodies2((int)swBodyType_e.swSolidBody, true);
        foreach (object bodyObj in ToObjectArray(bodiesObj))
        {
            if (bodyObj is not Body2 body)
                continue;

            for (object faceObj = SafeObj(() => body.GetFirstFace()); faceObj != null;)
            {
                if (faceObj is not Face2 face)
                    yield break;

                yield return face;
                faceObj = SafeObj(() => face.GetNextFace());
            }
        }
    }

    static double[] TransformAssemblyPointToPartModel(double[] point, double[] transform)
    {
        if (point.Length < 3 || transform.Length < 12)
            return point;

        double scale = transform.Length > 12 && Math.Abs(transform[12]) > 1e-12 ? transform[12] : 1.0;
        double x = (point[0] - transform[9]) / scale;
        double y = (point[1] - transform[10]) / scale;
        double z = (point[2] - transform[11]) / scale;
        return new[]
        {
            transform[0] * x + transform[3] * y + transform[6] * z,
            transform[1] * x + transform[4] * y + transform[7] * z,
            transform[2] * x + transform[5] * y + transform[8] * z
        };
    }

    static double[] TransformAssemblyVectorToPartModel(double[] vector, double[] transform)
    {
        if (vector.Length < 3 || transform.Length < 9)
            return Normalize(vector.ElementAtOrDefault(0), vector.ElementAtOrDefault(1), vector.ElementAtOrDefault(2));

        double x = vector[0], y = vector[1], z = vector[2];
        return Normalize(
            transform[0] * x + transform[3] * y + transform[6] * z,
            transform[1] * x + transform[4] * y + transform[7] * z,
            transform[2] * x + transform[5] * y + transform[8] * z);
    }

    static Dictionary<string, object> FaceSignature(Face2 face)
    {
        var result = new Dictionary<string, object>
        {
            ["geometry_coordinate_system"] = "part_model"
        };
        try
        {
            result["area_mm2"] = R(SafeDouble(() => face.GetArea()) * 1_000_000.0);
            var box = ToDoubleArray(face.GetBox());
            if (box.Length >= 6)
            {
                var boxMm = box.Take(6).Select(v => R(v * 1000.0)).ToArray();
                result["box_mm"] = boxMm;
                result["center_mm"] = new[]
                {
                    R((boxMm[0] + boxMm[3]) / 2.0),
                    R((boxMm[1] + boxMm[4]) / 2.0),
                    R((boxMm[2] + boxMm[5]) / 2.0)
                };
            }

            if (face.GetFeature() is Feature feat)
                result["source_feature"] = Safe(() => feat.Name);

            if (face.GetSurface() is Surface surface)
            {
                string surfaceType = SurfaceKind(surface);
                result["surface_type"] = surfaceType;
                if (surfaceType == "cylinder")
                {
                    var cp = ToDoubleArray(GetComProperty(surface, "CylinderParams"));
                    if (cp.Length >= 7)
                    {
                        result["origin_mm"] = new[] { R(cp[0] * 1000.0), R(cp[1] * 1000.0), R(cp[2] * 1000.0) };
                        result["axis_point_mm"] = new[] { R(cp[0] * 1000.0), R(cp[1] * 1000.0), R(cp[2] * 1000.0) };
                        result["axis"] = new[] { R(cp[3]), R(cp[4]), R(cp[5]) };
                        result["radius_mm"] = R(cp[6] * 1000.0);
                        AddCylinderAxialSignature(result);
                    }
                }
                else if (surfaceType == "plane")
                {
                    var pp = ToDoubleArray(GetComProperty(surface, "PlaneParams"));
                    if (pp.Length >= 6)
                        result["normal"] = Normalize(pp[0], pp[1], pp[2]);
                }
            }
        }
        catch { }
        return result;
    }

    static Dictionary<string, object> EdgeSignature(Edge edge)
    {
        var result = new Dictionary<string, object>
        {
            ["geometry_coordinate_system"] = "part_model"
        };
        try
        {
            var start = VertexPointMm(SafeObj(() => edge.GetStartVertex()) as Vertex);
            var end = VertexPointMm(SafeObj(() => edge.GetEndVertex()) as Vertex);
            if (start.Length >= 3)
                result["start_mm"] = start.Select(R).ToArray();
            if (end.Length >= 3)
                result["end_mm"] = end.Select(R).ToArray();
            if (start.Length >= 3 && end.Length >= 3)
            {
                result["center_mm"] = new[]
                {
                    R((start[0] + end[0]) / 2.0),
                    R((start[1] + end[1]) / 2.0),
                    R((start[2] + end[2]) / 2.0)
                };
                result["length_mm"] = R(Distance(start, end));
            }

            Curve curve = SafeObj(() => edge.GetCurve()) as Curve;
            if (curve == null) return result;
            if (SafeBool(() => curve.IsCircle()))
            {
                result["curve_type"] = "circle";
                var cp = ToDoubleArray(GetComProperty(curve, "CircleParams"));
                if (cp.Length >= 7)
                {
                    result["center_mm"] = new[] { R(cp[0] * 1000.0), R(cp[1] * 1000.0), R(cp[2] * 1000.0) };
                    result["axis"] = new[] { R(cp[3]), R(cp[4]), R(cp[5]) };
                    result["radius_mm"] = R(cp[6] * 1000.0);
                }
            }
            else if (SafeBool(() => curve.IsLine()))
            {
                result["curve_type"] = "line";
                var lp = ToDoubleArray(GetComProperty(curve, "LineParams"));
                if (lp.Length >= 6)
                {
                    result["line_point_mm"] = new[] { R(lp[0] * 1000.0), R(lp[1] * 1000.0), R(lp[2] * 1000.0) };
                    result["line_direction"] = Normalize(lp[3], lp[4], lp[5]);
                }
            }
            else
            {
                result["curve_type"] = "other";
            }
        }
        catch { }
        return result;
    }

    static void AddCylinderAxialSignature(Dictionary<string, object> signature)
    {
        var axis = NormalizeRaw(ReadVector(signature, "axis"));
        var axisPoint = ReadVector(signature, "axis_point_mm");
        if (axisPoint.Length < 3)
            axisPoint = ReadVector(signature, "origin_mm");
        if (axis.Length < 3 || axisPoint.Length < 3)
            return;

        signature["axis"] = axis.Select(R).ToArray();
        signature["axis_line_point_mm"] = axisPoint.Select(R).ToArray();

        var center = ReadVector(signature, "center_mm");
        if (center.Length >= 3)
            signature["axial_center_mm"] = R(Dot(center, axis));

        var box = ReadVector(signature, "box_mm");
        if (box.Length < 6)
            box = ReadVector(signature, "bbox_mm");
        if (box.Length < 6)
            return;

        double min = double.PositiveInfinity;
        double max = double.NegativeInfinity;
        for (int xi = 0; xi < 2; xi++)
        for (int yi = 0; yi < 2; yi++)
        for (int zi = 0; zi < 2; zi++)
        {
            var p = new[]
            {
                box[xi == 0 ? 0 : 3],
                box[yi == 0 ? 1 : 4],
                box[zi == 0 ? 2 : 5]
            };
            double t = Dot(p, axis);
            min = Math.Min(min, t);
            max = Math.Max(max, t);
        }

        if (!double.IsInfinity(min) && !double.IsInfinity(max))
        {
            signature["axial_min_mm"] = R(min);
            signature["axial_max_mm"] = R(max);
            if (!signature.ContainsKey("axial_center_mm"))
                signature["axial_center_mm"] = R((min + max) / 2.0);
        }
    }

    static double[] VertexPointMm(Vertex vertex)
    {
        if (vertex == null)
            return Array.Empty<double>();

        try
        {
            var values = ToDoubleArray(vertex.GetPoint());
            if (values.Length >= 3)
                return new[] { values[0] * 1000.0, values[1] * 1000.0, values[2] * 1000.0 };
        }
        catch { }

        return Array.Empty<double>();
    }

    static double Distance(double[] a, double[] b)
    {
        if (a.Length < 3 || b.Length < 3)
            return 0.0;

        double dx = a[0] - b[0];
        double dy = a[1] - b[1];
        double dz = a[2] - b[2];
        return Math.Sqrt(dx * dx + dy * dy + dz * dz);
    }

    static double DistancePointToLine(double[] point, double[] linePoint, double[] lineDirection)
    {
        var dir = NormalizeRaw(lineDirection);
        if (point.Length < 3 || linePoint.Length < 3 || dir.Length < 3)
            return double.PositiveInfinity;

        var delta = new[] { point[0] - linePoint[0], point[1] - linePoint[1], point[2] - linePoint[2] };
        double projection = Dot(delta, dir);
        var closest = new[]
        {
            linePoint[0] + dir[0] * projection,
            linePoint[1] + dir[1] * projection,
            linePoint[2] + dir[2] * projection
        };
        return Distance(point, closest);
    }

    static double Dot(double[] a, double[] b)
    {
        if (a.Length < 3 || b.Length < 3)
            return 0.0;
        return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
    }

    static double[] NormalizeRaw(double[] values)
    {
        if (values.Length < 3)
            return Array.Empty<double>();

        double len = Math.Sqrt(values[0] * values[0] + values[1] * values[1] + values[2] * values[2]);
        return len <= 1e-12 ? Array.Empty<double>() : new[] { values[0] / len, values[1] / len, values[2] / len };
    }

    static double[] ReadVector(Dictionary<string, object> source, string key)
    {
        return source.TryGetValue(key, out var value) ? ToDoubleArray(value) : Array.Empty<double>();
    }

    static double? ReadNullableDouble(Dictionary<string, object> source, string key)
    {
        try
        {
            if (!source.TryGetValue(key, out var value) || value == null)
                return null;
            double result = Convert.ToDouble(value);
            return double.IsNaN(result) || double.IsInfinity(result) ? null : result;
        }
        catch
        {
            return null;
        }
    }

    static void AnnotateMateEntityRoles(List<Dictionary<string, object>> entities, int mateType)
    {
        foreach (var entity in entities)
        {
            string thisText = ((entity.TryGetValue("component", out var c) ? c : "") + " " +
                               (entity.TryGetValue("part_name", out var p) ? p : "")).ToString() ?? "";
            string otherText = string.Join(" ", entities.Where(e => !ReferenceEquals(e, entity)).Select(e =>
                $"{(e.TryGetValue("component", out var oc) ? oc : "")} {(e.TryGetValue("part_name", out var op) ? op : "")}"));

            var roles = new List<string>();
            string surfaceType = "";
            if (entity.TryGetValue("face_signature", out var sigObj) && sigObj is Dictionary<string, object> sig)
                surfaceType = sig.TryGetValue("surface_type", out var st) ? st?.ToString() ?? "" : "";
            if (mateType == (int)swMateType_e.swMateCONCENTRIC)
            {
                roles.Add("cylindrical_mate_face");
                if (ContainsAny(otherText, "\u6321\u6cb9\u73af", "oil", "ring")) roles.Add("oil_ring_mount_face");
                if (ContainsAny(otherText, "\u8f74\u627f", "bearing")) roles.Add("bearing_mount_face");
            }

            if (mateType == (int)swMateType_e.swMateCOINCIDENT)
            {
                roles.Add(surfaceType == "plane" ? "planar_contact_face" : "coincident_mate_entity");
                if (ContainsAny(otherText, "\u952e", "key")) roles.Add("key_contact_or_keyway_face");
                if (ContainsAny(otherText, "\u6321\u6cb9\u73af", "oil", "ring")) roles.Add("oil_ring_axial_stop_face");
            }

            if (mateType == (int)swMateType_e.swMateDISTANCE)
                roles.Add("distance_mate_face");

            if (mateType == (int)swMateType_e.swMateGEAR)
                roles.Add("gear_mate_axis");

            if (ContainsAny(otherText, "\u952e", "key") && !roles.Contains("key_mate_entity"))
                roles.Add("key_mate_entity");

            entity["role_candidates"] = roles.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
            entity["role_evidence"] = new[]
            {
                $"mate_type={MateTypeName(mateType)}",
                $"entity_kind={entity["entity_kind"]}",
                $"surface_type={surfaceType}",
                $"other_component_text={otherText}"
            };
        }
    }

    static List<object> BuildMatePorts(List<object> mates)
    {
        var ports = new List<object>();
        foreach (dynamic mate in mates)
        {
            try
            {
                var entities = new List<Dictionary<string, object>>();
                foreach (var entity in mate.entities)
                    if (entity is Dictionary<string, object> dict)
                        entities.Add(dict);

                if (entities.Count != 2)
                    continue;

                var rolesA = ToStringList(entities[0].TryGetValue("role_candidates", out var rolesObjA) ? rolesObjA : null);
                var rolesB = ToStringList(entities[1].TryGetValue("role_candidates", out var rolesObjB) ? rolesObjB : null);
                string roleA = ChooseVerifiedFaceRole(entities[0], entities[1], SafeInt(() => Convert.ToInt32(mate.mateType)), rolesA);
                string roleB = ChooseVerifiedFaceRole(entities[1], entities[0], SafeInt(() => Convert.ToInt32(mate.mateType)), rolesB);
                if (string.IsNullOrWhiteSpace(roleA) || string.IsNullOrWhiteSpace(roleB))
                    continue;

                string sideA = InferSideTag(entities[0]);
                string sideB = InferSideTag(entities[1]);
                HarmonizeSideTags(roleA, roleB, ref sideA, ref sideB);
                var port = new Dictionary<string, object>
                {
                    ["sourceMate"] = mate.mateName,
                    ["mateType"] = MateTypeForSkill(Convert.ToString(mate.mateTypeName) ?? ""),
                    ["alignment"] = AlignmentForSkill(Convert.ToString(mate.alignmentName) ?? ""),
                    ["a"] = new
                    {
                        component = ReadDictString(entities[0], "component"),
                        originalComponent = ReadDictString(entities[0], "original_component"),
                        part = ReadDictString(entities[0], "part_name"),
                        faceRole = roleA,
                        sideTag = sideA,
                        portRole = string.IsNullOrWhiteSpace(sideA) ? roleA : $"{roleA}:{sideA}",
                        entityKind = ReadDictString(entities[0], "entity_kind")
                    },
                    ["b"] = new
                    {
                        component = ReadDictString(entities[1], "component"),
                        originalComponent = ReadDictString(entities[1], "original_component"),
                        part = ReadDictString(entities[1], "part_name"),
                        faceRole = roleB,
                        sideTag = sideB,
                        portRole = string.IsNullOrWhiteSpace(sideB) ? roleB : $"{roleB}:{sideB}",
                        entityKind = ReadDictString(entities[1], "entity_kind")
                    }
                };

                if (TryReadMateDistanceMm(mate, out double distanceMm))
                    port["distanceMm"] = distanceMm;

                ports.Add(port);
            }
            catch { }
        }
        return ports;
    }

    static object ReadJsonOrNull(string path)
    {
        try
        {
            if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) return null;
            using var doc = JsonDocument.Parse(File.ReadAllText(path));
            return doc.RootElement.Clone();
        }
        catch { return null; }
    }

    static string LatestFile(string dir, string pattern)
    {
        try
        {
            return Directory.EnumerateFiles(dir, pattern)
                .OrderByDescending(File.GetLastWriteTime)
                .FirstOrDefault() ?? "";
        }
        catch { return ""; }
    }

    static string PreferredOrLatestFile(string dir, string preferredFileName, string fallbackPattern)
    {
        try
        {
            string preferred = Path.Combine(dir, preferredFileName);
            if (File.Exists(preferred))
                return preferred;
        }
        catch { }

        return LatestFile(dir, fallbackPattern);
    }

    static bool IsForcePartExtractionEnabled()
    {
        string value = System.Environment.GetEnvironmentVariable("SWAGENT_FORCE_PART_EXTRACTION") ?? "";
        return string.Equals(value, "1", StringComparison.OrdinalIgnoreCase)
            || string.Equals(value, "true", StringComparison.OrdinalIgnoreCase)
            || string.Equals(value, "yes", StringComparison.OrdinalIgnoreCase);
    }

    static string SurfaceKind(Surface surface)
    {
        if (surface == null) return "";
        try
        {
            if (surface.IsPlane()) return "plane";
            if (surface.IsCylinder()) return "cylinder";
            if (surface.IsCone()) return "cone";
            if (surface.IsSphere()) return "sphere";
        }
        catch { }
        return "unknown";
    }

    static string EntityKind(object reference)
    {
        if (reference is Face2) return "face";
        if (reference is Edge) return "edge";
        if (reference is Vertex) return "vertex";
        if (reference is RefPlane) return "ref_plane";
        if (reference is Feature) return "feature";
        return reference?.GetType().Name ?? "";
    }

    static string MateTypeName(int mateType)
    {
        try { return ((swMateType_e)mateType).ToString(); } catch { return mateType.ToString(); }
    }

    static string AlignmentName(int alignment)
    {
        try { return ((swMateAlign_e)alignment).ToString(); } catch { return alignment.ToString(); }
    }

    static string ParentName(Component2 c)
    {
        try
        {
            if (c.GetParent() is Component2 parent) return Safe(() => parent.Name2);
        }
        catch { }
        return "";
    }

    static bool ContainsAny(string text, params string[] keys)
    {
        if (string.IsNullOrWhiteSpace(text)) return false;
        return keys.Any(k => text.Contains(k, StringComparison.OrdinalIgnoreCase));
    }

    static bool IsGbStandardPart(params string[] values)
    {
        return values.Any(value => !string.IsNullOrWhiteSpace(value) &&
            value.Contains("GB", StringComparison.OrdinalIgnoreCase));
    }

    static string StandardPartInsertPath(PartKnowledge part)
    {
        if (!string.IsNullOrWhiteSpace(part.DirectInsertPath) && File.Exists(part.DirectInsertPath))
            return part.DirectInsertPath;
        return part.FilePath;
    }

    static string ResolveStandardPartSourcePath(string partName, string originalPath, List<string> instances)
    {
        try
        {
            if (!Directory.Exists(StandardPartSourceRoot))
                return originalPath;

            var names = new List<string>();
            AddStandardPartSearchName(names, partName);
            AddStandardPartSearchName(names, Path.GetFileNameWithoutExtension(originalPath));
            foreach (string instance in instances ?? new List<string>())
                AddStandardPartSearchName(names, ComponentBaseName(instance));

            foreach (string name in names.Distinct(StringComparer.OrdinalIgnoreCase))
            {
                foreach (string ext in new[] { ".SLDPRT", ".sldprt", ".SLDASM", ".sldasm" })
                {
                    string candidate = Path.Combine(StandardPartSourceRoot, name + ext);
                    if (File.Exists(candidate))
                        return Path.GetFullPath(candidate);
                }
            }

            var files = Directory.EnumerateFiles(StandardPartSourceRoot, "*.*", SearchOption.TopDirectoryOnly)
                .Where(path => string.Equals(Path.GetExtension(path), ".SLDPRT", StringComparison.OrdinalIgnoreCase) ||
                               string.Equals(Path.GetExtension(path), ".SLDASM", StringComparison.OrdinalIgnoreCase))
                .ToList();

            foreach (string name in names.Distinct(StringComparer.OrdinalIgnoreCase))
            {
                string matched = files.FirstOrDefault(path =>
                    string.Equals(Path.GetFileNameWithoutExtension(path), name, StringComparison.OrdinalIgnoreCase)) ?? "";
                if (!string.IsNullOrWhiteSpace(matched))
                    return Path.GetFullPath(matched);
            }

            foreach (string name in names.Distinct(StringComparer.OrdinalIgnoreCase))
            {
                string matched = files.FirstOrDefault(path =>
                    Path.GetFileNameWithoutExtension(path).Contains(name, StringComparison.OrdinalIgnoreCase) ||
                    name.Contains(Path.GetFileNameWithoutExtension(path), StringComparison.OrdinalIgnoreCase)) ?? "";
                if (!string.IsNullOrWhiteSpace(matched))
                    return Path.GetFullPath(matched);
            }
        }
        catch { }

        return originalPath;
    }

    static void AddStandardPartSearchName(List<string> names, string value)
    {
        string name = ComponentBaseName(value);
        if (!string.IsNullOrWhiteSpace(name))
            names.Add(name);
    }

    static string ComponentBaseName(string value)
    {
        if (string.IsNullOrWhiteSpace(value))
            return "";

        string name = Path.GetFileNameWithoutExtension(value.Trim());
        int slash = name.IndexOf('/');
        if (slash >= 0)
            name = name[..slash];
        int dash = name.LastIndexOf('-');
        if (dash > 0 && dash < name.Length - 1 && name[(dash + 1)..].All(char.IsDigit))
            name = name[..dash];
        return name.Trim();
    }

    static string MakeSafeFileName(string value)
    {
        string name = string.IsNullOrWhiteSpace(value) ? "assembly" : value.Trim();
        foreach (char c in Path.GetInvalidFileNameChars())
            name = name.Replace(c, '_');
        return name;
    }

    static string MakeAssemblyStepFileBase(string assemblyName)
    {
        string name = string.IsNullOrWhiteSpace(assemblyName) ? "assembly" : assemblyName.Trim();
        if (string.Equals(Path.GetExtension(name), ".SLDASM", StringComparison.OrdinalIgnoreCase))
            name = Path.GetFileNameWithoutExtension(name);
        return MakeSafeFileName(name);
    }

    static string Quote(string value) => "\"" + value.Replace("\"", "\\\"") + "\"";

    static string Tail(string value, int max)
    {
        if (string.IsNullOrEmpty(value) || value.Length <= max) return value ?? "";
        return value[^max..];
    }

    static string SafeTaskResult(Task<string> task)
    {
        try
        {
            return task.Wait(10_000) ? task.Result : "";
        }
        catch { return ""; }
    }

    static void InitDiagnosticLog(string asmPath)
    {
        try
        {
            string dir = Path.GetDirectoryName(asmPath) ?? System.Environment.CurrentDirectory;
            string fileBase = MakeSafeFileName(Path.GetFileNameWithoutExtension(asmPath));
            DiagnosticLogPath = Path.Combine(dir, $"{fileBase}_assembly_audit_trace_{DateTime.Now:yyyyMMdd_HHmmss}.log");
            File.WriteAllText(DiagnosticLogPath, "", Encoding.UTF8);
            Log("[main] diagnostic_log=" + DiagnosticLogPath);
            Log("[main] assembly_path=" + asmPath);
        }
        catch { }
    }

    static void Log(string message)
    {
        string line = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {message}";
        try { Console.WriteLine(message); } catch { }
        try
        {
            if (!string.IsNullOrWhiteSpace(DiagnosticLogPath))
                File.AppendAllText(DiagnosticLogPath, line + System.Environment.NewLine, Encoding.UTF8);
        }
        catch { }
    }

    static double R(double v) => Math.Round(v, 6);

    static double[] Normalize(double x, double y, double z)
    {
        double len = Math.Sqrt(x * x + y * y + z * z);
        return len <= 1e-12 ? Array.Empty<double>() : new[] { R(x / len), R(y / len), R(z / len) };
    }

    static object GetComProperty(object obj, string name)
    {
        try { return obj?.GetType().InvokeMember(name, System.Reflection.BindingFlags.GetProperty, null, obj, null); }
        catch { return null; }
    }

    static bool TryReadComDouble(object obj, string name, out double value)
    {
        value = 0.0;
        try
        {
            object raw = GetComProperty(obj, name);
            if (raw == null)
                return false;

            value = Convert.ToDouble(raw);
            return !double.IsNaN(value) && !double.IsInfinity(value);
        }
        catch
        {
            return false;
        }
    }

    static bool TryReadComBool(object obj, string name, out bool value)
    {
        value = false;
        try
        {
            object raw = GetComProperty(obj, name);
            if (raw == null)
                return false;

            if (raw is bool direct)
            {
                value = direct;
                return true;
            }

            if (raw is int i)
            {
                value = i != 0;
                return true;
            }

            if (raw is long l)
            {
                value = l != 0;
                return true;
            }

            if (raw is double d)
            {
                value = Math.Abs(d) > 1e-12;
                return !double.IsNaN(d) && !double.IsInfinity(d);
            }

            if (bool.TryParse(raw.ToString(), out bool parsedBool))
            {
                value = parsedBool;
                return true;
            }

            if (double.TryParse(raw.ToString(), out double parsedNumber))
            {
                value = Math.Abs(parsedNumber) > 1e-12;
                return !double.IsNaN(parsedNumber) && !double.IsInfinity(parsedNumber);
            }
        }
        catch
        {
        }

        return false;
    }

    static object InvokeIfExists(object obj, string name, params object[] args)
    {
        try
        {
            return obj?.GetType().InvokeMember(
                name,
                System.Reflection.BindingFlags.InvokeMethod,
                null,
                obj,
                args);
        }
        catch
        {
            return null;
        }
    }

    static double[] ToDoubleArray(object value)
    {
        try
        {
            if (value is double[] typed) return typed;
            if (value is Array array)
            {
                var list = new List<double>();
                foreach (object item in array)
                    if (item != null) list.Add(Convert.ToDouble(item));
                return list.ToArray();
            }
        }
        catch { }
        return Array.Empty<double>();
    }

    static string Safe(Func<string> f, string d = "") { try { return f() ?? d; } catch { return d; } }
    static object SafeObj(Func<object> f) { try { return f(); } catch { return null; } }
    static int SafeInt(Func<int> f, int d = 0) { try { return f(); } catch { return d; } }
    static double SafeDouble(Func<double> f, double d = 0) { try { return f(); } catch { return d; } }
    static bool SafeBool(Func<bool> f, bool d = false) { try { return f(); } catch { return d; } }
    static void SafeAction(Action action) { try { action(); } catch { } }
}

