using System.Globalization;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.RegularExpressions;
using Microsoft.Win32;

try
{
var options = CliOptions.Parse(args);
if (string.IsNullOrWhiteSpace(options.DrawingPath) || !File.Exists(options.DrawingPath))
    throw new FileNotFoundException($"DWG 文件不存在：{options.DrawingPath}");

Directory.CreateDirectory(options.OutputDir);
var result = string.IsNullOrWhiteSpace(options.ReuseExtractionDir)
    ? StaRunner.Run(() => AutoCadExtractor.Extract(options))
    : AutoCadExtractor.ReanalyzeExistingExtraction(options);
Console.WriteLine(JsonSerializer.Serialize(result, JsonDefaults.Options));
}
catch (Exception ex)
{
    Environment.ExitCode = 1;
    Console.Error.WriteLine(ex.ToString());
    Console.WriteLine(JsonSerializer.Serialize(new Dictionary<string, object?>
    {
        ["ok"] = false,
        ["error_type"] = ex.GetType().FullName,
        ["message"] = ex.Message
    }, JsonDefaults.Options));
}

sealed class AutoCadExtractor
{
    const double AngleToleranceDeg = 1.0;
    const double CollinearDistanceTolerance = 0.05;
    const double GapTolerance = 0.05;
    const double MainViewConnectTolerance = 0.5;
    const double ViewRegionProbeTolerance = 0.5;
    const double ViewRegionSpatialExpansionTolerance = 12.0;

    public static ExtractionResult Extract(CliOptions options)
    {
        LogProgress("connect_autocad_start");
        var app = ConnectOrCreate(options.ProgId, options.Visible, options.AllowStartAutoCad, out var started);
        LogProgress("connect_autocad_ok");
        object? doc = null;
        try
        {
            LogProgress("wait_autocad_start");
            WaitForQuiescent(app, TimeSpan.FromSeconds(30));
            LogProgress("open_document_start");
            doc = OpenDocument(app, options.DrawingPath);
            LogProgress("open_document_ok");
            WaitForQuiescent(app, TimeSpan.FromSeconds(30));

            LogProgress("read_spline_endpoints_start");
            var splineEndpoints = ReadSplineEndpointsViaAutoLisp(app, doc, options.OutputDir);
            LogProgress("read_spline_endpoints_ok");
            LogProgress("read_block_explosions_start");
            var blockExplosions = ReadBlockExplosionsViaAutoLisp(app, doc, options.OutputDir);
            LogProgress("read_block_explosions_ok");
            LogProgress("read_dimension_definition_points_start");
            var dimensionDefinitionPoints = ReadDimensionDefinitionPointsViaAutoLisp(app, doc, options.OutputDir);
            LogProgress("read_dimension_definition_points_ok");
            var extraction = new DocumentExtraction();
            LogProgress("extract_modelspace_start");
            ExtractSpace(doc, "ModelSpace", extraction, options.MaxEntities, splineEndpoints, blockExplosions, dimensionDefinitionPoints);
            LogProgress("extract_modelspace_ok");
            LogProgress("extract_paperspace_start");
            ExtractSpace(doc, "PaperSpace", extraction, options.MaxEntities, splineEndpoints, blockExplosions, dimensionDefinitionPoints);
            LogProgress("extract_paperspace_ok");
            LogProgress("select_main_view_start");
            SelectMainView(extraction, options.MainViewSeedMode, options.MainViewReferenceSelectionPath);
            LogProgress("select_main_view_ok");
            LogProgress("analyze_view_regions_start");
            AnalyzeViewRegions(extraction, options);
            LogProgress("analyze_view_regions_ok");
            LogProgress("normalize_geometry_start");
            NormalizeGeometry(extraction);
            LogProgress("normalize_geometry_ok");
            if (!string.IsNullOrWhiteSpace(options.SaveMainViewOnlyDwgPath))
            {
                var keptHandles = extraction.MainViewGeometry
                    .Select(item => ReadDictString(item, "handle"))
                    .Where(handle => !string.IsNullOrWhiteSpace(handle))
                    .ToHashSet(StringComparer.OrdinalIgnoreCase);
                var savePath = Path.GetFullPath(options.SaveMainViewOnlyDwgPath);
                var deleteResult = SaveMainViewOnlyDwg(doc, savePath, keptHandles);
                extraction.MainViewSelection["main_view_only_dwg_path"] = savePath;
                extraction.MainViewSelection["main_view_only_deleted_entity_count"] = deleteResult.DeletedCount;
                extraction.MainViewSelection["main_view_only_kept_handle_count"] = keptHandles.Count;
            }

            LogProgress("write_files_start");
            var manifest = WriteFiles(options.OutputDir, options.DrawingPath, options.ProgId, started, extraction);
            LogProgress("write_files_ok");
            if (options.CloseAfterExtract)
            {
                LogProgress("close_document_start");
                TryInvoke(doc, "Close", [false]);
                LogProgress("close_document_ok");
            }

            return new ExtractionResult
            {
                Ok = true,
                Source = "AutoCAD COM external extractor",
                ProgId = options.ProgId,
                Started = started,
                DrawingPath = options.DrawingPath,
                ExportDir = options.OutputDir,
                ManifestPath = Path.Combine(options.OutputDir, "manifest.json"),
                EntityCount = extraction.EntitiesCommon.Count,
                TypeCounts = extraction.TypeCounts,
                CategoryFiles = manifest.CategoryFiles,
                Message = $"已通过 AutoCAD COM 提取 DWG：{Path.GetFileName(options.DrawingPath)}"
            };
        }
        finally
        {
            if (doc != null)
                ReleaseCom(doc);
            ReleaseCom(app);
        }
    }

    public static ExtractionResult ReanalyzeExistingExtraction(CliOptions options)
    {
        LogProgress("reuse_extraction_load_start");
        var extraction = LoadExistingExtraction(options.ReuseExtractionDir);
        LogProgress("reuse_extraction_load_ok");

        LogProgress("select_main_view_start");
        SelectMainView(extraction, options.MainViewSeedMode, options.MainViewReferenceSelectionPath);
        LogProgress("select_main_view_ok");

        LogProgress("analyze_view_regions_start");
        AnalyzeViewRegions(extraction, options);
        LogProgress("analyze_view_regions_ok");

        LogProgress("normalize_geometry_start");
        NormalizeGeometry(extraction);
        LogProgress("normalize_geometry_ok");

        LogProgress("write_files_start");
        var manifest = WriteFiles(options.OutputDir, options.DrawingPath, options.ProgId, false, extraction);
        LogProgress("write_files_ok");

        return new ExtractionResult
        {
            Ok = true,
            Source = "Existing AutoCAD extraction reanalysis",
            ProgId = options.ProgId,
            Started = false,
            DrawingPath = options.DrawingPath,
            ExportDir = options.OutputDir,
            ManifestPath = Path.Combine(options.OutputDir, "manifest.json"),
            EntityCount = extraction.EntitiesCommon.Count,
            TypeCounts = extraction.TypeCounts,
            CategoryFiles = manifest.CategoryFiles,
            Message = $"Reanalyzed existing extraction: {Path.GetFileName(options.ReuseExtractionDir)}"
        };
    }

    static void LogProgress(string stage) =>
        Console.Error.WriteLine($"[{DateTime.Now:HH:mm:ss}] AutoCadDwgExtractor {stage}");

    static void ExtractSpace(
        object doc,
        string spaceName,
        DocumentExtraction extraction,
        int maxEntities,
        Dictionary<string, SplineEndpoint> splineEndpoints,
        Dictionary<string, List<Dictionary<string, object?>>> blockExplosions,
        Dictionary<string, DimensionDefinitionPoints> dimensionDefinitionPoints)
    {
        var space = GetProperty(doc, spaceName);
        if (space == null)
            return;

        try
        {
            var count = ToInt(GetProperty(space, "Count")) ?? 0;
            if (spaceName.Equals("ModelSpace", StringComparison.OrdinalIgnoreCase))
                extraction.ModelSpaceCount = count;
            else
                extraction.PaperSpaceCount = count;

            for (var i = 0; i < count && extraction.EntitiesCommon.Count < maxEntities; i++)
            {
                var entity = GetCollectionItem(space, i);
                if (entity == null)
                    continue;
                try
                {
                    ExtractEntity(entity, spaceName, i, extraction, splineEndpoints, blockExplosions, dimensionDefinitionPoints);
                }
                finally
                {
                    ReleaseCom(entity);
                }
            }
        }
        finally
        {
            ReleaseCom(space);
        }
    }

    static void ExtractEntity(
        object entity,
        string spaceName,
        int index,
        DocumentExtraction extraction,
        Dictionary<string, SplineEndpoint> splineEndpoints,
        Dictionary<string, List<Dictionary<string, object?>>> blockExplosions,
        Dictionary<string, DimensionDefinitionPoints> dimensionDefinitionPoints)
    {
        var objectName = ReadString(entity, "ObjectName");
        if (string.IsNullOrWhiteSpace(objectName))
            objectName = entity.GetType().Name;

        extraction.TypeCounts[objectName] = extraction.TypeCounts.TryGetValue(objectName, out var count) ? count + 1 : 1;

        var common = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
        {
            ["space"] = spaceName,
            ["index"] = index,
            ["object_name"] = objectName,
            ["handle"] = ReadString(entity, "Handle"),
            ["layer"] = ReadString(entity, "Layer"),
            ["linetype"] = ReadString(entity, "Linetype"),
            ["color"] = ToInt(GetProperty(entity, "Color")),
            ["lineweight"] = ToInt(GetProperty(entity, "Lineweight"))
        };
        extraction.EntitiesCommon.Add(common);

        var details = ExtractDetails(entity, objectName);
        var handle = ReadDictString(common, "handle");
        if (objectName.Contains("Spline", StringComparison.OrdinalIgnoreCase) &&
            !string.IsNullOrWhiteSpace(handle) &&
            splineEndpoints.TryGetValue(handle, out var splineEndpoint))
        {
            details["start"] = splineEndpoint.Start;
            details["end"] = splineEndpoint.End;
            details["spline_endpoint_source"] = splineEndpoint.Source;
        }
        if (objectName.Contains("BlockReference", StringComparison.OrdinalIgnoreCase) &&
            !string.IsNullOrWhiteSpace(handle) &&
            blockExplosions.TryGetValue(handle, out var exploded))
        {
            details["exploded_entities"] = exploded;
            details["exploded_entity_count"] = exploded.Count;
        }
        if (objectName.Contains("Dimension", StringComparison.OrdinalIgnoreCase) &&
            !string.IsNullOrWhiteSpace(handle) &&
            dimensionDefinitionPoints.TryGetValue(handle, out var definitionPoints))
        {
            ApplyDimensionDefinitionPoints(details, definitionPoints);
        }
        var merged = Merge(common, details);
        if (objectName.Contains("Dimension", StringComparison.OrdinalIgnoreCase))
        {
            extraction.Dimensions.Add(merged);
            var tol = ExtractDimensionalTolerance(common, details);
            if (tol != null)
                extraction.DimensionalTolerances.Add(tol);
        }
        else if (IsGeometry(objectName))
        {
            extraction.Geometry.Add(merged);
        }
        else if (IsAnnotation(objectName))
        {
            extraction.Annotations.Add(merged);
        }
        else
        {
            extraction.UnknownEntities.Add(merged);
        }

        var text = details.TryGetValue("text", out var t) ? Convert.ToString(t, CultureInfo.InvariantCulture) ?? "" : "";
        var blockName = details.TryGetValue("effective_name", out var b) ? Convert.ToString(b, CultureInfo.InvariantCulture) ?? "" : "";
        if (LooksLikeRoughness(text, blockName))
            extraction.Roughness.Add(merged);
        if (LooksLikeGeometricTolerance(text, blockName))
            extraction.GeometricTolerances.Add(merged);
        if (LooksLikeDatum(text, blockName))
            extraction.Datums.Add(merged);
    }

    static Dictionary<string, object?> ExtractDetails(object entity, string objectName)
    {
        var details = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
        var bbox = ReadEntityBoundingBox(entity);
        if (bbox != null)
            details["bbox"] = bbox;

        if (IsPolyline(objectName))
        {
            var coordinates = DoubleList(GetProperty(entity, "Coordinates"));
            details["coordinates"] = coordinates;
            details["closed"] = ToBool(GetProperty(entity, "Closed"));
            details["elevation"] = ToDouble(GetProperty(entity, "Elevation"));
            details["bulges"] = ReadPolylineBulges(entity, coordinates.Count);
        }
        else if (objectName.Contains("Circle", StringComparison.OrdinalIgnoreCase))
        {
            AddPoint(details, "center", GetProperty(entity, "Center"));
            var radius = ToDouble(GetProperty(entity, "Radius"));
            details["radius"] = radius;
            details["diameter"] = radius == null ? null : radius * 2;
        }
        else if (objectName.Contains("Arc", StringComparison.OrdinalIgnoreCase))
        {
            AddPoint(details, "center", GetProperty(entity, "Center"));
            details["radius"] = ToDouble(GetProperty(entity, "Radius"));
            details["start_angle"] = ToDouble(GetProperty(entity, "StartAngle"));
            details["end_angle"] = ToDouble(GetProperty(entity, "EndAngle"));
        }
        else if (objectName.Contains("Spline", StringComparison.OrdinalIgnoreCase))
        {
            AddPoint(details, "start", GetProperty(entity, "StartPoint"));
            AddPoint(details, "end", GetProperty(entity, "EndPoint"));
            var fitPoints = DoubleList(GetProperty(entity, "FitPoints"));
            var controlPoints = DoubleList(GetProperty(entity, "ControlPoints"));
            if (fitPoints.Count > 0)
                details["fit_points"] = fitPoints;
            if (controlPoints.Count > 0)
                details["control_points"] = controlPoints;
            EnsureSplineEndpoint(details, "start", fitPoints, controlPoints, takeFirst: true);
            EnsureSplineEndpoint(details, "end", fitPoints, controlPoints, takeFirst: false);
            details["degree"] = ToInt(GetProperty(entity, "Degree"));
            details["closed"] = ToBool(GetProperty(entity, "Closed"));
        }
        else if (objectName.Contains("Line", StringComparison.OrdinalIgnoreCase))
        {
            AddPoint(details, "start", GetProperty(entity, "StartPoint"));
            AddPoint(details, "end", GetProperty(entity, "EndPoint"));
        }
        else if (objectName.Contains("Text", StringComparison.OrdinalIgnoreCase))
        {
            var raw = ReadString(entity, "TextString");
            details["text"] = CleanAcadText(raw);
            details["raw_text"] = raw;
            details["height"] = ToDouble(GetProperty(entity, "Height"));
            details["rotation"] = ToDouble(GetProperty(entity, "Rotation"));
            AddPoint(details, "insertion_point", GetProperty(entity, "InsertionPoint"));
        }
        else if (objectName.Contains("Dimension", StringComparison.OrdinalIgnoreCase))
        {
            var raw = ReadString(entity, "TextOverride");
            details["measurement"] = ToDouble(GetProperty(entity, "Measurement"));
            details["text_override"] = raw;
            details["text"] = CleanAcadText(raw);
            var missingPointFields = new List<string>();
            AddPointFromProperties(details, "text_position", entity, missingPointFields, "TextPosition");
            AddPointFromProperties(details, "xline1_point", entity, missingPointFields, "XLine1Point", "ExtLine1Point", "ExtensionLine1Point");
            AddPointFromProperties(details, "xline2_point", entity, missingPointFields, "XLine2Point", "ExtLine2Point", "ExtensionLine2Point");
            AddPointFromProperties(details, "dim_line_point", entity, missingPointFields, "DimLinePoint", "DimLineLocation");
            AddPointFromProperties(details, "center", entity, missingPointFields, "Center");
            AddPointFromProperties(details, "chord_point", entity, missingPointFields, "ChordPoint");
            AddPointFromProperties(details, "far_chord_point", entity, missingPointFields, "FarChordPoint");
            if (missingPointFields.Count > 0)
                details["dimension_point_missing_fields"] = missingPointFields;
        }
        else if (objectName.Contains("BlockReference", StringComparison.OrdinalIgnoreCase))
        {
            details["name"] = ReadString(entity, "Name");
            details["effective_name"] = ReadString(entity, "EffectiveName");
            details["rotation"] = ToDouble(GetProperty(entity, "Rotation"));
            AddPoint(details, "insertion_point", GetProperty(entity, "InsertionPoint"));
            var attributes = ReadBlockAttributes(entity);
            if (attributes.Count > 0)
            {
                details["attributes"] = attributes;
                details["text"] = string.Join(" ", attributes.Select(a => a.TryGetValue("value", out var value) ? value : null));
            }
        }
        else if (objectName.Contains("Hatch", StringComparison.OrdinalIgnoreCase))
        {
            details["pattern_name"] = ReadString(entity, "PatternName");
            details["pattern_scale"] = ToDouble(GetProperty(entity, "PatternScale"));
        }

        return details;
    }

    static Dictionary<string, object?>? ReadEntityBoundingBox(object entity)
    {
        try
        {
            object? minPoint = null;
            object? maxPoint = null;
            object?[] args = [minPoint, maxPoint];
            var modifiers = new ParameterModifier(2);
            modifiers[0] = true;
            modifiers[1] = true;
            entity.GetType().InvokeMember(
                "GetBoundingBox",
                BindingFlags.InvokeMethod,
                null,
                entity,
                args,
                [modifiers],
                CultureInfo.InvariantCulture,
                null);

            var min = DoubleList(args[0]);
            var max = DoubleList(args[1]);
            if (min.Count < 2 || max.Count < 2)
                return null;

            return new Dictionary<string, object?>
            {
                ["min_x"] = Math.Min(min[0], max[0]),
                ["min_y"] = Math.Min(min[1], max[1]),
                ["max_x"] = Math.Max(min[0], max[0]),
                ["max_y"] = Math.Max(min[1], max[1])
            };
        }
        catch
        {
            return null;
        }
    }

    static List<Dictionary<string, object?>> ReadBlockAttributes(object entity)
    {
        var result = new List<Dictionary<string, object?>>();
        if (ToBool(GetProperty(entity, "HasAttributes")) != true)
            return result;

        var raw = TryInvoke(entity, "GetAttributes", []);
        if (raw is not Array array)
            return result;

        foreach (var item in array)
        {
            if (item == null)
                continue;
            result.Add(new Dictionary<string, object?>
            {
                ["tag"] = ReadString(item, "TagString"),
                ["value"] = CleanAcadText(ReadString(item, "TextString")),
                ["raw_value"] = ReadString(item, "TextString")
            });
            ReleaseCom(item);
        }
        return result;
    }

    static Dictionary<string, object?>? ExtractDimensionalTolerance(Dictionary<string, object?> common, Dictionary<string, object?> details)
    {
        var raw = details.TryGetValue("text_override", out var rawValue) ? Convert.ToString(rawValue, CultureInfo.InvariantCulture) ?? "" : "";
        var text = details.TryGetValue("text", out var textValue) ? Convert.ToString(textValue, CultureInfo.InvariantCulture) ?? "" : "";
        if (!DimensionalToleranceParser.LooksLikeTolerance(raw, text))
            return null;

        var measurement = details.TryGetValue("measurement", out var measurementValue)
            ? ToDouble(measurementValue)
            : null;
        var objectName = common.TryGetValue("object_name", out var objectNameValue)
            ? Convert.ToString(objectNameValue, CultureInfo.InvariantCulture) ?? ""
            : "";
        var parsed = DimensionalToleranceParser.Parse(text, raw, measurement, objectName);
        var result = Merge(Merge(common, details), parsed);
        result["measurement"] = measurementValue;
        result["numeric_values"] = ExtractNumbers(text);
        return result;
    }

    static void NormalizeGeometry(DocumentExtraction extraction)
    {
        var full = NormalizePrimitiveSet(extraction.Geometry, "full_drawing");
        extraction.NormalizedGeometry = full.Normalized;
        extraction.MergedLines = full.MergedLines;
        extraction.GeometryOverlaps = full.Overlaps;
        extraction.NormalizationSummary = full.Summary;

        var main = NormalizePrimitiveSet(extraction.MainViewGeometry, "main_view");
        extraction.MainViewNormalizedGeometry = main.Normalized;
        extraction.MainViewMergedLines = main.MergedLines;
        extraction.MainViewOverlaps = main.Overlaps;
        extraction.MainViewSignature = BuildViewSignature(
            extraction.MainViewNormalizedGeometry,
            extraction.MainViewMergedLines,
            "main_view");

        foreach (var item in main.Summary)
            extraction.MainViewSelection["normalization_" + item.Key] = item.Value;
    }

    static NormalizedGeometrySet NormalizePrimitiveSet(List<Dictionary<string, object?>> geometry, string scope)
    {
        var primitives = new List<Dictionary<string, object?>>();
        foreach (var entity in geometry)
            AddNormalizedPrimitives(entity, primitives);

        var lines = primitives
            .Where(item => string.Equals(ReadDictString(item, "kind"), "line", StringComparison.OrdinalIgnoreCase))
            .Select(LinePrimitive.FromDictionary)
            .Where(item => item != null && item.Length > 1e-9)
            .Cast<LinePrimitive>()
            .ToList();

        var mergedLines = MergeStrongLines(lines);
        var overlaps = FindStyleDifferentOverlaps(lines);
        var summary = new Dictionary<string, object?>
        {
            ["scope"] = scope,
            ["raw_geometry_count"] = geometry.Count,
            ["normalized_primitive_count"] = primitives.Count,
            ["normalized_line_count"] = lines.Count,
            ["merged_line_count"] = mergedLines.Count,
            ["overlap_group_count"] = overlaps.Count,
            ["angle_tolerance_deg"] = AngleToleranceDeg,
            ["collinear_distance_tolerance"] = CollinearDistanceTolerance,
            ["gap_tolerance"] = GapTolerance,
            ["merge_policy"] = "strong_only_same_style_collinear_interval_union"
        };

        return new NormalizedGeometrySet(primitives, mergedLines, overlaps, summary);
    }

    static void AddNormalizedPrimitives(Dictionary<string, object?> entity, List<Dictionary<string, object?>> output)
    {
        var objectName = ReadDictString(entity, "object_name");
        if (IsPolyline(objectName))
        {
            AddPolylinePrimitives(entity, output);
            return;
        }

        if (objectName.Contains("Circle", StringComparison.OrdinalIgnoreCase))
        {
            var center = ReadPoint(entity, "center");
            var radius = ReadDictDouble(entity, "radius");
            if (center.Count >= 2 && radius != null)
                output.Add(BasePrimitive(entity, "circle", new Dictionary<string, object?>
                {
                    ["center"] = center,
                    ["radius"] = radius,
                    ["diameter"] = radius * 2
                }));
            return;
        }

        if (objectName.Contains("Arc", StringComparison.OrdinalIgnoreCase))
        {
            var center = ReadPoint(entity, "center");
            var radius = ReadDictDouble(entity, "radius");
            var startAngle = ReadDictDouble(entity, "start_angle");
            var endAngle = ReadDictDouble(entity, "end_angle");
            if (center.Count >= 2 && radius != null)
            {
                var extra = new Dictionary<string, object?>
                {
                    ["center"] = center,
                    ["radius"] = radius,
                    ["start_angle"] = startAngle,
                    ["end_angle"] = endAngle
                };
                if (startAngle != null)
                    extra["start"] = PointOnCircle(center, radius.Value, startAngle.Value);
                if (endAngle != null)
                    extra["end"] = PointOnCircle(center, radius.Value, endAngle.Value);
                output.Add(BasePrimitive(entity, "arc", extra));
            }
            return;
        }

        if (objectName.Contains("Spline", StringComparison.OrdinalIgnoreCase))
        {
            output.Add(BasePrimitive(entity, "spline", new Dictionary<string, object?>
            {
                ["start"] = ReadPoint(entity, "start"),
                ["end"] = ReadPoint(entity, "end"),
                ["degree"] = ReadDictInt(entity, "degree"),
                ["closed"] = ReadDictBool(entity, "closed")
            }));
            return;
        }

        if (objectName.Contains("Line", StringComparison.OrdinalIgnoreCase))
        {
            AddLinePrimitive(entity, output, ReadPoint(entity, "start"), ReadPoint(entity, "end"), null, "line");
            return;
        }

        if (objectName.Contains("Hatch", StringComparison.OrdinalIgnoreCase))
        {
            output.Add(BasePrimitive(entity, "hatch", new Dictionary<string, object?>
            {
                ["pattern_name"] = ReadDictString(entity, "pattern_name"),
                ["pattern_scale"] = ReadDictDouble(entity, "pattern_scale")
            }));
        }
    }

    static void AddPolylinePrimitives(Dictionary<string, object?> entity, List<Dictionary<string, object?>> output)
    {
        var coordinates = ReadDoubleList(entity, "coordinates");
        if (coordinates.Count < 4)
            return;

        var stride = coordinates.Count % 3 == 0 ? 3 : 2;
        var points = new List<List<double>>();
        for (var i = 0; i + stride - 1 < coordinates.Count; i += stride)
            points.Add(new List<double> { coordinates[i], coordinates[i + 1] });

        if (points.Count < 2)
            return;

        var bulges = ReadDoubleList(entity, "bulges");
        var closed = ReadDictBool(entity, "closed") == true;
        var segmentCount = closed ? points.Count : points.Count - 1;
        for (var i = 0; i < segmentCount; i++)
        {
            var start = points[i];
            var end = points[(i + 1) % points.Count];
            var bulge = i < bulges.Count ? bulges[i] : 0.0;
            if (Math.Abs(bulge) <= 1e-12)
            {
                AddLinePrimitive(entity, output, start, end, i + 1, "polyline_segment");
            }
            else
            {
                output.Add(BasePrimitive(entity, "arc", new Dictionary<string, object?>
                {
                    ["start"] = start,
                    ["end"] = end,
                    ["bulge"] = bulge,
                    ["segment_index"] = i + 1,
                    ["source_kind"] = "polyline_bulge_segment"
                }));
            }
        }
    }

    static void AddLinePrimitive(
        Dictionary<string, object?> entity,
        List<Dictionary<string, object?>> output,
        List<double> start,
        List<double> end,
        int? segmentIndex,
        string sourceKind)
    {
        if (start.Count < 2 || end.Count < 2)
            return;

        var dx = end[0] - start[0];
        var dy = end[1] - start[1];
        var length = Math.Sqrt(dx * dx + dy * dy);
        if (length <= 1e-9)
            return;

        var angle = NormalizeAngleDeg(Math.Atan2(dy, dx) * 180.0 / Math.PI);
        var extra = new Dictionary<string, object?>
        {
            ["start"] = start,
            ["end"] = end,
            ["length"] = length,
            ["angle_deg"] = angle,
            ["source_kind"] = sourceKind
        };
        if (segmentIndex != null)
            extra["segment_index"] = segmentIndex;

        output.Add(BasePrimitive(entity, "line", extra));
    }

    static List<Dictionary<string, object?>> MergeStrongLines(List<LinePrimitive> lines)
    {
        var result = new List<Dictionary<string, object?>>();
        foreach (var styleGroup in lines.GroupBy(line => line.StyleKey, StringComparer.OrdinalIgnoreCase))
        {
            foreach (var angleGroup in styleGroup.GroupBy(line => AngleBucket(line.AngleDeg)))
            {
                foreach (var cluster in BuildCollinearClusters(angleGroup.ToList()))
                {
                    foreach (var merged in MergeClusterIntervals(cluster))
                        result.Add(merged);
                }
            }
        }

        return result;
    }

    static List<Dictionary<string, object?>> FindStyleDifferentOverlaps(List<LinePrimitive> lines)
    {
        var result = new List<Dictionary<string, object?>>();
        var groupIndex = 0;
        foreach (var angleGroup in lines.GroupBy(line => AngleBucket(line.AngleDeg)))
        {
            foreach (var cluster in BuildCollinearClusters(angleGroup.ToList()))
            {
                var intervals = cluster
                    .Select(line => new LineInterval(line, Math.Min(line.T0, line.T1), Math.Max(line.T0, line.T1)))
                    .OrderBy(item => item.Start)
                    .ToList();

                var active = new List<LineInterval>();
                foreach (var current in intervals)
                {
                    active.RemoveAll(item => item.End < current.Start + CollinearDistanceTolerance);
                    foreach (var prior in active)
                    {
                        if (string.Equals(prior.Line.StyleKey, current.Line.StyleKey, StringComparison.OrdinalIgnoreCase))
                            continue;

                        var overlapStart = Math.Max(prior.Start, current.Start);
                        var overlapEnd = Math.Min(prior.End, current.End);
                        var overlapLength = overlapEnd - overlapStart;
                        if (overlapLength <= GapTolerance)
                            continue;

                        groupIndex++;
                        result.Add(new Dictionary<string, object?>
                        {
                            ["overlap_group_id"] = $"overlap-{groupIndex:000000}",
                            ["handles"] = new[] { prior.Line.Handle, current.Line.Handle }.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToList(),
                            ["indices"] = new[] { prior.Line.Index, current.Line.Index },
                            ["overlap_length"] = overlapLength,
                            ["reason"] = "same_geometry_different_style",
                            ["style_a"] = prior.Line.Style,
                            ["style_b"] = current.Line.Style
                        });
                    }
                    active.Add(current);
                }
            }
        }

        return result;
    }

    static List<List<LinePrimitive>> BuildCollinearClusters(List<LinePrimitive> lines)
    {
        var clusters = new List<List<LinePrimitive>>();
        foreach (var line in lines.OrderBy(item => item.Offset))
        {
            List<LinePrimitive>? target = null;
            foreach (var cluster in clusters)
            {
                if (IsCollinear(cluster[0], line))
                {
                    target = cluster;
                    break;
                }
            }

            if (target == null)
            {
                target = new List<LinePrimitive>();
                clusters.Add(target);
            }
            target.Add(line);
        }

        return clusters;
    }

    static List<Dictionary<string, object?>> MergeClusterIntervals(List<LinePrimitive> cluster)
    {
        var result = new List<Dictionary<string, object?>>();
        if (cluster.Count == 0)
            return result;

        var basis = cluster[0];
        var intervals = cluster
            .Select(line => new LineInterval(line, Math.Min(line.T0, line.T1), Math.Max(line.T0, line.T1)))
            .OrderBy(item => item.Start)
            .ToList();

        var currentStart = intervals[0].Start;
        var currentEnd = intervals[0].End;
        var currentLines = new List<LinePrimitive> { intervals[0].Line };

        void Flush()
        {
            var start = basis.PointAt(currentStart);
            var end = basis.PointAt(currentEnd);
            var rawLengthSum = currentLines.Sum(line => line.Length);
            var coveredLength = Math.Max(0.0, currentEnd - currentStart);
            result.Add(new Dictionary<string, object?>
            {
                ["kind"] = "line",
                ["start"] = start,
                ["end"] = end,
                ["length"] = coveredLength,
                ["angle_deg"] = basis.AngleDeg,
                ["layer"] = basis.Layer,
                ["linetype"] = basis.Linetype,
                ["color"] = basis.Color,
                ["lineweight"] = basis.Lineweight,
                ["style_key"] = basis.StyleKey,
                ["source_handles"] = currentLines.SelectMany(line => line.SourceHandles).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToList(),
                ["source_indices"] = currentLines.Select(line => line.Index).Distinct().ToList(),
                ["merge_count"] = currentLines.Count,
                ["merge_type"] = "collinear_interval_union",
                ["covered_length"] = coveredLength,
                ["raw_length_sum"] = rawLengthSum,
                ["overlap_length"] = Math.Max(0.0, rawLengthSum - coveredLength)
            });
        }

        foreach (var interval in intervals.Skip(1))
        {
            if (interval.Start <= currentEnd + GapTolerance)
            {
                currentEnd = Math.Max(currentEnd, interval.End);
                currentLines.Add(interval.Line);
                continue;
            }

            Flush();
            currentStart = interval.Start;
            currentEnd = interval.End;
            currentLines = new List<LinePrimitive> { interval.Line };
        }

        Flush();
        return result;
    }

    static Dictionary<string, object?> BasePrimitive(Dictionary<string, object?> entity, string kind, Dictionary<string, object?> extra)
    {
        var handle = ReadDictString(entity, "handle");
        var result = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
        {
            ["kind"] = kind,
            ["space"] = ReadDictString(entity, "space"),
            ["index"] = ReadDictInt(entity, "index"),
            ["object_name"] = ReadDictString(entity, "object_name"),
            ["handle"] = handle,
            ["source_handles"] = string.IsNullOrWhiteSpace(handle) ? new List<string>() : new List<string> { handle },
            ["layer"] = ReadDictString(entity, "layer"),
            ["linetype"] = ReadDictString(entity, "linetype"),
            ["color"] = ReadDictInt(entity, "color"),
            ["lineweight"] = ReadDictInt(entity, "lineweight")
        };
        foreach (var item in extra)
            result[item.Key] = item.Value;
        result["style_key"] = StyleKey(result);
        return result;
    }

    static bool IsCollinear(LinePrimitive a, LinePrimitive b)
    {
        if (Math.Abs(AngleDeltaDeg(a.AngleDeg, b.AngleDeg)) > AngleToleranceDeg)
            return false;
        return Math.Abs(a.SignedDistanceToLine(b.StartX, b.StartY)) <= CollinearDistanceTolerance &&
               Math.Abs(a.SignedDistanceToLine(b.EndX, b.EndY)) <= CollinearDistanceTolerance;
    }

    static int AngleBucket(double angleDeg) => (int)Math.Round(NormalizeAngleDeg(angleDeg) / AngleToleranceDeg);

    static double NormalizeAngleDeg(double angle)
    {
        angle %= 180.0;
        if (angle < 0)
            angle += 180.0;
        return angle;
    }

    static double AngleDeltaDeg(double a, double b)
    {
        var delta = Math.Abs(NormalizeAngleDeg(a) - NormalizeAngleDeg(b));
        return Math.Min(delta, 180.0 - delta);
    }

    static string StyleKey(Dictionary<string, object?> item)
    {
        return string.Join("|",
            ReadDictString(item, "space"),
            ReadDictString(item, "layer"),
            ReadDictString(item, "linetype"),
            ReadDictInt(item, "color")?.ToString(CultureInfo.InvariantCulture) ?? "",
            ReadDictInt(item, "lineweight")?.ToString(CultureInfo.InvariantCulture) ?? "");
    }

    static List<double> PointOnCircle(List<double> center, double radius, double angleRad)
    {
        return new List<double>
        {
            center[0] + radius * Math.Cos(angleRad),
            center[1] + radius * Math.Sin(angleRad)
        };
    }

    static string ReadDictString(Dictionary<string, object?> item, string key)
    {
        return item.TryGetValue(key, out var value) ? Convert.ToString(value, CultureInfo.InvariantCulture) ?? "" : "";
    }

    static int? ReadDictInt(Dictionary<string, object?> item, string key)
    {
        return item.TryGetValue(key, out var value) ? ToInt(value) : null;
    }

    static double? ReadDictDouble(Dictionary<string, object?> item, string key)
    {
        return item.TryGetValue(key, out var value) ? ToDouble(value) : null;
    }

    static bool? ReadDictBool(Dictionary<string, object?> item, string key)
    {
        return item.TryGetValue(key, out var value) ? ToBool(value) : null;
    }

    static List<double> ReadPoint(Dictionary<string, object?> item, string key) => ReadDoubleList(item, key);

    static List<double> ReadDoubleList(Dictionary<string, object?> item, string key)
    {
        return item.TryGetValue(key, out var value) ? DoubleList(value) : new List<double>();
    }

    static void SelectMainView(DocumentExtraction extraction, string seedMode, string mainViewReferenceSelectionPath)
    {
        if (string.Equals(seedMode, "solidworks-candidate", StringComparison.OrdinalIgnoreCase))
            SelectSolidWorksCandidateMainView(extraction);
        else if (string.Equals(seedMode, "solidworks-multiview", StringComparison.OrdinalIgnoreCase))
            SelectSolidWorksMultiviewMainView(extraction, mainViewReferenceSelectionPath);
        else if (string.Equals(seedMode, "solidworks-multiview-inner-frame", StringComparison.OrdinalIgnoreCase))
            SelectSolidWorksMultiviewInnerFrameMainView(extraction);
        else
            SelectUpperLeftMainView(extraction);
    }

    static void SelectUpperLeftMainView(DocumentExtraction extraction)
    {
        var rawNodes = BuildRawGeometryNodes(extraction.Geometry)
            .Where(node => !string.Equals(node.Kind, "hatch", StringComparison.OrdinalIgnoreCase))
            .ToList();
        var drawingBox = BoundingBox.Union(rawNodes.Select(node => node.Box));
        var nodes = rawNodes
            .Where(node => !IsFrameOrBorderNode(node, drawingBox))
            .ToList();
        var lineSeedCandidates = nodes
            .Where(node => string.Equals(node.Kind, "line", StringComparison.OrdinalIgnoreCase))
            .ToList();
        var seedCandidates = lineSeedCandidates.Where(IsPreferredMainViewSeed).ToList();
        if (seedCandidates.Count == 0)
            seedCandidates = lineSeedCandidates;

        if (seedCandidates.Count == 0)
        {
            extraction.MainViewSelection = new Dictionary<string, object?>
            {
                ["ok"] = false,
                ["reason"] = "no_raw_line_seed_available",
                ["selection_rule"] = "min_x_then_max_y_seed_line_connected_expansion"
            };
            return;
        }

        var seed = seedCandidates
            .OrderBy(node => node.Box.MinX)
            .ThenByDescending(node => node.Box.MaxY)
            .First();

        var selected = ExpandConnectedNodes(seed, nodes);
        var selectedBox = BoundingBoxForViewBbox(selected, out _, out _);

        // Include isolated holes/arcs/circles inside the seed-connected outline; they belong to
        // the same view but may not touch the contour geometrically.
        foreach (var node in nodes)
        {
            if (selected.Contains(node))
                continue;
            if (selectedBox.Expand(MainViewConnectTolerance).Contains(node.Box.CenterX, node.Box.CenterY))
                selected.Add(node);
        }

        selectedBox = BoundingBoxForViewBbox(
            selected,
            out var bboxExcludedCenterlineNodeCount,
            out var bboxExcludedSectionCutSymbolNodeCount);
        var selectedHandles = selected
            .SelectMany(node => node.SourceHandles)
            .Where(handle => !string.IsNullOrWhiteSpace(handle))
            .ToHashSet(StringComparer.OrdinalIgnoreCase);

        extraction.MainViewGeometry = extraction.Geometry
            .Where(item => selectedHandles.Contains(ReadDictString(item, "handle")))
            .ToList();

        extraction.MainViewSelection = new Dictionary<string, object?>
        {
            ["ok"] = true,
            ["selection_rule"] = "min_x_then_max_y_seed_line_connected_expansion",
            ["processing_order"] = "raw_geometry_seed_expand_then_main_view_normalization_merge_overlap",
            ["seed"] = new Dictionary<string, object?>
            {
                ["bbox"] = seed.Box.ToDictionary(),
                ["source_handles"] = seed.SourceHandles,
                ["object_name"] = ReadDictString(seed.Data, "object_name"),
                ["layer"] = ReadDictString(seed.Data, "layer"),
                ["linetype"] = ReadDictString(seed.Data, "linetype"),
                ["seed_filter"] = "preferred_visible_contour_line_excluding_frame"
            },
            ["bbox"] = selectedBox.ToDictionary(),
            ["bbox_policy"] = "exclude_centerline_and_section_cut_symbol_nodes_keep_geometry",
            ["bbox_excluded_centerline_node_count"] = bboxExcludedCenterlineNodeCount,
            ["bbox_excluded_section_cut_symbol_node_count"] = bboxExcludedSectionCutSymbolNodeCount,
            ["bbox_excluded_auxiliary_node_count"] = bboxExcludedCenterlineNodeCount + bboxExcludedSectionCutSymbolNodeCount,
            ["drawing_bbox"] = drawingBox.ToDictionary(),
            ["connect_tolerance"] = MainViewConnectTolerance,
            ["selected_node_count"] = selected.Count,
            ["selected_source_handle_count"] = selectedHandles.Count,
            ["main_view_geometry_count"] = extraction.MainViewGeometry.Count,
            ["excluded"] = "annotations_dimensions_text_hatch_frame_border"
        };
    }

    static void SelectSolidWorksCandidateMainView(DocumentExtraction extraction)
    {
        var rawNodes = BuildRawGeometryNodes(extraction.Geometry)
            .Where(node => !string.Equals(node.Kind, "hatch", StringComparison.OrdinalIgnoreCase))
            .ToList();
        var drawingBox = BoundingBox.Union(rawNodes.Select(node => node.Box));
        var frameSeeds = rawNodes
            .Where(node => IsOuterDrawingEdgeNode(node, drawingBox))
            .OrderByDescending(node => Math.Max(node.Box.Width, node.Box.Height))
            .ToList();
        var frameAndTitleNodes = new HashSet<PrimitiveNode>();
        foreach (var seed in frameSeeds)
            frameAndTitleNodes.UnionWith(ExpandConnectedNodes(seed, rawNodes));

        var selected = rawNodes
            .Where(node => !frameAndTitleNodes.Contains(node))
            .ToHashSet();

        if (selected.Count == 0)
        {
            extraction.MainViewSelection = new Dictionary<string, object?>
            {
                ["ok"] = false,
                ["reason"] = "no_geometry_after_frame_title_filter",
                ["selection_rule"] = "solidworks_candidate_remove_frame_title_take_remaining_geometry"
            };
            return;
        }

        var selectedBox = BoundingBox.Union(selected.Select(node => node.Box));
        var internalNodesBeforeAbsorb = rawNodes
            .Where(node => !selected.Contains(node) &&
                           selectedBox.Expand(MainViewConnectTolerance).ContainsBox(node.Box))
            .ToList();
        foreach (var node in internalNodesBeforeAbsorb)
            selected.Add(node);

        selectedBox = BoundingBox.Union(selected.Select(node => node.Box));
        var internalNodesAfterAbsorb = rawNodes
            .Where(node => !selected.Contains(node) &&
                           selectedBox.Expand(MainViewConnectTolerance).ContainsBox(node.Box))
            .ToList();
        var selectedHandles = selected
            .SelectMany(node => node.SourceHandles)
            .Where(handle => !string.IsNullOrWhiteSpace(handle))
            .ToHashSet(StringComparer.OrdinalIgnoreCase);

        extraction.MainViewGeometry = extraction.Geometry
            .Where(item => selectedHandles.Contains(ReadDictString(item, "handle")))
            .ToList();

        extraction.MainViewSelection = new Dictionary<string, object?>
        {
            ["ok"] = true,
            ["selection_rule"] = "solidworks_candidate_remove_frame_title_take_remaining_geometry",
            ["processing_order"] = "raw_geometry_expand_outer_edge_connected_frame_title_then_take_remaining_geometry",
            ["bbox"] = selectedBox.ToDictionary(),
            ["drawing_bbox"] = drawingBox.ToDictionary(),
            ["frame_seed_count"] = frameSeeds.Count,
            ["frame_title_node_count"] = frameAndTitleNodes.Count,
            ["bbox_internal_absorbed_node_count"] = internalNodesBeforeAbsorb.Count,
            ["bbox_internal_missing_node_count"] = internalNodesAfterAbsorb.Count,
            ["bbox_internal_missing_handles"] = internalNodesAfterAbsorb
                .SelectMany(node => node.SourceHandles)
                .Where(handle => !string.IsNullOrWhiteSpace(handle))
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToList(),
            ["selected_node_count"] = selected.Count,
            ["selected_source_handle_count"] = selectedHandles.Count,
            ["main_view_geometry_count"] = extraction.MainViewGeometry.Count,
            ["excluded"] = "annotations_dimensions_text_hatch_frame_border_solidworks_title_block"
        };
    }

    static void SelectSolidWorksMultiviewMainView(DocumentExtraction extraction, string mainViewReferenceSelectionPath)
    {
        var rawNodes = BuildRawGeometryNodes(extraction.Geometry)
            .Where(node => !string.Equals(node.Kind, "hatch", StringComparison.OrdinalIgnoreCase))
            .ToList();
        var drawingBox = BoundingBox.Union(rawNodes.Select(node => node.Box));
        var frameSeeds = rawNodes
            .Where(node => IsOuterDrawingEdgeNode(node, drawingBox))
            .OrderByDescending(node => Math.Max(node.Box.Width, node.Box.Height))
            .ToList();
        var frameAndTitleNodes = new HashSet<PrimitiveNode>();
        foreach (var seed in frameSeeds)
            frameAndTitleNodes.UnionWith(ExpandConnectedNodes(seed, rawNodes));

        var remaining = rawNodes
            .Where(node => !frameAndTitleNodes.Contains(node))
            .ToList();
        if (remaining.Count == 0)
        {
            extraction.MainViewSelection = new Dictionary<string, object?>
            {
                ["ok"] = false,
                ["reason"] = "no_geometry_after_frame_title_filter",
                ["selection_rule"] = "solidworks_multiview_remove_frame_title_then_pick_upper_left_component"
            };
            return;
        }

        if (TrySelectSolidWorksMultiviewMainViewByReferenceBox(
                extraction,
                remaining,
                drawingBox,
                frameSeeds.Count,
                frameAndTitleNodes.Count,
                mainViewReferenceSelectionPath))
        {
            return;
        }

        var components = new List<HashSet<PrimitiveNode>>();
        var unassigned = new HashSet<PrimitiveNode>(remaining);
        while (unassigned.Count > 0)
        {
            var seed = unassigned
                .OrderBy(node => node.Box.MinX)
                .ThenByDescending(node => node.Box.MaxY)
                .First();
            var component = ExpandConnectedNodes(seed, remaining);
            components.Add(component);
            unassigned.ExceptWith(component);
        }

        var selected = components
            .Select(component => new
            {
                Nodes = component,
                Box = BoundingBox.Union(component.Select(node => node.Box))
            })
            .OrderBy(item => item.Box.MinX)
            .ThenByDescending(item => item.Box.MaxY)
            .First();

        var selectedNodes = selected.Nodes;
        var selectedBox = selected.Box;
        var internalNodes = remaining
            .Where(node => !selectedNodes.Contains(node) &&
                           selectedBox.Expand(MainViewConnectTolerance).Contains(node.Box.CenterX, node.Box.CenterY))
            .ToList();
        foreach (var node in internalNodes)
            selectedNodes.Add(node);

        selectedBox = BoundingBox.Union(selectedNodes.Select(node => node.Box));
        var selectedHandles = selectedNodes
            .SelectMany(node => node.SourceHandles)
            .Where(handle => !string.IsNullOrWhiteSpace(handle))
            .ToHashSet(StringComparer.OrdinalIgnoreCase);

        extraction.MainViewGeometry = extraction.Geometry
            .Where(item => selectedHandles.Contains(ReadDictString(item, "handle")))
            .ToList();

        extraction.MainViewSelection = new Dictionary<string, object?>
        {
            ["ok"] = true,
            ["selection_rule"] = "solidworks_multiview_remove_frame_title_then_pick_upper_left_component",
            ["processing_order"] = "remove_outer_edge_connected_frame_title_then_connected_components_pick_min_x_max_y",
            ["bbox"] = selectedBox.ToDictionary(),
            ["drawing_bbox"] = drawingBox.ToDictionary(),
            ["frame_seed_count"] = frameSeeds.Count,
            ["frame_title_node_count"] = frameAndTitleNodes.Count,
            ["component_count"] = components.Count,
            ["bbox_internal_absorbed_node_count"] = internalNodes.Count,
            ["selected_node_count"] = selectedNodes.Count,
            ["selected_source_handle_count"] = selectedHandles.Count,
            ["main_view_geometry_count"] = extraction.MainViewGeometry.Count,
            ["excluded"] = "annotations_dimensions_text_hatch_frame_border_solidworks_title_block_other_projected_views"
        };
    }

    static bool TrySelectSolidWorksMultiviewMainViewByReferenceBox(
        DocumentExtraction extraction,
        List<PrimitiveNode> remaining,
        BoundingBox drawingBox,
        int frameSeedCount,
        int frameTitleNodeCount,
        string mainViewReferenceSelectionPath)
    {
        if (string.IsNullOrWhiteSpace(mainViewReferenceSelectionPath))
            return false;
        if (!File.Exists(mainViewReferenceSelectionPath))
            throw new FileNotFoundException($"main view reference selection 不存在: {mainViewReferenceSelectionPath}");

        var referenceBox = ReadReferenceSelectionBox(mainViewReferenceSelectionPath);
        if (referenceBox == null)
            throw new InvalidOperationException($"main view reference selection 缺少 bbox: {mainViewReferenceSelectionPath}");

        var expandedReferenceBox = referenceBox.Value.Expand(Math.Max(MainViewConnectTolerance, 1.0));
        var selectedNodes = remaining
            .Where(node => expandedReferenceBox.ContainsBox(node.Box) ||
                           expandedReferenceBox.Contains(node.Box.CenterX, node.Box.CenterY) ||
                           BoxesOverlap(expandedReferenceBox, node.Box, MainViewConnectTolerance))
            .ToHashSet();
        if (selectedNodes.Count == 0)
            return false;

        var selectedBox = BoundingBox.Union(selectedNodes.Select(node => node.Box));
        var absorbedNodes = remaining
            .Where(node => !selectedNodes.Contains(node) &&
                           selectedBox.Expand(MainViewConnectTolerance).ContainsBox(node.Box))
            .ToList();
        foreach (var node in absorbedNodes)
            selectedNodes.Add(node);

        selectedBox = BoundingBox.Union(selectedNodes.Select(node => node.Box));
        var selectedHandles = selectedNodes
            .SelectMany(node => node.SourceHandles)
            .Where(handle => !string.IsNullOrWhiteSpace(handle))
            .ToHashSet(StringComparer.OrdinalIgnoreCase);

        extraction.MainViewGeometry = extraction.Geometry
            .Where(item => selectedHandles.Contains(ReadDictString(item, "handle")))
            .ToList();

        extraction.MainViewSelection = new Dictionary<string, object?>
        {
            ["ok"] = true,
            ["selection_rule"] = "solidworks_multiview_reference_main_bbox",
            ["processing_order"] = "remove_outer_edge_connected_frame_title_then_select_geometry_inside_previous_main_view_bbox",
            ["bbox"] = selectedBox.ToDictionary(),
            ["reference_bbox"] = referenceBox.Value.ToDictionary(),
            ["reference_selection_path"] = mainViewReferenceSelectionPath,
            ["drawing_bbox"] = drawingBox.ToDictionary(),
            ["frame_seed_count"] = frameSeedCount,
            ["frame_title_node_count"] = frameTitleNodeCount,
            ["bbox_internal_absorbed_node_count"] = absorbedNodes.Count,
            ["selected_node_count"] = selectedNodes.Count,
            ["selected_source_handle_count"] = selectedHandles.Count,
            ["main_view_geometry_count"] = extraction.MainViewGeometry.Count,
            ["excluded"] = "annotations_dimensions_text_hatch_frame_border_solidworks_title_block_other_projected_views"
        };
        return true;
    }

    static void SelectSolidWorksMultiviewInnerFrameMainView(DocumentExtraction extraction)
    {
        var rawNodes = BuildRawGeometryNodes(extraction.Geometry)
            .Where(node => !string.Equals(node.Kind, "hatch", StringComparison.OrdinalIgnoreCase))
            .ToList();
        var drawingBox = BoundingBox.Union(rawNodes.Select(node => node.Box));
        if (!TryFindInnerFrameAnchors(rawNodes, drawingBox, out var leftFrame, out var topFrame))
        {
            extraction.MainViewSelection = new Dictionary<string, object?>
            {
                ["ok"] = false,
                ["reason"] = "inner_frame_left_or_top_line_not_found",
                ["selection_rule"] = "solidworks_multiview_inner_frame_seed_spatial_expansion",
                ["drawing_bbox"] = drawingBox.ToDictionary()
            };
            return;
        }

        var frameSeeds = rawNodes
            .Where(node => IsOuterDrawingEdgeNode(node, drawingBox))
            .OrderByDescending(node => Math.Max(node.Box.Width, node.Box.Height))
            .ToList();
        var frameAndTitleNodes = new HashSet<PrimitiveNode>();
        foreach (var seed in frameSeeds)
            frameAndTitleNodes.UnionWith(ExpandConnectedNodes(seed, rawNodes));

        var remaining = rawNodes
            .Where(node => !frameAndTitleNodes.Contains(node))
            .ToList();
        var innerTolerance = Math.Max(MainViewConnectTolerance, Math.Min(drawingBox.Width, drawingBox.Height) * 0.001);
        var seedCandidates = remaining
            .Where(node => string.Equals(node.Kind, "line", StringComparison.OrdinalIgnoreCase))
            .Where(node => node.Box.MinX > leftFrame!.Box.MinX + innerTolerance)
            .Where(node => node.Box.MaxY < topFrame!.Box.MaxY - innerTolerance)
            .ToList();
        var preferredSeedCandidates = seedCandidates.Where(IsPreferredMainViewSeed).ToList();
        if (preferredSeedCandidates.Count > 0)
            seedCandidates = preferredSeedCandidates;

        if (seedCandidates.Count == 0)
        {
            extraction.MainViewSelection = new Dictionary<string, object?>
            {
                ["ok"] = false,
                ["reason"] = "no_seed_line_inside_inner_frame",
                ["selection_rule"] = "solidworks_multiview_inner_frame_seed_spatial_expansion",
                ["drawing_bbox"] = drawingBox.ToDictionary(),
                ["inner_frame_left"] = NodeToDictionary(leftFrame!),
                ["inner_frame_top"] = NodeToDictionary(topFrame!),
                ["frame_seed_count"] = frameSeeds.Count,
                ["frame_title_node_count"] = frameAndTitleNodes.Count
            };
            return;
        }

        var seedLine = seedCandidates
            .OrderBy(node => node.Box.MinX)
            .ThenByDescending(node => node.Box.MaxY)
            .ThenByDescending(node => Math.Max(node.Box.Width, node.Box.Height))
            .First();
        var selectedRegion = ExpandRegionFromSeed(
            seedLine,
            remaining,
            new HashSet<string>(StringComparer.OrdinalIgnoreCase));
        var selectedNodes = selectedRegion.Nodes;
        var selectedBox = BoundingBoxForViewBbox(selectedNodes, out _, out _);

        var changed = true;
        var absorbedNodeCount = 0;
        while (changed)
        {
            changed = false;
            var expandedSelectedBox = selectedBox.Expand(MainViewConnectTolerance);
            var internalNodes = remaining
                .Where(node => !selectedNodes.Contains(node))
                .Where(node => expandedSelectedBox.ContainsBox(node.Box) ||
                               expandedSelectedBox.Contains(node.Box.CenterX, node.Box.CenterY))
                .ToList();
            foreach (var node in internalNodes)
                selectedNodes.Add(node);
            if (internalNodes.Count == 0)
                continue;

            absorbedNodeCount += internalNodes.Count;
            selectedBox = BoundingBoxForViewBbox(selectedNodes, out _, out _);
            changed = true;
        }

        selectedBox = BoundingBoxForViewBbox(
            selectedNodes,
            out var bboxExcludedCenterlineNodeCount,
            out var bboxExcludedSectionCutSymbolNodeCount);

        var selectedHandles = selectedNodes
            .SelectMany(node => node.SourceHandles)
            .Where(handle => !string.IsNullOrWhiteSpace(handle))
            .ToHashSet(StringComparer.OrdinalIgnoreCase);

        extraction.MainViewGeometry = extraction.Geometry
            .Where(item => selectedHandles.Contains(ReadDictString(item, "handle")))
            .ToList();

        extraction.MainViewSelection = new Dictionary<string, object?>
        {
            ["ok"] = true,
            ["selection_rule"] = "solidworks_multiview_inner_frame_seed_spatial_expansion",
            ["processing_order"] = "find_inner_left_top_frame_lines_then_pick_min_inner_x_max_y_seed_and_expand_spatial_view_region",
            ["bbox"] = selectedBox.ToDictionary(),
            ["bbox_policy"] = "exclude_centerline_and_section_cut_symbol_nodes_keep_geometry",
            ["bbox_excluded_centerline_node_count"] = bboxExcludedCenterlineNodeCount,
            ["bbox_excluded_section_cut_symbol_node_count"] = bboxExcludedSectionCutSymbolNodeCount,
            ["bbox_excluded_auxiliary_node_count"] = bboxExcludedCenterlineNodeCount + bboxExcludedSectionCutSymbolNodeCount,
            ["drawing_bbox"] = drawingBox.ToDictionary(),
            ["inner_frame_left"] = NodeToDictionary(leftFrame!),
            ["inner_frame_top"] = NodeToDictionary(topFrame!),
            ["seed"] = NodeToDictionary(seedLine),
            ["seed_candidate_count"] = seedCandidates.Count,
            ["frame_seed_count"] = frameSeeds.Count,
            ["frame_title_node_count"] = frameAndTitleNodes.Count,
            ["bbox_internal_absorbed_node_count"] = absorbedNodeCount,
            ["selected_node_count"] = selectedNodes.Count,
            ["selected_source_handle_count"] = selectedHandles.Count,
            ["main_view_geometry_count"] = extraction.MainViewGeometry.Count,
            ["excluded"] = "annotations_dimensions_text_hatch_frame_border_solidworks_title_block_other_projected_views"
        };
    }

    static bool TryFindInnerFrameAnchors(
        List<PrimitiveNode> rawNodes,
        BoundingBox drawingBox,
        out PrimitiveNode? leftFrame,
        out PrimitiveNode? topFrame)
    {
        var tolerance = Math.Max(MainViewConnectTolerance, Math.Min(drawingBox.Width, drawingBox.Height) * 0.001);
        var verticalMinHeight = drawingBox.Height * 0.45;
        var horizontalMinWidth = drawingBox.Width * 0.45;

        leftFrame = rawNodes
            .Where(node => IsVerticalLineNode(node, tolerance))
            .Where(node => node.Box.Height >= verticalMinHeight)
            .Where(node => node.Box.MinX > drawingBox.MinX + tolerance)
            .Where(node => node.Box.CenterX < drawingBox.CenterX)
            .OrderByDescending(node => node.Box.Height)
            .ThenBy(node => node.Box.MinX)
            .FirstOrDefault();

        topFrame = rawNodes
            .Where(node => IsHorizontalLineNode(node, tolerance))
            .Where(node => node.Box.Width >= horizontalMinWidth)
            .Where(node => node.Box.MaxY < drawingBox.MaxY - tolerance)
            .Where(node => node.Box.CenterY > drawingBox.CenterY)
            .OrderByDescending(node => node.Box.Width)
            .ThenByDescending(node => node.Box.MaxY)
            .FirstOrDefault();

        return leftFrame != null && topFrame != null;
    }

    static BoundingBox? ReadReferenceSelectionBox(string path)
    {
        using var document = JsonDocument.Parse(File.ReadAllText(path));
        if (!document.RootElement.TryGetProperty("bbox", out var bbox) ||
            bbox.ValueKind != JsonValueKind.Object)
            return null;

        if (!TryGetJsonDouble(bbox, "min_x", out var minX) ||
            !TryGetJsonDouble(bbox, "min_y", out var minY) ||
            !TryGetJsonDouble(bbox, "max_x", out var maxX) ||
            !TryGetJsonDouble(bbox, "max_y", out var maxY))
        {
            return null;
        }

        return new BoundingBox(minX, minY, maxX, maxY);
    }

    static bool TryGetJsonDouble(JsonElement element, string propertyName, out double value)
    {
        value = 0;
        return element.TryGetProperty(propertyName, out var property) &&
               property.ValueKind == JsonValueKind.Number &&
               property.TryGetDouble(out value);
    }

    static void AnalyzeViewRegions(DocumentExtraction extraction, CliOptions options)
    {
        var rawNodes = BuildRawGeometryNodes(extraction.Geometry)
            .Where(node => !string.Equals(node.Kind, "hatch", StringComparison.OrdinalIgnoreCase))
            .ToList();
        if (rawNodes.Count == 0)
        {
            extraction.ViewRegions = new Dictionary<string, object?>
            {
                ["ok"] = false,
                ["reason"] = "no_raw_geometry_nodes"
            };
            return;
        }

        var drawingBox = BoundingBox.Union(rawNodes.Select(node => node.Box));
        var nodes = rawNodes
            .Where(node => !IsFrameOrBorderNode(node, drawingBox))
            .ToList();
        var mainBox = ReadEntityBox(extraction.MainViewSelection, "bbox");
        if (mainBox == null)
        {
            extraction.ViewRegions = new Dictionary<string, object?>
            {
                ["ok"] = false,
                ["reason"] = "main_view_bbox_missing",
                ["drawing_bbox"] = drawingBox.ToDictionary()
            };
            return;
        }

        var mainHandles = extraction.MainViewGeometry
            .Select(item => ReadDictString(item, "handle"))
            .Where(handle => !string.IsNullOrWhiteSpace(handle))
            .ToHashSet(StringComparer.OrdinalIgnoreCase);

        var sectionSymbols = FindSectionSymbols(extraction.Annotations)
            .OrderByDescending(symbol => symbol.Box.CenterY)
            .ThenBy(symbol => symbol.Box.CenterX)
            .ToList();
        var sectionViews = new List<Dictionary<string, object?>>();
        var sectionHandles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        foreach (var symbol in sectionSymbols)
        {
            var seed = FindSectionSeed(symbol, nodes, mainHandles, sectionHandles, options.SectionViewProbeDirection);
            if (seed == null)
            {
                sectionViews.Add(new Dictionary<string, object?>
                {
                    ["ok"] = false,
                    ["reason"] = $"no_geometry_seed_{options.SectionViewProbeDirection}_section_symbol",
                    ["probe_direction"] = options.SectionViewProbeDirection,
                    ["label"] = symbol.Label,
                    ["symbol"] = symbol.ToDictionary()
                });
                continue;
            }

            var excluded = new HashSet<string>(mainHandles, StringComparer.OrdinalIgnoreCase);
            excluded.UnionWith(sectionHandles);
            var region = ExpandRegionFromSeed(seed, nodes, excluded);
            if (sectionViews.Any(item =>
                    ReadEntityBox(item, "bbox") is { } existing &&
                    BoxesOverlap(existing, region.Box, MainViewConnectTolerance)))
            {
                continue;
            }

            foreach (var handle in region.Handles)
                sectionHandles.Add(handle);
            sectionViews.Add(new Dictionary<string, object?>
            {
                ["ok"] = true,
                ["kind"] = "section_view",
                ["label"] = symbol.Label,
                ["probe_direction"] = options.SectionViewProbeDirection,
                ["symbol"] = symbol.ToDictionary(),
                ["seed"] = NodeToDictionary(seed),
                ["bbox"] = region.Box.ToDictionary(),
                ["geometry_node_count"] = region.Nodes.Count,
                ["source_handle_count"] = region.Handles.Count,
                ["source_handles"] = region.Handles.OrderBy(handle => handle, StringComparer.OrdinalIgnoreCase).ToList()
            });
        }

        var orthographicViews = new List<Dictionary<string, object?>>();
        orthographicViews.Add(ProbeOrthographicView(
            "down",
            "bottom_view_candidate",
            mainBox.Value,
            nodes,
            mainHandles,
            sectionHandles));
        orthographicViews.Add(ProbeOrthographicView(
            "right",
            "right_of_main_view_candidate",
            mainBox.Value,
            nodes,
            mainHandles,
            sectionHandles));

        foreach (var sectionView in sectionViews)
            AttachRegionSignature(sectionView, extraction.Geometry, ReadDictString(sectionView, "kind"));
        foreach (var orthographicView in orthographicViews)
            AttachRegionSignature(orthographicView, extraction.Geometry, ReadDictString(orthographicView, "kind"));

        var bottomBox = orthographicViews
            .Where(item => string.Equals(ReadDictString(item, "kind"), "bottom_view_candidate", StringComparison.OrdinalIgnoreCase))
            .Where(item => item.TryGetValue("ok", out var ok) && ok is true)
            .Select(item => ReadEntityBox(item, "bbox"))
            .FirstOrDefault(item => item != null);
        extraction.SectionCutSymbols = FindSectionCutSymbols(extraction.Annotations, mainBox.Value, bottomBox);

        extraction.BottomViewSignature = orthographicViews
            .Where(item => string.Equals(ReadDictString(item, "kind"), "bottom_view_candidate", StringComparison.OrdinalIgnoreCase))
            .Where(item => item.TryGetValue("ok", out var ok) && ok is true)
            .Select(item => item.TryGetValue("signature", out var signature) && signature is Dictionary<string, object?> dict ? dict : null)
            .FirstOrDefault(item => item != null)
            ?? new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);

        extraction.ViewRegions = new Dictionary<string, object?>
        {
            ["ok"] = true,
            ["rule"] = $"section_symbols_expand_{options.SectionViewProbeDirection}_then_probe_orthographic_down_right_from_main_bbox_center",
            ["section_view_probe_direction"] = options.SectionViewProbeDirection,
            ["main_view_bbox"] = mainBox.Value.ToDictionary(),
            ["drawing_bbox"] = drawingBox.ToDictionary(),
            ["section_symbol_count"] = sectionSymbols.Count,
            ["section_view_count"] = sectionViews.Count(item => item.TryGetValue("ok", out var ok) && ok is true),
            ["section_views"] = sectionViews,
            ["section_cut_symbol_count"] = extraction.SectionCutSymbols.Count,
            ["section_cut_symbols"] = extraction.SectionCutSymbols,
            ["orthographic_probe_directions"] = new[] { "down", "right" },
            ["orthographic_views"] = orthographicViews,
            ["excluded_from_orthographic_probe"] = "main_view_handles_and_section_view_handles"
        };
    }

    static List<Dictionary<string, object?>> FindSectionCutSymbols(
        List<Dictionary<string, object?>> annotations,
        BoundingBox mainBox,
        BoundingBox? bottomBox)
    {
        var result = new List<Dictionary<string, object?>>();
        foreach (var block in annotations)
        {
            if (!block.TryGetValue("exploded_entities", out var explodedValue) ||
                explodedValue is not IEnumerable<object> explodedObjects)
            {
                continue;
            }

            var children = explodedObjects
                .OfType<Dictionary<string, object?>>()
                .ToList();
            if (children.Count == 0)
                continue;

            var labels = children
                .Select(child => new
                {
                    Text = ExtractSingleLetterLabel(ReadDictString(child, "text")),
                    Box = ReadEntityBox(child, "bbox"),
                    Child = child
                })
                .Where(item => item.Text != null && item.Box != null)
                .ToList();
            if (labels.Count != 2 || !string.Equals(labels[0].Text, labels[1].Text, StringComparison.OrdinalIgnoreCase))
                continue;

            var lines = children
                .Where(child => string.Equals(ReadDictString(child, "object_name"), "AcDbLine", StringComparison.OrdinalIgnoreCase))
                .ToList();
            var hatches = children
                .Where(child => string.Equals(ReadDictString(child, "object_name"), "AcDbHatch", StringComparison.OrdinalIgnoreCase))
                .Select(child => new { Box = ReadEntityBox(child, "bbox"), Child = child })
                .Where(item => item.Box != null)
                .ToList();
            if (lines.Count < 4 || hatches.Count < 2)
                continue;

            var blockBox = ReadEntityBox(block, "bbox") ?? BoundingBox.Union(children.Select(child => ReadEntityBox(child, "bbox")).Where(box => box != null).Select(box => box!.Value));
            var markers = hatches
                .OrderByDescending(item => item.Box!.Value.CenterY)
                .Select(item => BuildSectionCutMarker(item.Box!.Value, lines, labels.Select(label => label.Box!.Value).ToList()))
                .Where(item => item != null)
                .Cast<Dictionary<string, object?>>()
                .ToList();
            if (markers.Count < 2)
                continue;

            var corners = markers
                .Select(marker => ReadPoint(marker, "corner"))
                .Where(point => point.Count >= 2)
                .ToList();
            if (corners.Count < 2)
                continue;

            var cornerPair = FindFarthestPointPair(corners);
            var rawStart = cornerPair.Start;
            var rawEnd = cornerPair.End;
            var cutDirection = Math.Abs(rawStart[0] - rawEnd[0]) <= Math.Abs(rawStart[1] - rawEnd[1])
                ? "vertical"
                : "horizontal";
            var cornerStart = cutDirection == "vertical"
                ? new[] { rawStart, rawEnd }.OrderByDescending(point => point[1]).ThenBy(point => point[0]).First()
                : new[] { rawStart, rawEnd }.OrderBy(point => point[0]).ThenByDescending(point => point[1]).First();
            var cornerEnd = cutDirection == "vertical"
                ? new[] { rawStart, rawEnd }.OrderBy(point => point[1]).ThenBy(point => point[0]).First()
                : new[] { rawStart, rawEnd }.OrderByDescending(point => point[0]).ThenByDescending(point => point[1]).First();
            var cutCenter = new List<double>
            {
                (cornerStart[0] + cornerEnd[0]) / 2.0,
                (cornerStart[1] + cornerEnd[1]) / 2.0
            };
            var cornerDistance = Distance(cornerStart, cornerEnd);
            var halfLength = Math.Max(cornerDistance / 2.0, 1.0);
            var cutLineStart = cutDirection == "vertical"
                ? new List<double> { cutCenter[0], cutCenter[1] + halfLength }
                : new List<double> { cutCenter[0] - halfLength, cutCenter[1] };
            var cutLineEnd = cutDirection == "vertical"
                ? new List<double> { cutCenter[0], cutCenter[1] - halfLength }
                : new List<double> { cutCenter[0] + halfLength, cutCenter[1] };
            var viewKind = ClassifySectionCutView(blockBox, mainBox, bottomBox);

            result.Add(new Dictionary<string, object?>
            {
                ["ok"] = true,
                ["kind"] = "section_cut_symbol_pair",
                ["label"] = labels[0].Text,
                ["source"] = "block_reference_exploded_entities",
                ["parent_handle"] = ReadDictString(block, "handle"),
                ["block_name"] = ReadDictString(block, "name"),
                ["effective_name"] = ReadDictString(block, "effective_name"),
                ["view_kind"] = viewKind,
                ["bbox"] = blockBox.ToDictionary(),
                ["text_count"] = labels.Count,
                ["line_count"] = lines.Count,
                ["arrow_hatch_count"] = hatches.Count,
                ["cut_line"] = new Dictionary<string, object?>
                {
                    ["start"] = cutLineStart,
                    ["end"] = cutLineEnd,
                    ["center"] = cutCenter,
                    ["direction"] = cutDirection,
                    ["source"] = "paired_section_symbol_corners_common_center",
                    ["corner_start"] = cornerStart,
                    ["corner_end"] = cornerEnd,
                    ["corner_count"] = corners.Count
                },
                ["markers"] = markers
            });
        }

        return result
            .OrderBy(item => ReadDictString(item, "label"), StringComparer.OrdinalIgnoreCase)
            .ThenBy(item => ReadEntityBox(item, "bbox")?.CenterX ?? 0)
            .ThenByDescending(item => ReadEntityBox(item, "bbox")?.CenterY ?? 0)
            .ToList();
    }

    static Dictionary<string, object?>? BuildSectionCutMarker(
        BoundingBox arrowBox,
        List<Dictionary<string, object?>> lines,
        List<BoundingBox> labelBoxes)
    {
        var candidates = new List<(Dictionary<string, object?> Marker, double Score)>();
        var horizontal = BuildSectionCutMarkerForLeaderAxis(arrowBox, lines, labelBoxes, leaderIsHorizontal: true);
        if (horizontal != null)
            candidates.Add(horizontal.Value);
        var vertical = BuildSectionCutMarkerForLeaderAxis(arrowBox, lines, labelBoxes, leaderIsHorizontal: false);
        if (vertical != null)
            candidates.Add(vertical.Value);

        return candidates
            .OrderBy(candidate => candidate.Score)
            .Select(candidate => candidate.Marker)
            .FirstOrDefault();
    }

    static (Dictionary<string, object?> Marker, double Score)? BuildSectionCutMarkerForLeaderAxis(
        BoundingBox arrowBox,
        List<Dictionary<string, object?>> lines,
        List<BoundingBox> labelBoxes,
        bool leaderIsHorizontal)
    {
        var arrowCenter = new List<double> { arrowBox.CenterX, arrowBox.CenterY };
        var lineItems = lines
            .Select(line => new { Line = line, Start = ReadPoint(line, "start"), End = ReadPoint(line, "end"), Box = ReadEntityBox(line, "bbox") })
            .Where(item => item.Start.Count >= 2 && item.End.Count >= 2 && item.Box != null)
            .ToList();
        var leaders = lineItems
            .Where(item => leaderIsHorizontal
                ? Math.Abs(item.Start[1] - item.End[1]) <= 1e-6
                : Math.Abs(item.Start[0] - item.End[0]) <= 1e-6)
            .OrderBy(item => leaderIsHorizontal
                ? Math.Abs(item.Box!.Value.CenterY - arrowBox.CenterY)
                : Math.Abs(item.Box!.Value.CenterX - arrowBox.CenterX))
            .ThenBy(item => item.Box!.Value.DistanceTo(arrowBox))
            .FirstOrDefault();
        if (leaders == null)
            return null;

        var endpoints = new[] { leaders.Start, leaders.End };
        var sectionSymbolLines = lineItems
            .Where(item => leaderIsHorizontal
                ? Math.Abs(item.Start[0] - item.End[0]) <= 1e-6
                : Math.Abs(item.Start[1] - item.End[1]) <= 1e-6)
            .ToList();
        var cornerCandidate = endpoints
            .Select(point => new
            {
                Point = point,
                SectionSymbolLine = sectionSymbolLines
                    .OrderBy(line => new[] { line.Start, line.End }.Min(vp => Distance(point, vp)))
                    .FirstOrDefault(),
                DistanceToSectionSymbol = sectionSymbolLines
                    .SelectMany(line => new[] { line.Start, line.End })
                    .Select(vp => Distance(point, vp))
                    .DefaultIfEmpty(double.MaxValue)
                    .Min()
            })
            .OrderBy(item => item.DistanceToSectionSymbol)
            .First();
        var corner = cornerCandidate.Point;
        var sectionSymbolLine = cornerCandidate.SectionSymbolLine;
        if (sectionSymbolLine == null)
            return null;

        var dx = arrowCenter[0] - corner[0];
        var dy = arrowCenter[1] - corner[1];
        var length = Math.Sqrt(dx * dx + dy * dy);
        var labelBox = labelBoxes
            .OrderBy(box => box.DistanceTo(arrowBox))
            .FirstOrDefault();

        var marker = new Dictionary<string, object?>
        {
            ["corner"] = corner,
            ["arrow_tip"] = arrowCenter,
            ["arrow_direction"] = length > 1e-9
                ? new[] { dx / length, dy / length }
                : new[] { 0.0, 0.0 },
            [leaderIsHorizontal ? "horizontal_line" : "vertical_line"] = new Dictionary<string, object?>
            {
                ["start"] = leaders.Start,
                ["end"] = leaders.End
            },
            ["section_symbol_line"] = new Dictionary<string, object?>
            {
                ["start"] = sectionSymbolLine.Start,
                ["end"] = sectionSymbolLine.End
            },
            ["label_bbox"] = labelBox.ToDictionary()
        };
        var score = (leaderIsHorizontal
                ? Math.Abs(leaders.Box!.Value.CenterY - arrowBox.CenterY)
                : Math.Abs(leaders.Box!.Value.CenterX - arrowBox.CenterX)) +
            leaders.Box!.Value.DistanceTo(arrowBox) +
            cornerCandidate.DistanceToSectionSymbol;
        return (marker, score);
    }

    static (List<double> Start, List<double> End) FindFarthestPointPair(List<List<double>> points)
    {
        var bestStart = points[0];
        var bestEnd = points[1];
        var bestDistance = Distance(bestStart, bestEnd);
        for (var i = 0; i < points.Count; i++)
        {
            for (var j = i + 1; j < points.Count; j++)
            {
                var distance = Distance(points[i], points[j]);
                if (distance > bestDistance)
                {
                    bestStart = points[i];
                    bestEnd = points[j];
                    bestDistance = distance;
                }
            }
        }

        return (bestStart, bestEnd);
    }

    static (List<double> Start, List<double> End)? ReadLine(Dictionary<string, object?> dict, string key)
    {
        if (!dict.TryGetValue(key, out var value) || value is not Dictionary<string, object?> line)
            return null;
        var start = ReadPoint(line, "start");
        var end = ReadPoint(line, "end");
        return start.Count >= 2 && end.Count >= 2 ? (start, end) : null;
    }

    static string? ExtractSingleLetterLabel(string text)
    {
        if (string.IsNullOrWhiteSpace(text))
            return null;
        var cleaned = Regex.Replace(text, @"\\[^;]*;", "");
        cleaned = Regex.Replace(cleaned, @"[{}\\]", "").Trim();
        var match = Regex.Match(cleaned, @"^[A-Z]$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
        return match.Success ? match.Value.ToUpperInvariant() : null;
    }

    static string ClassifySectionCutView(BoundingBox symbolBox, BoundingBox mainBox, BoundingBox? bottomBox)
    {
        var mainOverlap = OverlapArea(symbolBox, mainBox);
        var bottomOverlap = bottomBox == null ? 0.0 : OverlapArea(symbolBox, bottomBox.Value);
        if (mainOverlap <= 0 && bottomOverlap <= 0)
            return "unknown";
        return mainOverlap >= bottomOverlap ? "main_view" : "bottom_view_candidate";
    }

    static double OverlapArea(BoundingBox a, BoundingBox b)
    {
        var width = Math.Max(0.0, Math.Min(a.MaxX, b.MaxX) - Math.Max(a.MinX, b.MinX));
        var height = Math.Max(0.0, Math.Min(a.MaxY, b.MaxY) - Math.Max(a.MinY, b.MinY));
        return width * height;
    }

    static void AttachRegionSignature(
        Dictionary<string, object?> region,
        List<Dictionary<string, object?>> geometry,
        string scope)
    {
        if (!region.TryGetValue("ok", out var ok) || ok is not true)
            return;

        var handles = ReadStringList(region, "source_handles")
            .ToHashSet(StringComparer.OrdinalIgnoreCase);
        if (handles.Count == 0)
            return;

        var regionGeometry = geometry
            .Where(item => handles.Contains(ReadDictString(item, "handle")))
            .ToList();
        var normalized = NormalizePrimitiveSet(regionGeometry, scope);
        var signature = BuildViewSignature(normalized.Normalized, normalized.MergedLines, scope);
        signature["normalization_summary"] = normalized.Summary;
        signature["source_handle_count"] = handles.Count;
        signature["raw_geometry_count"] = regionGeometry.Count;
        region["signature"] = signature;
    }

    static List<SectionSymbol> FindSectionSymbols(List<Dictionary<string, object?>> annotations)
    {
        var result = new List<SectionSymbol>();
        foreach (var annotation in annotations)
        {
            var text = ReadDictString(annotation, "text");
            if (string.IsNullOrWhiteSpace(text))
                continue;
            var match = Regex.Match(
                text,
                @"(?<![A-Z0-9])([A-Z0-9]{1,4})\s*-\s*\1(?![A-Z0-9])(?:\s*/?\s*\d+(?:\.\d+)?\s*:\s*\d+(?:\.\d+)?)?",
                RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
            if (!match.Success)
                continue;

            var box = ReadAnnotationBox(annotation);
            if (box == null)
                continue;

            var labelToken = match.Groups[1].Value.ToUpperInvariant();
            result.Add(new SectionSymbol(labelToken + "-" + labelToken, match.Value, text, box.Value, annotation));
        }
        return result;
    }

    static BoundingBox? ReadAnnotationBox(Dictionary<string, object?> annotation)
    {
        if (ReadEntityBox(annotation, "bbox") is { } box)
            return box;

        var point = ReadPoint(annotation, "insertion_point");
        if (point.Count < 2)
            point = ReadPoint(annotation, "text_position");
        if (point.Count < 2)
            return null;

        var height = ReadDictDouble(annotation, "height") ?? 1.0;
        return new BoundingBox(point[0] - height, point[1] - height, point[0] + height, point[1] + height);
    }

    static PrimitiveNode? FindSectionSeed(
        SectionSymbol symbol,
        List<PrimitiveNode> nodes,
        HashSet<string> mainHandles,
        HashSet<string> existingSectionHandles,
        string probeDirection)
    {
        var searchHalfWidth = Math.Max(20.0, symbol.Box.Width * 8.0);
        var candidates = nodes
            .Where(node => !NodeHasAnyHandle(node, mainHandles))
            .Where(node => !NodeHasAnyHandle(node, existingSectionHandles))
            .Where(node => Math.Abs(node.Box.CenterX - symbol.Box.CenterX) <= searchHalfWidth ||
                           RangesOverlap(node.Box.MinX, node.Box.MaxX, symbol.Box.MinX - searchHalfWidth, symbol.Box.MaxX + searchHalfWidth));

        if (string.Equals(probeDirection, "up", StringComparison.OrdinalIgnoreCase))
        {
            return candidates
                .Where(node => node.Box.MinY >= symbol.Box.MaxY + ViewRegionProbeTolerance)
                .OrderBy(node => node.Box.MinY - symbol.Box.MaxY)
                .ThenBy(node => Math.Abs(node.Box.CenterX - symbol.Box.CenterX))
                .FirstOrDefault();
        }

        return candidates
            .Where(node => node.Box.MaxY <= symbol.Box.MinY - ViewRegionProbeTolerance)
            .OrderBy(node => symbol.Box.MinY - node.Box.MaxY)
            .ThenBy(node => Math.Abs(node.Box.CenterX - symbol.Box.CenterX))
            .FirstOrDefault();
    }

    static Dictionary<string, object?> ProbeOrthographicView(
        string direction,
        string kind,
        BoundingBox mainBox,
        List<PrimitiveNode> nodes,
        HashSet<string> mainHandles,
        HashSet<string> sectionHandles)
    {
        var seed = FindRayHitFromMainView(direction, mainBox, nodes, mainHandles);
        if (seed == null)
        {
            return new Dictionary<string, object?>
            {
                ["ok"] = false,
                ["kind"] = kind,
                ["direction"] = direction,
                ["reason"] = "no_ray_hit"
            };
        }

        if (NodeHasAnyHandle(seed, sectionHandles))
        {
            return new Dictionary<string, object?>
            {
                ["ok"] = false,
                ["kind"] = kind,
                ["direction"] = direction,
                ["reason"] = "first_ray_hit_belongs_to_section_view",
                ["seed"] = NodeToDictionary(seed)
            };
        }

        var excluded = new HashSet<string>(mainHandles, StringComparer.OrdinalIgnoreCase);
        excluded.UnionWith(sectionHandles);
        var region = ExpandRegionFromSeed(seed, nodes, excluded);
        return new Dictionary<string, object?>
        {
            ["ok"] = true,
            ["kind"] = kind,
            ["direction"] = direction,
            ["seed"] = NodeToDictionary(seed),
            ["bbox"] = region.Box.ToDictionary(),
            ["geometry_node_count"] = region.Nodes.Count,
            ["source_handle_count"] = region.Handles.Count,
            ["source_handles"] = region.Handles.OrderBy(handle => handle, StringComparer.OrdinalIgnoreCase).ToList()
        };
    }

    static PrimitiveNode? FindRayHitFromMainView(
        string direction,
        BoundingBox mainBox,
        List<PrimitiveNode> nodes,
        HashSet<string> mainHandles)
    {
        if (string.Equals(direction, "down", StringComparison.OrdinalIgnoreCase))
        {
            var x = mainBox.CenterX;
            var candidates = nodes
                .Where(node => !NodeHasAnyHandle(node, mainHandles))
                .Where(node => node.Box.MaxY <= mainBox.MinY - ViewRegionProbeTolerance)
                .Where(node => x >= node.Box.MinX - ViewRegionProbeTolerance && x <= node.Box.MaxX + ViewRegionProbeTolerance)
                .OrderBy(node => mainBox.MinY - node.Box.MaxY)
                .ToList();
            return candidates.FirstOrDefault();
        }

        if (string.Equals(direction, "right", StringComparison.OrdinalIgnoreCase))
        {
            var y = mainBox.CenterY;
            var candidates = nodes
                .Where(node => !NodeHasAnyHandle(node, mainHandles))
                .Where(node => node.Box.MinX >= mainBox.MaxX + ViewRegionProbeTolerance)
                .Where(node => y >= node.Box.MinY - ViewRegionProbeTolerance && y <= node.Box.MaxY + ViewRegionProbeTolerance)
                .OrderBy(node => node.Box.MinX - mainBox.MaxX)
                .ToList();
            return candidates.FirstOrDefault();
        }

        return null;
    }

    static ViewRegion ExpandRegionFromSeed(PrimitiveNode seed, List<PrimitiveNode> nodes, HashSet<string> excludedHandles)
    {
        var available = nodes
            .Where(node => ReferenceEquals(node, seed) || !NodeHasAnyHandle(node, excludedHandles))
            .ToList();
        var selected = ExpandConnectedNodes(seed, available);
        var selectedBox = BoundingBox.Union(selected.Select(node => node.Box));

        var changed = true;
        while (changed)
        {
            changed = false;
            foreach (var node in available)
            {
                if (selected.Contains(node))
                    continue;
                var expandedBox = selectedBox.Expand(ViewRegionSpatialExpansionTolerance);
                if (!expandedBox.ContainsBox(node.Box) &&
                    !BoxesOverlap(selectedBox, node.Box, ViewRegionSpatialExpansionTolerance) &&
                    !selected.Any(item => AreConnected(item, node)))
                {
                    continue;
                }

                selected.Add(node);
                selectedBox = BoundingBox.Union(selected.Select(item => item.Box));
                changed = true;
            }
        }

        var handles = selected
            .SelectMany(node => node.SourceHandles)
            .Where(handle => !string.IsNullOrWhiteSpace(handle))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToList();
        return new ViewRegion(selected, selectedBox, handles);
    }

    static bool NodeHasAnyHandle(PrimitiveNode node, HashSet<string> handles)
    {
        return node.SourceHandles.Any(handle => handles.Contains(handle));
    }

    static Dictionary<string, object?> NodeToDictionary(PrimitiveNode node)
    {
        return new Dictionary<string, object?>
        {
            ["kind"] = node.Kind,
            ["bbox"] = node.Box.ToDictionary(),
            ["source_handles"] = node.SourceHandles,
            ["object_name"] = ReadDictString(node.Data, "object_name"),
            ["layer"] = ReadDictString(node.Data, "layer"),
            ["linetype"] = ReadDictString(node.Data, "linetype")
        };
    }

    static bool RangesOverlap(double aMin, double aMax, double bMin, double bMax)
    {
        return aMin <= bMax && aMax >= bMin;
    }

    static bool BoxesOverlap(BoundingBox a, BoundingBox b, double tolerance)
    {
        return RangesOverlap(a.MinX - tolerance, a.MaxX + tolerance, b.MinX, b.MaxX) &&
               RangesOverlap(a.MinY - tolerance, a.MaxY + tolerance, b.MinY, b.MaxY);
    }

    static BoundingBox? ReadEntityBox(Dictionary<string, object?> item, string key)
    {
        if (!item.TryGetValue(key, out var value) || value == null)
            return null;
        if (value is not Dictionary<string, object?> dict)
            return null;

        var minX = ReadDictDouble(dict, "min_x");
        var minY = ReadDictDouble(dict, "min_y");
        var maxX = ReadDictDouble(dict, "max_x");
        var maxY = ReadDictDouble(dict, "max_y");
        if (minX == null || minY == null || maxX == null || maxY == null)
            return null;
        return new BoundingBox(minX.Value, minY.Value, maxX.Value, maxY.Value);
    }

    static bool IsOuterDrawingEdgeNode(PrimitiveNode node, BoundingBox drawingBox)
    {
        var tolerance = Math.Max(MainViewConnectTolerance, Math.Min(drawingBox.Width, drawingBox.Height) * 0.002);
        return Math.Abs(node.Box.MinX - drawingBox.MinX) <= tolerance ||
               Math.Abs(node.Box.MaxX - drawingBox.MaxX) <= tolerance ||
               Math.Abs(node.Box.MinY - drawingBox.MinY) <= tolerance ||
               Math.Abs(node.Box.MaxY - drawingBox.MaxY) <= tolerance;
    }

    static bool IsPreferredMainViewSeed(PrimitiveNode node)
    {
        var layer = ReadDictString(node.Data, "layer");
        var linetype = ReadDictString(node.Data, "linetype");
        var lineweight = ReadDictInt(node.Data, "lineweight");
        if (layer.Contains("粗实线", StringComparison.OrdinalIgnoreCase))
            return true;
        return string.Equals(linetype, "Continuous", StringComparison.OrdinalIgnoreCase) &&
               lineweight != null &&
               lineweight > 0;
    }

    static BoundingBox BoundingBoxForViewBbox(
        IEnumerable<PrimitiveNode> nodes,
        out int excludedCenterlineNodeCount,
        out int excludedSectionCutSymbolNodeCount)
    {
        var nodeList = nodes.ToList();
        var sectionCutSymbolNodes = FindSectionCutSymbolNodes(nodeList);
        var bboxNodes = nodeList
            .Where(node => !IsCenterlineNode(node) && !sectionCutSymbolNodes.Contains(node))
            .ToList();
        excludedCenterlineNodeCount = nodeList.Count(IsCenterlineNode);
        excludedSectionCutSymbolNodeCount = sectionCutSymbolNodes.Count;

        if (bboxNodes.Count == 0)
        {
            excludedCenterlineNodeCount = 0;
            excludedSectionCutSymbolNodeCount = 0;
            bboxNodes = nodeList;
        }

        return BoundingBox.Union(bboxNodes.Select(node => node.Box));
    }

    static HashSet<PrimitiveNode> FindSectionCutSymbolNodes(List<PrimitiveNode> nodes)
    {
        var result = new HashSet<PrimitiveNode>();
        var lineNodes = nodes
            .Where(node => string.Equals(node.Kind, "line", StringComparison.OrdinalIgnoreCase))
            .ToList();
        var phantomLines = lineNodes
            .Where(node => string.Equals(ReadDictString(node.Data, "linetype"), "PHANTOM", StringComparison.OrdinalIgnoreCase))
            .Where(node => Math.Max(node.Box.Width, node.Box.Height) >= 20.0)
            .ToList();

        foreach (var phantom in phantomLines)
        {
            var isVertical = phantom.Box.Width <= MainViewConnectTolerance && phantom.Box.Height > MainViewConnectTolerance;
            var isHorizontal = phantom.Box.Height <= MainViewConnectTolerance && phantom.Box.Width > MainViewConnectTolerance;
            if (!isVertical && !isHorizontal)
                continue;

            var symbolLeaders = lineNodes
                .Where(node => !ReferenceEquals(node, phantom))
                .Where(node => IsSectionCutLeaderForPhantom(node, phantom, isVertical))
                .ToList();
            if (symbolLeaders.Count < 2)
                continue;

            result.Add(phantom);
            foreach (var leader in symbolLeaders)
                result.Add(leader);
        }

        return result;
    }

    static bool IsSectionCutLeaderForPhantom(PrimitiveNode node, PrimitiveNode phantom, bool phantomIsVertical)
    {
        var tolerance = Math.Max(3.0, MainViewConnectTolerance);
        var length = Math.Max(node.Box.Width, node.Box.Height);
        if (length < 2.0 || length > 25.0)
            return false;

        if (phantomIsVertical)
        {
            if (!IsHorizontalLineNode(node, MainViewConnectTolerance))
                return false;
            var nearPhantomX = Math.Abs(node.Box.MinX - phantom.Box.MinX) <= tolerance ||
                               Math.Abs(node.Box.MaxX - phantom.Box.MinX) <= tolerance ||
                               (node.Box.MinX <= phantom.Box.MinX && node.Box.MaxX >= phantom.Box.MinX);
            var nearPhantomEndY = Math.Abs(node.Box.CenterY - phantom.Box.MinY) <= tolerance ||
                                  Math.Abs(node.Box.CenterY - phantom.Box.MaxY) <= tolerance;
            return nearPhantomX && nearPhantomEndY;
        }

        if (!IsVerticalLineNode(node, MainViewConnectTolerance))
            return false;
        var nearPhantomY = Math.Abs(node.Box.MinY - phantom.Box.MinY) <= tolerance ||
                           Math.Abs(node.Box.MaxY - phantom.Box.MinY) <= tolerance ||
                           (node.Box.MinY <= phantom.Box.MinY && node.Box.MaxY >= phantom.Box.MinY);
        var nearPhantomEndX = Math.Abs(node.Box.CenterX - phantom.Box.MinX) <= tolerance ||
                              Math.Abs(node.Box.CenterX - phantom.Box.MaxX) <= tolerance;
        return nearPhantomY && nearPhantomEndX;
    }

    static bool IsCenterlineNode(PrimitiveNode node)
    {
        return ContainsCenterlineMarker(ReadDictString(node.Data, "layer")) ||
               ContainsCenterlineMarker(ReadDictString(node.Data, "linetype")) ||
               ContainsCenterlineMarker(ReadDictString(node.Data, "style_key"));
    }

    static bool ContainsCenterlineMarker(string value)
    {
        return value.Contains("中心", StringComparison.OrdinalIgnoreCase) ||
               value.Contains("CENTER", StringComparison.OrdinalIgnoreCase) ||
               value.Contains("CENTRE", StringComparison.OrdinalIgnoreCase);
    }

    static bool IsVerticalLineNode(PrimitiveNode node, double tolerance) =>
        string.Equals(node.Kind, "line", StringComparison.OrdinalIgnoreCase) &&
        node.Box.Width <= tolerance &&
        node.Box.Height > tolerance;

    static bool IsHorizontalLineNode(PrimitiveNode node, double tolerance) =>
        string.Equals(node.Kind, "line", StringComparison.OrdinalIgnoreCase) &&
        node.Box.Height <= tolerance &&
        node.Box.Width > tolerance;

    static bool IsFrameOrBorderNode(PrimitiveNode node, BoundingBox drawingBox)
    {
        var layer = ReadDictString(node.Data, "layer");
        var linetype = ReadDictString(node.Data, "linetype");

        var width = drawingBox.Width;
        var height = drawingBox.Height;
        var nodeWidth = node.Box.Width;
        var nodeHeight = node.Box.Height;
        var edgeTolerance = Math.Max(MainViewConnectTolerance, Math.Min(width, height) * 0.08);
        var nearOuterEdge =
            Math.Abs(node.Box.MinX - drawingBox.MinX) <= MainViewConnectTolerance ||
            Math.Abs(node.Box.MaxX - drawingBox.MaxX) <= MainViewConnectTolerance ||
            Math.Abs(node.Box.MinY - drawingBox.MinY) <= MainViewConnectTolerance ||
            Math.Abs(node.Box.MaxY - drawingBox.MaxY) <= MainViewConnectTolerance;
        var nearFrameMargin =
            Math.Abs(node.Box.MinX - drawingBox.MinX) <= edgeTolerance ||
            Math.Abs(node.Box.MaxX - drawingBox.MaxX) <= edgeTolerance ||
            Math.Abs(node.Box.MinY - drawingBox.MinY) <= edgeTolerance ||
            Math.Abs(node.Box.MaxY - drawingBox.MaxY) <= edgeTolerance;

        var longHorizontal = nodeHeight <= MainViewConnectTolerance && nodeWidth >= width * 0.5;
        var longVertical = nodeWidth <= MainViewConnectTolerance && nodeHeight >= height * 0.5;
        var frameLayerStyle =
            string.Equals(layer, "0", StringComparison.OrdinalIgnoreCase) ||
            string.Equals(linetype, "ByLayer", StringComparison.OrdinalIgnoreCase);
        return (nearOuterEdge || nearFrameMargin) && (longHorizontal || longVertical) && frameLayerStyle;
    }

    static List<PrimitiveNode> BuildRawGeometryNodes(List<Dictionary<string, object?>> geometry)
    {
        var result = new List<PrimitiveNode>();
        foreach (var entity in geometry)
        {
            var node = PrimitiveNode.FromRawGeometry(entity);
            if (node != null)
                result.Add(node);
        }
        return result;
    }

    static HashSet<PrimitiveNode> ExpandConnectedNodes(PrimitiveNode seed, List<PrimitiveNode> lineNodes)
    {
        var selected = new HashSet<PrimitiveNode> { seed };
        var queue = new Queue<PrimitiveNode>();
        queue.Enqueue(seed);

        while (queue.Count > 0)
        {
            var current = queue.Dequeue();
            foreach (var candidate in lineNodes)
            {
                if (selected.Contains(candidate))
                    continue;
                if (!AreConnected(current, candidate))
                    continue;

                selected.Add(candidate);
                queue.Enqueue(candidate);
            }
        }

        return selected;
    }

    static bool AreConnected(PrimitiveNode a, PrimitiveNode b)
    {
        if (string.Equals(a.Kind, "line", StringComparison.OrdinalIgnoreCase) &&
            string.Equals(b.Kind, "line", StringComparison.OrdinalIgnoreCase) &&
            a.AnchorPoints.Count >= 2 &&
            b.AnchorPoints.Count >= 2)
        {
            return SegmentDistance(a.AnchorPoints[0], a.AnchorPoints[1], b.AnchorPoints[0], b.AnchorPoints[1]) <= MainViewConnectTolerance;
        }

        foreach (var pa in a.AnchorPoints)
        {
            foreach (var pb in b.AnchorPoints)
            {
                if (Distance(pa, pb) <= MainViewConnectTolerance)
                    return true;
            }
        }

        return false;
    }

    static double SegmentDistance(List<double> a0, List<double> a1, List<double> b0, List<double> b1)
    {
        if (SegmentsIntersect(a0, a1, b0, b1))
            return 0.0;

        return new[]
        {
            PointSegmentDistance(a0, b0, b1),
            PointSegmentDistance(a1, b0, b1),
            PointSegmentDistance(b0, a0, a1),
            PointSegmentDistance(b1, a0, a1)
        }.Min();
    }

    static bool SegmentsIntersect(List<double> a0, List<double> a1, List<double> b0, List<double> b1)
    {
        if (a0.Count < 2 || a1.Count < 2 || b0.Count < 2 || b1.Count < 2)
            return false;

        var d1 = Cross(a0, a1, b0);
        var d2 = Cross(a0, a1, b1);
        var d3 = Cross(b0, b1, a0);
        var d4 = Cross(b0, b1, a1);

        if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
            ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0)))
            return true;

        return Math.Abs(d1) <= MainViewConnectTolerance && PointOnSegment(b0, a0, a1) ||
               Math.Abs(d2) <= MainViewConnectTolerance && PointOnSegment(b1, a0, a1) ||
               Math.Abs(d3) <= MainViewConnectTolerance && PointOnSegment(a0, b0, b1) ||
               Math.Abs(d4) <= MainViewConnectTolerance && PointOnSegment(a1, b0, b1);
    }

    static double Cross(List<double> a, List<double> b, List<double> p)
    {
        return (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
    }

    static bool PointOnSegment(List<double> p, List<double> a, List<double> b)
    {
        return p[0] >= Math.Min(a[0], b[0]) - MainViewConnectTolerance &&
               p[0] <= Math.Max(a[0], b[0]) + MainViewConnectTolerance &&
               p[1] >= Math.Min(a[1], b[1]) - MainViewConnectTolerance &&
               p[1] <= Math.Max(a[1], b[1]) + MainViewConnectTolerance;
    }

    static double PointSegmentDistance(List<double> p, List<double> a, List<double> b)
    {
        if (p.Count < 2 || a.Count < 2 || b.Count < 2)
            return double.MaxValue;

        var dx = b[0] - a[0];
        var dy = b[1] - a[1];
        var len2 = dx * dx + dy * dy;
        if (len2 <= 1e-12)
            return Distance(p, a);

        var t = ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / len2;
        t = Math.Max(0.0, Math.Min(1.0, t));
        var proj = new List<double> { a[0] + t * dx, a[1] + t * dy };
        return Distance(p, proj);
    }

    static double Distance(List<double> a, List<double> b)
    {
        if (a.Count < 2 || b.Count < 2)
            return double.MaxValue;
        var dx = a[0] - b[0];
        var dy = a[1] - b[1];
        return Math.Sqrt(dx * dx + dy * dy);
    }

    static List<string> ReadStringList(Dictionary<string, object?> item, string key)
    {
        if (!item.TryGetValue(key, out var value) || value == null)
            return new List<string>();
        if (value is IEnumerable<string> strings)
            return strings.Where(text => !string.IsNullOrWhiteSpace(text)).ToList();
        if (value is IEnumerable<object> objects)
            return objects.Select(obj => Convert.ToString(obj, CultureInfo.InvariantCulture) ?? "").Where(text => !string.IsNullOrWhiteSpace(text)).ToList();
        return new List<string>();
    }

    static Dictionary<string, object?> BuildViewSignature(
        List<Dictionary<string, object?>> normalizedGeometry,
        List<Dictionary<string, object?>> mergedLines,
        string scope)
    {
        var nonLine = normalizedGeometry
            .Where(item => !string.Equals(ReadDictString(item, "kind"), "line", StringComparison.OrdinalIgnoreCase))
            .ToList();
        var normalizedLines = normalizedGeometry
            .Where(item => string.Equals(ReadDictString(item, "kind"), "line", StringComparison.OrdinalIgnoreCase))
            .ToList();

        var lines = mergedLines;
        var circles = nonLine.Where(item => string.Equals(ReadDictString(item, "kind"), "circle", StringComparison.OrdinalIgnoreCase)).ToList();
        var arcs = nonLine.Where(item => string.Equals(ReadDictString(item, "kind"), "arc", StringComparison.OrdinalIgnoreCase)).ToList();
        var splines = nonLine.Where(item => string.Equals(ReadDictString(item, "kind"), "spline", StringComparison.OrdinalIgnoreCase)).ToList();

        var lineLengths = lines.Select(item => ReadDictDouble(item, "length")).Where(value => value != null).Select(value => value!.Value).ToList();
        var circleRadii = circles.Select(item => ReadDictDouble(item, "radius")).Where(value => value != null).Select(value => value!.Value).ToList();
        var arcRadii = arcs.Select(item => ReadDictDouble(item, "radius")).Where(value => value != null).Select(value => value!.Value).ToList();
        var arcSweepAngles = arcs.Select(ArcSweepDeg).Where(value => value != null).Select(value => value!.Value).ToList();
        var arcLengths = arcs
            .Select(item =>
            {
                var radius = ReadDictDouble(item, "radius");
                var sweep = ArcSweepDeg(item);
                return radius != null && sweep != null ? radius.Value * sweep.Value * Math.PI / 180.0 : (double?)null;
            })
            .Where(value => value != null)
            .Select(value => value!.Value)
            .ToList();

        return new Dictionary<string, object?>
        {
            ["scope"] = scope,
            ["coordinate_policy"] = "relative_origin_from_leftmost_vertical_line_top_point",
            ["primitive_counts"] = new Dictionary<string, object?>
            {
                ["line"] = lines.Count,
                ["circle"] = circles.Count,
                ["arc"] = arcs.Count,
                ["spline"] = splines.Count,
                ["total"] = lines.Count + circles.Count + arcs.Count + splines.Count
            },
            ["line_direction_counts"] = BuildLineDirectionCounts(lines),
            ["relative_line_coordinates"] = BuildRelativeLineCoordinates(normalizedLines),
            ["line_length_histogram"] = Histogram(lineLengths, 1.0),
            ["line_length_stats"] = Stats(lineLengths),
            ["circle_radius_histogram"] = Histogram(circleRadii, 0.5),
            ["circle_radius_stats"] = Stats(circleRadii),
            ["arc_radius_histogram"] = Histogram(arcRadii, 0.5),
            ["arc_radius_stats"] = Stats(arcRadii),
            ["arc_sweep_angle_histogram"] = Histogram(arcSweepAngles, 5.0),
            ["arc_length_histogram"] = Histogram(arcLengths, 1.0),
            ["matching_primary_fields"] = new[]
            {
                "kind",
                "length",
                "radius",
                "diameter",
                "arc_length",
                "sweep_angle",
                "direction_class",
                "angle_deg",
                "relative_start",
                "relative_end",
                "relative_midpoint"
            },
            ["matching_excluded_primary_fields"] = new[]
            {
                "start",
                "end",
                "center",
                "absolute_start",
                "absolute_end"
            }
        };
    }

    static Dictionary<string, object?> BuildRelativeLineCoordinates(List<Dictionary<string, object?>> lines)
    {
        var parsedLines = lines
            .Select(LinePrimitive.FromDictionary)
            .Where(item => item != null)
            .Cast<LinePrimitive>()
            .ToList();
        var verticalLines = parsedLines
            .Where(line => string.Equals(DirectionClass(line.AngleDeg), "vertical", StringComparison.OrdinalIgnoreCase))
            .ToList();

        if (verticalLines.Count == 0)
        {
            return new Dictionary<string, object?>
            {
                ["ok"] = false,
                ["reason"] = "no_vertical_line_available",
                ["origin_rule"] = "leftmost_vertical_line_top_point"
            };
        }

        var originLine = verticalLines
            .OrderBy(line => Math.Min(line.StartX, line.EndX))
            .ThenByDescending(line => Math.Max(line.StartY, line.EndY))
            .ThenByDescending(line => line.Length)
            .First();

        var originX = originLine.StartY >= originLine.EndY ? originLine.StartX : originLine.EndX;
        var originY = Math.Max(originLine.StartY, originLine.EndY);
        var relativeLines = parsedLines
            .Select((line, index) => BuildRelativeLineRecord(line, index + 1, originX, originY))
            .OrderBy(item => Convert.ToDouble(item["relative_mid_x"], CultureInfo.InvariantCulture))
            .ThenByDescending(item => Convert.ToDouble(item["relative_mid_y"], CultureInfo.InvariantCulture))
            .ThenBy(item => Convert.ToDouble(item["length"], CultureInfo.InvariantCulture))
            .ToList();

        return new Dictionary<string, object?>
        {
            ["ok"] = true,
            ["origin_rule"] = "leftmost_vertical_line_top_point",
            ["origin"] = new Dictionary<string, object?>
            {
                ["x"] = originX,
                ["y"] = originY,
                ["source_handles"] = originLine.SourceHandles,
                ["line"] = new Dictionary<string, object?>
                {
                    ["start"] = new[] { originLine.StartX, originLine.StartY },
                    ["end"] = new[] { originLine.EndX, originLine.EndY },
                    ["length"] = originLine.Length,
                    ["angle_deg"] = originLine.AngleDeg
                }
            },
            ["line_count"] = relativeLines.Count,
            ["bucket_size"] = 1.0,
            ["relative_midpoints"] = relativeLines.Select(item => new Dictionary<string, object?>
            {
                ["index"] = item["index"],
                ["source_handles"] = item["source_handles"],
                ["x"] = item["relative_mid_x"],
                ["y"] = item["relative_mid_y"],
                ["x_bucket"] = item["relative_mid_x_bucket"],
                ["y_bucket"] = item["relative_mid_y_bucket"],
                ["midpoint_key"] = item["midpoint_key"]
            }).ToList(),
            ["relative_midpoint_histogram"] = Histogram(relativeLines
                .Select(item => Convert.ToString(item["midpoint_key"], CultureInfo.InvariantCulture) ?? "")
                .Where(key => !string.IsNullOrWhiteSpace(key))
                .ToList()),
            ["lines"] = relativeLines,
            ["feature_histogram"] = Histogram(relativeLines
                .Select(item => Convert.ToString(item["feature_key"], CultureInfo.InvariantCulture) ?? "")
                .Where(key => !string.IsNullOrWhiteSpace(key))
                .ToList())
        };
    }

    static Dictionary<string, object?> BuildRelativeLineRecord(LinePrimitive line, int index, double originX, double originY)
    {
        var rsx = line.StartX - originX;
        var rsy = line.StartY - originY;
        var rex = line.EndX - originX;
        var rey = line.EndY - originY;
        var midX = (rsx + rex) / 2.0;
        var midY = (rsy + rey) / 2.0;
        var direction = DirectionClass(line.AngleDeg);
        var lengthBucket = Bucket(line.Length, 1.0);
        var midXBucket = Bucket(midX, 1.0);
        var midYBucket = Bucket(midY, 1.0);
        var midpointKey = $"X{midXBucket:0.###}|Y{midYBucket:0.###}";

        return new Dictionary<string, object?>
        {
            ["index"] = index,
            ["source_handles"] = line.SourceHandles,
            ["direction_class"] = direction,
            ["angle_deg"] = line.AngleDeg,
            ["length"] = line.Length,
            ["length_bucket"] = lengthBucket,
            ["relative_start"] = new[] { rsx, rsy },
            ["relative_end"] = new[] { rex, rey },
            ["relative_midpoint"] = new[] { midX, midY },
            ["relative_mid_x"] = midX,
            ["relative_mid_y"] = midY,
            ["relative_mid_x_bucket"] = midXBucket,
            ["relative_mid_y_bucket"] = midYBucket,
            ["midpoint_key"] = midpointKey,
            ["feature_key"] = $"{direction}|L{lengthBucket:0.###}|X{midXBucket:0.###}|Y{midYBucket:0.###}"
        };
    }

    static double Bucket(double value, double bucketSize)
    {
        return bucketSize <= 0 ? value : Math.Round(value / bucketSize) * bucketSize;
    }

    static Dictionary<string, int> Histogram(List<string> values)
    {
        var result = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
        foreach (var value in values)
            result[value] = result.TryGetValue(value, out var count) ? count + 1 : 1;
        return result.OrderBy(item => item.Key, StringComparer.OrdinalIgnoreCase)
            .ToDictionary(item => item.Key, item => item.Value, StringComparer.OrdinalIgnoreCase);
    }

    static Dictionary<string, int> BuildLineDirectionCounts(List<Dictionary<string, object?>> lines)
    {
        var result = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
        {
            ["horizontal"] = 0,
            ["vertical"] = 0,
            ["diagonal"] = 0,
            ["unknown"] = 0
        };

        foreach (var line in lines)
        {
            var angle = ReadDictDouble(line, "angle_deg");
            var key = angle == null ? "unknown" : DirectionClass(angle.Value);
            result[key] = result.TryGetValue(key, out var count) ? count + 1 : 1;
        }

        return result;
    }

    static string DirectionClass(double angleDeg)
    {
        var normalized = NormalizeAngleDeg(angleDeg);
        if (AngleDistance(normalized, 0) <= 5.0 || AngleDistance(normalized, 180) <= 5.0)
            return "horizontal";
        if (AngleDistance(normalized, 90) <= 5.0)
            return "vertical";
        return "diagonal";
    }

    static double AngleDistance(double a, double b)
    {
        var delta = Math.Abs(a - b) % 180.0;
        return Math.Min(delta, 180.0 - delta);
    }

    static Dictionary<string, int> Histogram(List<double> values, double bucketSize)
    {
        var result = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
        if (bucketSize <= 0)
            return result;

        foreach (var value in values.Where(value => !double.IsNaN(value) && !double.IsInfinity(value)))
        {
            var bucket = Math.Round(value / bucketSize) * bucketSize;
            var key = bucket.ToString("0.###", CultureInfo.InvariantCulture);
            result[key] = result.TryGetValue(key, out var count) ? count + 1 : 1;
        }

        return result.OrderBy(item => double.Parse(item.Key, CultureInfo.InvariantCulture)).ToDictionary(item => item.Key, item => item.Value, StringComparer.OrdinalIgnoreCase);
    }

    static Dictionary<string, object?> Stats(List<double> values)
    {
        if (values.Count == 0)
        {
            return new Dictionary<string, object?>
            {
                ["count"] = 0
            };
        }

        return new Dictionary<string, object?>
        {
            ["count"] = values.Count,
            ["min"] = values.Min(),
            ["max"] = values.Max(),
            ["sum"] = values.Sum(),
            ["avg"] = values.Average()
        };
    }

    static double? ArcSweepDeg(Dictionary<string, object?> item)
    {
        var start = ReadDictDouble(item, "start_angle");
        var end = ReadDictDouble(item, "end_angle");
        if (start == null || end == null)
            return null;

        var sweep = (end.Value - start.Value) * 180.0 / Math.PI;
        while (sweep < 0)
            sweep += 360.0;
        while (sweep >= 360.0)
            sweep -= 360.0;
        return sweep;
    }

    static Manifest WriteFiles(string outputDir, string drawingPath, string progId, bool started, DocumentExtraction extraction)
    {
        var files = new List<CategoryFile>
        {
            WriteCategory(outputDir, "entities-common", "entities-common.json", extraction.EntitiesCommon),
            WriteCategory(outputDir, "geometry", "geometry.json", extraction.Geometry),
            WriteCategory(outputDir, "normalized-geometry", "normalized-geometry.json", extraction.NormalizedGeometry),
            WriteCategory(outputDir, "merged-lines", "merged-lines.json", extraction.MergedLines),
            WriteCategory(outputDir, "geometry-overlaps", "geometry-overlaps.json", extraction.GeometryOverlaps),
            WriteCategory(outputDir, "normalization-summary", "normalization-summary.json", extraction.NormalizationSummary),
            WriteCategory(outputDir, "main-view-geometry", "main-view-geometry.json", extraction.MainViewGeometry),
            WriteCategory(outputDir, "main-view-normalized-geometry", "main-view-normalized-geometry.json", extraction.MainViewNormalizedGeometry),
            WriteCategory(outputDir, "main-view-merged-lines", "main-view-merged-lines.json", extraction.MainViewMergedLines),
            WriteCategory(outputDir, "main-view-overlaps", "main-view-overlaps.json", extraction.MainViewOverlaps),
            WriteCategory(outputDir, "main-view-signature", "main-view-signature.json", extraction.MainViewSignature),
            WriteCategory(outputDir, "bottom-view-signature", "bottom-view-signature.json", extraction.BottomViewSignature),
            WriteCategory(outputDir, "section-cut-symbols", "section-cut-symbols.json", extraction.SectionCutSymbols),
            WriteCategory(outputDir, "main-view-selection", "main-view-selection.json", extraction.MainViewSelection),
            WriteCategory(outputDir, "view-regions", "view-regions.json", extraction.ViewRegions),
            WriteCategory(outputDir, "annotations", "annotations.json", extraction.Annotations),
            WriteCategory(outputDir, "dimensions", "dimensions.json", extraction.Dimensions),
            WriteCategory(outputDir, "dimensional-tolerances", "dimensional-tolerances.json", extraction.DimensionalTolerances),
            WriteCategory(outputDir, "roughness", "roughness.json", extraction.Roughness),
            WriteCategory(outputDir, "geometric-tolerances", "geometric-tolerances.json", extraction.GeometricTolerances),
            WriteCategory(outputDir, "datums", "datums.json", extraction.Datums),
            WriteCategory(outputDir, "unknown-entities", "unknown-entities.json", extraction.UnknownEntities)
        };

        var manifest = new Manifest
        {
            Ok = true,
            Schema = "agent4.dwg-draft.extraction.v1",
            Source = "AutoCAD COM external extractor",
            ProgId = progId,
            StartedAutoCad = started,
            DrawingPath = drawingPath,
            ExportedAtUtc = DateTime.UtcNow,
            ModelSpaceCount = extraction.ModelSpaceCount,
            PaperSpaceCount = extraction.PaperSpaceCount,
            EntityCount = extraction.EntitiesCommon.Count,
            TypeCounts = extraction.TypeCounts,
            CategoryFiles = files
        };
        WriteJson(Path.Combine(outputDir, "manifest.json"), manifest);
        return manifest;
    }

    static DocumentExtraction LoadExistingExtraction(string inputDir)
    {
        if (string.IsNullOrWhiteSpace(inputDir))
            throw new ArgumentException("--reuse-extraction-dir is required for existing extraction reanalysis.");
        inputDir = Path.GetFullPath(inputDir.Trim().Trim('"'));
        if (!Directory.Exists(inputDir))
            throw new DirectoryNotFoundException($"Existing extraction directory not found: {inputDir}");

        var extraction = new DocumentExtraction
        {
            EntitiesCommon = ReadJsonListFile(Path.Combine(inputDir, "entities-common.json")),
            Geometry = ReadJsonListFile(Path.Combine(inputDir, "geometry.json")),
            Annotations = ReadJsonListFile(Path.Combine(inputDir, "annotations.json")),
            Dimensions = ReadJsonListFile(Path.Combine(inputDir, "dimensions.json")),
            DimensionalTolerances = ReadJsonListFile(Path.Combine(inputDir, "dimensional-tolerances.json")),
            Roughness = ReadJsonListFile(Path.Combine(inputDir, "roughness.json")),
            GeometricTolerances = ReadJsonListFile(Path.Combine(inputDir, "geometric-tolerances.json")),
            Datums = ReadJsonListFile(Path.Combine(inputDir, "datums.json")),
            UnknownEntities = ReadJsonListFile(Path.Combine(inputDir, "unknown-entities.json"))
        };

        extraction.TypeCounts = ReadTypeCounts(Path.Combine(inputDir, "manifest.json"));
        extraction.ModelSpaceCount = extraction.EntitiesCommon.Count(item => string.Equals(ReadDictString(item, "space"), "ModelSpace", StringComparison.OrdinalIgnoreCase));
        extraction.PaperSpaceCount = extraction.EntitiesCommon.Count(item => string.Equals(ReadDictString(item, "space"), "PaperSpace", StringComparison.OrdinalIgnoreCase));
        return extraction;
    }

    static List<Dictionary<string, object?>> ReadJsonListFile(string path)
    {
        if (!File.Exists(path))
            return new List<Dictionary<string, object?>>();
        using var doc = JsonDocument.Parse(File.ReadAllText(path));
        if (doc.RootElement.ValueKind != JsonValueKind.Array)
            return new List<Dictionary<string, object?>>();

        var result = new List<Dictionary<string, object?>>();
        foreach (var item in doc.RootElement.EnumerateArray())
        {
            if (ConvertJsonValue(item) is Dictionary<string, object?> dict)
                result.Add(dict);
        }
        return result;
    }

    static Dictionary<string, int> ReadTypeCounts(string manifestPath)
    {
        var result = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
        if (!File.Exists(manifestPath))
            return result;
        using var doc = JsonDocument.Parse(File.ReadAllText(manifestPath));
        if (!doc.RootElement.TryGetProperty("type_counts", out var typeCounts) ||
            typeCounts.ValueKind != JsonValueKind.Object)
        {
            return result;
        }

        foreach (var item in typeCounts.EnumerateObject())
        {
            if (item.Value.ValueKind == JsonValueKind.Number && item.Value.TryGetInt32(out var count))
                result[item.Name] = count;
        }
        return result;
    }

    static object? ConvertJsonValue(JsonElement element)
    {
        return element.ValueKind switch
        {
            JsonValueKind.Object => element.EnumerateObject()
                .ToDictionary(item => item.Name, item => ConvertJsonValue(item.Value), StringComparer.OrdinalIgnoreCase),
            JsonValueKind.Array => ConvertJsonArray(element),
            JsonValueKind.String => element.GetString(),
            JsonValueKind.Number => element.TryGetInt32(out var intValue) ? intValue : element.GetDouble(),
            JsonValueKind.True => true,
            JsonValueKind.False => false,
            _ => null
        };
    }

    static object ConvertJsonArray(JsonElement element)
    {
        var values = element.EnumerateArray().Select(ConvertJsonValue).ToList();
        if (values.All(value => value is int or double or long or float or decimal))
            return values.Select(value => Convert.ToDouble(value, CultureInfo.InvariantCulture)).ToList();
        return values;
    }

    static CategoryFile WriteCategory(string outputDir, string category, string file, object data)
    {
        var path = Path.Combine(outputDir, file);
        WriteJson(path, data);
        return new CategoryFile
        {
            Category = category,
            File = file,
            FullPath = path,
            ItemCount = data is System.Collections.ICollection collection ? collection.Count : 0,
            Exists = true
        };
    }

    static void WriteJson(string path, object data)
    {
        File.WriteAllText(path, JsonSerializer.Serialize(data, JsonDefaults.Options));
    }

    static object ConnectOrCreate(string progId, bool visible, bool allowStartAutoCad, out bool started)
    {
        var app = TryGetActiveComObject(progId);
        started = false;
        if (app != null)
        {
            TrySetProperty(app, "Visible", visible);
            return app;
        }

        if (!allowStartAutoCad)
        {
            throw new InvalidOperationException(
                $"No active AutoCAD COM object: {progId}. Open AutoCAD manually, or pass --allow-start-autocad true.");
        }

        if (app == null)
        {
            LogProgress("start_autocad_process");
            try
            {
                StartAutoCadFromRegistry(progId);
                app = WaitForActiveComObject(progId, TimeSpan.FromSeconds(90));
                if (app != null)
                    started = true;
            }
            catch (Exception ex)
            {
                LogProgress($"start_autocad_process_failed {ex.GetType().Name}: {ex.Message}");
            }
        }

        if (app == null)
        {
            try
            {
                var type = Type.GetTypeFromProgID(progId, throwOnError: true)
                    ?? throw new InvalidOperationException($"找不到 AutoCAD COM ProgID：{progId}");
                app = Activator.CreateInstance(type)
                    ?? throw new InvalidOperationException($"创建 AutoCAD COM 对象失败：{progId}");
                started = true;
            }
            catch
            {
                StartAutoCadFromRegistry(progId);
                var deadline = DateTime.UtcNow.AddSeconds(90);
                while (DateTime.UtcNow < deadline)
                {
                    Thread.Sleep(1500);
                    app = TryGetActiveComObject(progId);
                    if (app != null)
                    {
                        started = true;
                        break;
                    }
                }
            }
        }

        if (app == null)
            throw new InvalidOperationException($"AutoCAD 启动或 COM 连接失败：{progId}");

        TrySetProperty(app, "Visible", visible);
        return app;
    }

    static object? WaitForActiveComObject(string progId, TimeSpan timeout)
    {
        LogProgress("wait_autocad_com_start");
        var deadline = DateTime.UtcNow.Add(timeout);
        while (DateTime.UtcNow < deadline)
        {
            Thread.Sleep(1500);
            var app = TryGetActiveComObject(progId);
            if (app != null)
            {
                LogProgress("wait_autocad_com_ok");
                return app;
            }
        }

        LogProgress("wait_autocad_com_timeout");
        return null;
    }

    static void StartAutoCadFromRegistry(string progId)
    {
        var clsid = ReadDefaultValue($@"{progId}\CLSID");
        var command = string.IsNullOrWhiteSpace(clsid) ? "" : ReadDefaultValue($@"CLSID\{clsid}\LocalServer32");
        var exe = ExtractExePath(command);
        if (string.IsNullOrWhiteSpace(exe) || !File.Exists(exe))
            exe = Environment.GetEnvironmentVariable("AUTOCAD_EXE") ?? "";
        if (!File.Exists(exe))
            throw new FileNotFoundException($"找不到 acad.exe。ProgID={progId}, LocalServer32={command}");

        Process.Start(new ProcessStartInfo
        {
            FileName = exe,
            Arguments = "/Automation",
            UseShellExecute = true,
            WorkingDirectory = Path.GetDirectoryName(exe) ?? Environment.CurrentDirectory
        });
    }

    static string ReadDefaultValue(string subKey)
    {
        using var key = Registry.ClassesRoot.OpenSubKey(subKey);
        return key?.GetValue("")?.ToString() ?? "";
    }

    static string ExtractExePath(string command)
    {
        if (string.IsNullOrWhiteSpace(command))
            return "";

        var trimmed = command.Trim();
        if (trimmed.StartsWith('"'))
        {
            var end = trimmed.IndexOf('"', 1);
            return end > 1 ? trimmed[1..end] : "";
        }

        var exeIndex = trimmed.IndexOf(".exe", StringComparison.OrdinalIgnoreCase);
        return exeIndex >= 0 ? trimmed[..(exeIndex + 4)] : trimmed;
    }

    static object OpenDocument(object app, string drawingPath)
    {
        var documents = GetProperty(app, "Documents") ?? throw new InvalidOperationException("AutoCAD.Application.Documents 不可用。");
        try
        {
            return TryInvoke(documents, "Open", [drawingPath, true])
                ?? TryInvoke(documents, "Open", [drawingPath])
                ?? throw new InvalidOperationException($"AutoCAD 打开 DWG 失败：{drawingPath}");
        }
        finally
        {
            ReleaseCom(documents);
        }
    }

    static Dictionary<string, SplineEndpoint> ReadSplineEndpointsViaAutoLisp(object app, object doc, string outputDir)
    {
        var result = new Dictionary<string, SplineEndpoint>(StringComparer.OrdinalIgnoreCase);
        try
        {
            Directory.CreateDirectory(outputDir);
            var lispPath = Path.Combine(outputDir, "agent4-read-spline-endpoints.lsp");
            var outputPath = Path.Combine(outputDir, "spline-endpoints.tsv");
            if (File.Exists(outputPath))
                File.Delete(outputPath);

            File.WriteAllText(lispPath, BuildSplineEndpointLisp(outputPath));
            TryInvoke(doc, "SendCommand", [$"(load \"{LispPath(lispPath)}\")\n"]);

            var deadline = DateTime.UtcNow.AddSeconds(30);
            while (DateTime.UtcNow < deadline)
            {
                WaitForQuiescent(app, TimeSpan.FromSeconds(2));
                if (File.Exists(outputPath))
                    break;
                Thread.Sleep(250);
            }

            if (!File.Exists(outputPath))
                return result;

            foreach (var line in File.ReadAllLines(outputPath))
            {
                var parts = line.Split('\t');
                if (parts.Length < 5)
                    continue;
                if (!TryParseInvariant(parts[1], out var sx) ||
                    !TryParseInvariant(parts[2], out var sy) ||
                    !TryParseInvariant(parts[3], out var ex) ||
                    !TryParseInvariant(parts[4], out var ey))
                {
                    continue;
                }

                result[parts[0]] = new SplineEndpoint(
                    new List<double> { sx, sy },
                    new List<double> { ex, ey },
                    "autolisp_vlax_curve_param_point");
            }
        }
        catch
        {
            // Keep extraction usable; direct COM/control-point fallback still runs.
        }

        return result;
    }

    static string BuildSplineEndpointLisp(string outputPath)
    {
        var path = LispPath(outputPath);
        return string.Join("\n", new[]
        {
            "(vl-load-com)",
            "(defun agent4-format-point (p)",
            "  (if (and p (listp p) (numberp (car p)) (numberp (cadr p)))",
            "    (strcat (rtos (car p) 2 16) \"\\t\" (rtos (cadr p) 2 16))",
            "    \"\\t\"",
            "  )",
            ")",
            "(defun agent4-start-point (obj / param point)",
            "  (setq param (vl-catch-all-apply 'vlax-curve-getStartParam (list obj)))",
            "  (if (not (vl-catch-all-error-p param))",
            "    (setq point (vl-catch-all-apply 'vlax-curve-getPointAtParam (list obj param)))",
            "  )",
            "  (if (or (null point) (vl-catch-all-error-p point))",
            "    (setq point (vl-catch-all-apply 'vlax-curve-getStartPoint (list obj)))",
            "  )",
            "  (if (vl-catch-all-error-p point) nil point)",
            ")",
            "(defun agent4-end-point (obj / param point)",
            "  (setq param (vl-catch-all-apply 'vlax-curve-getEndParam (list obj)))",
            "  (if (not (vl-catch-all-error-p param))",
            "    (setq point (vl-catch-all-apply 'vlax-curve-getPointAtParam (list obj param)))",
            "  )",
            "  (if (or (null point) (vl-catch-all-error-p point))",
            "    (setq point (vl-catch-all-apply 'vlax-curve-getEndPoint (list obj)))",
            "  )",
            "  (if (vl-catch-all-error-p point) nil point)",
            ")",
            "(defun agent4-write-spline-endpoints (/ f ss i ent obj h sp ep)",
            $"  (setq f (open \"{path}\" \"w\"))",
            "  (setq ss (ssget \"_X\" '((0 . \"SPLINE\"))))",
            "  (if ss",
            "    (progn",
            "      (setq i 0)",
            "      (while (< i (sslength ss))",
            "        (setq ent (ssname ss i))",
            "        (setq obj (vlax-ename->vla-object ent))",
            "        (setq h (vla-get-Handle obj))",
            "        (setq sp (agent4-start-point obj))",
            "        (setq ep (agent4-end-point obj))",
            "        (write-line (strcat h \"\\t\" (agent4-format-point sp) \"\\t\" (agent4-format-point ep)) f)",
            "        (setq i (1+ i))",
            "      )",
            "    )",
            "  )",
            "  (if f (close f))",
            "  (princ)",
            ")",
            "(agent4-write-spline-endpoints)"
        }) + "\n";
    }

    static Dictionary<string, DimensionDefinitionPoints> ReadDimensionDefinitionPointsViaAutoLisp(object app, object doc, string outputDir)
    {
        var result = new Dictionary<string, DimensionDefinitionPoints>(StringComparer.OrdinalIgnoreCase);
        try
        {
            Directory.CreateDirectory(outputDir);
            var lispPath = Path.Combine(outputDir, "agent4-read-dimension-definition-points.lsp");
            var outputPath = Path.Combine(outputDir, "dimension-definition-points.tsv");
            if (File.Exists(outputPath))
                File.Delete(outputPath);

            File.WriteAllText(lispPath, BuildDimensionDefinitionPointLisp(outputPath));
            TryInvoke(doc, "SendCommand", [$"(load \"{LispPath(lispPath)}\")\n"]);

            var deadline = DateTime.UtcNow.AddSeconds(30);
            while (DateTime.UtcNow < deadline)
            {
                WaitForQuiescent(app, TimeSpan.FromSeconds(2));
                if (File.Exists(outputPath))
                    break;
                Thread.Sleep(250);
            }

            if (!File.Exists(outputPath))
                return result;

            foreach (var line in File.ReadAllLines(outputPath))
            {
                var parts = line.Split('\t');
                if (parts.Length < 19)
                    continue;

                result[parts[0]] = new DimensionDefinitionPoints(
                    ParsePointColumns(parts, 1),
                    ParsePointColumns(parts, 4),
                    ParsePointColumns(parts, 7),
                    ParsePointColumns(parts, 10),
                    ParsePointColumns(parts, 13),
                    ParsePointColumns(parts, 16));
            }
        }
        catch
        {
            // Keep DWG extraction usable; ActiveX point properties remain the fallback.
        }

        return result;
    }

    static string BuildDimensionDefinitionPointLisp(string outputPath)
    {
        var path = LispPath(outputPath);
        return string.Join("\n", new[]
        {
            "(vl-load-com)",
            "(defun agent4-format-dxf-point (data code / p)",
            "  (setq p (cdr (assoc code data)))",
            "  (if (and p (listp p) (numberp (car p)) (numberp (cadr p)))",
            "    (strcat (rtos (car p) 2 16) \"\\t\" (rtos (cadr p) 2 16) \"\\t\" (rtos (if (numberp (caddr p)) (caddr p) 0.0) 2 16))",
            "    \"\\t\\t\"",
            "  )",
            ")",
            "(defun agent4-write-dimension-definition-points (/ f ss i ent data obj h)",
            $"  (setq f (open \"{path}\" \"w\"))",
            "  (setq ss (ssget \"_X\" '((0 . \"*DIMENSION\"))))",
            "  (if ss",
            "    (progn",
            "      (setq i 0)",
            "      (while (< i (sslength ss))",
            "        (setq ent (ssname ss i))",
            "        (setq data (entget ent))",
            "        (setq obj (vlax-ename->vla-object ent))",
            "        (setq h (vla-get-Handle obj))",
            "        (write-line",
            "          (strcat",
            "            h \"\\t\"",
            "            (agent4-format-dxf-point data 13) \"\\t\"",
            "            (agent4-format-dxf-point data 14) \"\\t\"",
            "            (agent4-format-dxf-point data 10) \"\\t\"",
            "            (agent4-format-dxf-point data 11) \"\\t\"",
            "            (agent4-format-dxf-point data 15) \"\\t\"",
            "            (agent4-format-dxf-point data 16)",
            "          )",
            "          f",
            "        )",
            "        (setq i (1+ i))",
            "      )",
            "    )",
            "  )",
            "  (if f (close f))",
            "  (princ)",
            ")",
            "(agent4-write-dimension-definition-points)"
        }) + "\n";
    }

    static List<double>? ParsePointColumns(string[] parts, int start)
    {
        if (parts.Length <= start + 1)
            return null;
        if (!TryParseInvariant(parts[start], out var x) ||
            !TryParseInvariant(parts[start + 1], out var y))
        {
            return null;
        }

        var z = parts.Length > start + 2 && TryParseInvariant(parts[start + 2], out var parsedZ)
            ? parsedZ
            : 0.0;
        return new List<double> { x, y, z };
    }

    static Dictionary<string, List<Dictionary<string, object?>>> ReadBlockExplosionsViaAutoLisp(
        object app,
        object doc,
        string outputDir)
    {
        var result = new Dictionary<string, List<Dictionary<string, object?>>>(StringComparer.OrdinalIgnoreCase);
        try
        {
            Directory.CreateDirectory(outputDir);
            var lispPath = Path.Combine(outputDir, "agent4-read-block-explosions.lsp");
            var outputPath = Path.Combine(outputDir, "block-explosions.tsv");
            if (File.Exists(outputPath))
                File.Delete(outputPath);

            File.WriteAllText(lispPath, BuildBlockExplosionLisp(outputPath));
            TryInvoke(doc, "SendCommand", [$"(load \"{LispPath(lispPath)}\")\n"]);

            var deadline = DateTime.UtcNow.AddSeconds(90);
            while (DateTime.UtcNow < deadline)
            {
                WaitForQuiescent(app, TimeSpan.FromSeconds(2));
                if (File.Exists(outputPath))
                    break;
                Thread.Sleep(250);
            }

            if (!File.Exists(outputPath))
                return result;

            foreach (var line in File.ReadAllLines(outputPath))
            {
                var parts = line.Split('\t');
                if (parts.Length < 16)
                    continue;

                var parent = parts[0];
                if (string.IsNullOrWhiteSpace(parent))
                    continue;

                var child = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
                {
                    ["index"] = ToInt(parts[1]),
                    ["object_name"] = parts[2],
                    ["text"] = parts[3]
                };
                AddParsedPoint(child, "start", parts[4], parts[5]);
                AddParsedPoint(child, "end", parts[6], parts[7]);
                AddParsedPoint(child, "insertion_point", parts[8], parts[9]);
                if (TryParseInvariant(parts[10], out var minX) &&
                    TryParseInvariant(parts[11], out var minY) &&
                    TryParseInvariant(parts[12], out var maxX) &&
                    TryParseInvariant(parts[13], out var maxY))
                {
                    child["bbox"] = new BoundingBox(minX, minY, maxX, maxY).ToDictionary();
                }
                child["layer"] = parts[14];
                child["source"] = parts[15];

                if (!result.TryGetValue(parent, out var children))
                {
                    children = new List<Dictionary<string, object?>>();
                    result[parent] = children;
                }
                children.Add(child);
            }
        }
        catch
        {
            // Keep base extraction usable if a block cannot be exploded.
        }

        return result;
    }

    static string BuildBlockExplosionLisp(string outputPath)
    {
        var path = LispPath(outputPath);
        return string.Join("\n", new[]
        {
            "(vl-load-com)",
            "(defun agent4-num (v) (if (numberp v) (rtos v 2 16) \"\"))",
            "(defun agent4-ptx (p) (if (and p (listp p) (numberp (car p))) (rtos (car p) 2 16) \"\"))",
            "(defun agent4-pty (p) (if (and p (listp p) (numberp (cadr p))) (rtos (cadr p) 2 16) \"\"))",
            "(defun agent4-clean (s) (if s (vl-string-translate \"\\t\\r\\n\" \"   \" s) \"\"))",
            "(defun agent4-bbox (obj / mn mx r)",
            "  (setq r (vl-catch-all-apply 'vla-GetBoundingBox (list obj 'mn 'mx)))",
            "  (if (vl-catch-all-error-p r)",
            "    (list \"\" \"\" \"\" \"\")",
            "    (progn",
            "      (setq mn (vlax-safearray->list mn))",
            "      (setq mx (vlax-safearray->list mx))",
            "      (list (agent4-ptx mn) (agent4-pty mn) (agent4-ptx mx) (agent4-pty mx))",
            "    )",
            "  )",
            ")",
            "(defun agent4-prop-point (obj prop / v)",
            "  (setq v (vl-catch-all-apply 'vlax-get-property (list obj prop)))",
            "  (if (or (null v) (vl-catch-all-error-p v)) nil (vlax-safearray->list (vlax-variant-value v)))",
            ")",
            "(defun agent4-prop-text (obj / s)",
            "  (setq s (vl-catch-all-apply 'vlax-get-property (list obj 'TextString)))",
            "  (if (vl-catch-all-error-p s) \"\" (agent4-clean s))",
            ")",
            "(defun agent4-write-child (f parent idx obj / typ txt sp ep ip bb layer)",
            "  (setq typ (vla-get-ObjectName obj))",
            "  (setq txt (agent4-prop-text obj))",
            "  (setq sp (agent4-prop-point obj 'StartPoint))",
            "  (setq ep (agent4-prop-point obj 'EndPoint))",
            "  (setq ip (agent4-prop-point obj 'InsertionPoint))",
            "  (setq bb (agent4-bbox obj))",
            "  (setq layer (vl-catch-all-apply 'vlax-get-property (list obj 'Layer)))",
            "  (if (vl-catch-all-error-p layer) (setq layer \"\"))",
            "  (write-line",
            "    (strcat parent \"\\t\" (itoa idx) \"\\t\" typ \"\\t\" txt \"\\t\"",
            "            (agent4-ptx sp) \"\\t\" (agent4-pty sp) \"\\t\"",
            "            (agent4-ptx ep) \"\\t\" (agent4-pty ep) \"\\t\"",
            "            (agent4-ptx ip) \"\\t\" (agent4-pty ip) \"\\t\"",
            "            (nth 0 bb) \"\\t\" (nth 1 bb) \"\\t\" (nth 2 bb) \"\\t\" (nth 3 bb) \"\\t\"",
            "            layer \"\\t\" \"vla-explode\")",
            "    f)",
            ")",
            "(defun agent4-read-block-explosions (/ f ss i ent obj parent arr children child idx)",
            $"  (setq f (open \"{path}\" \"w\"))",
            "  (setq ss (ssget \"_X\" '((0 . \"INSERT\"))))",
            "  (if ss",
            "    (progn",
            "      (setq i 0)",
            "      (while (< i (sslength ss))",
            "        (setq ent (ssname ss i))",
            "        (setq obj (vlax-ename->vla-object ent))",
            "        (setq parent (vla-get-Handle obj))",
            "        (setq arr (vl-catch-all-apply 'vla-Explode (list obj)))",
            "        (if (not (vl-catch-all-error-p arr))",
            "          (progn",
            "            (setq children (vlax-safearray->list (vlax-variant-value arr)))",
            "            (setq idx 0)",
            "            (foreach child children",
            "              (agent4-write-child f parent idx child)",
            "              (vl-catch-all-apply 'vla-Delete (list child))",
            "              (setq idx (1+ idx))",
            "            )",
            "          )",
            "        )",
            "        (setq i (1+ i))",
            "      )",
            "    )",
            "  )",
            "  (if f (close f))",
            "  (princ)",
            ")",
            "(agent4-read-block-explosions)"
        }) + "\n";
    }

    static void AddParsedPoint(Dictionary<string, object?> item, string key, string xText, string yText)
    {
        if (TryParseInvariant(xText, out var x) && TryParseInvariant(yText, out var y))
            item[key] = new List<double> { x, y };
    }

    static string LispPath(string path) => Path.GetFullPath(path).Replace("\\", "/").Replace("\"", "\\\"");

    static bool TryParseInvariant(string text, out double value) =>
        double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out value);

    static void WaitForQuiescent(object app, TimeSpan timeout)
    {
        var deadline = DateTime.UtcNow + timeout;
        while (DateTime.UtcNow < deadline)
        {
            var state = TryInvoke(app, "GetAcadState", []);
            if (state == null)
                return;
            try
            {
                if (ToBool(GetProperty(state, "IsQuiescent")) ?? true)
                    return;
            }
            finally
            {
                ReleaseCom(state);
            }
            Thread.Sleep(250);
        }
    }

    static DeleteResult SaveMainViewOnlyDwg(object doc, string savePath, HashSet<string> keepHandles)
    {
        Directory.CreateDirectory(Path.GetDirectoryName(savePath) ?? Environment.CurrentDirectory);
        var deleted = 0;
        deleted += DeleteEntitiesNotInHandles(doc, "ModelSpace", keepHandles);
        deleted += DeleteEntitiesNotInHandles(doc, "PaperSpace", keepHandles);
        TryInvoke(doc, "SaveAs", [savePath]);
        return new DeleteResult(deleted);
    }

    static int DeleteEntitiesNotInHandles(object doc, string spaceName, HashSet<string> keepHandles)
    {
        var deleted = 0;
        var space = GetProperty(doc, spaceName);
        if (space == null)
            return deleted;

        try
        {
            var count = ToInt(GetProperty(space, "Count")) ?? 0;
            for (var i = count - 1; i >= 0; i--)
            {
                var entity = GetCollectionItem(space, i);
                if (entity == null)
                    continue;
                try
                {
                    var handle = ReadString(entity, "Handle");
                    if (!keepHandles.Contains(handle))
                    {
                        TryInvoke(entity, "Delete", []);
                        deleted++;
                    }
                }
                finally
                {
                    ReleaseCom(entity);
                }
            }
        }
        finally
        {
            ReleaseCom(space);
        }

        return deleted;
    }

    static object? GetProperty(object obj, string name) => RetryCom(() =>
    {
        try
        {
            return obj.GetType().InvokeMember(name, BindingFlags.GetProperty, null, obj, null, CultureInfo.InvariantCulture);
        }
        catch (TargetInvocationException ex) when (IsRetryableCom(ex.InnerException))
        {
            throw;
        }
        catch (COMException ex) when (IsRetryableCom(ex))
        {
            throw;
        }
        catch
        {
            return null;
        }
    });

    static object? GetCollectionItem(object obj, int index) => RetryCom(() =>
    {
        foreach (var flags in new[] { BindingFlags.GetProperty, BindingFlags.InvokeMethod })
        {
            try
            {
                return obj.GetType().InvokeMember("Item", flags, null, obj, [index], CultureInfo.InvariantCulture);
            }
            catch (TargetInvocationException ex) when (IsRetryableCom(ex.InnerException))
            {
                throw;
            }
            catch (COMException ex) when (IsRetryableCom(ex))
            {
                throw;
            }
            catch
            {
                // Try next dispatch style.
            }
        }
        return null;
    });

    static object? TryInvoke(object obj, string name, object?[] args) => RetryCom(() =>
    {
        try
        {
            return obj.GetType().InvokeMember(name, BindingFlags.InvokeMethod, null, obj, args, CultureInfo.InvariantCulture);
        }
        catch (TargetInvocationException ex) when (IsRetryableCom(ex.InnerException))
        {
            throw;
        }
        catch (COMException ex) when (IsRetryableCom(ex))
        {
            throw;
        }
        catch
        {
            return null;
        }
    });

    static void TrySetProperty(object obj, string name, object? value)
    {
        RetryCom(() =>
        {
            try
            {
                obj.GetType().InvokeMember(name, BindingFlags.SetProperty, null, obj, [value], CultureInfo.InvariantCulture);
            }
            catch
            {
                // Optional.
            }
            return null;
        });
    }

    static object? RetryCom(Func<object?> action)
    {
        Exception? last = null;
        for (var i = 0; i < 180; i++)
        {
            try
            {
                return action();
            }
            catch (TargetInvocationException ex) when (IsRetryableCom(ex.InnerException))
            {
                last = ex.InnerException;
                Thread.Sleep(100);
            }
            catch (COMException ex) when (IsRetryableCom(ex))
            {
                last = ex;
                Thread.Sleep(100);
            }
        }
        if (last != null)
            throw last;
        return action();
    }

    static bool IsRetryableCom(Exception? ex)
    {
        return ex is COMException com && unchecked((uint)com.HResult) is 0x80010001 or 0x8001010A;
    }

    static bool IsGeometry(string objectName)
    {
        return objectName.Contains("Line", StringComparison.OrdinalIgnoreCase) ||
               objectName.Contains("Circle", StringComparison.OrdinalIgnoreCase) ||
               objectName.Contains("Arc", StringComparison.OrdinalIgnoreCase) ||
               objectName.Contains("Spline", StringComparison.OrdinalIgnoreCase) ||
               objectName.Contains("Hatch", StringComparison.OrdinalIgnoreCase) ||
               objectName.Contains("Wipeout", StringComparison.OrdinalIgnoreCase) ||
               objectName.Contains("RasterImage", StringComparison.OrdinalIgnoreCase);
    }

    static bool IsPolyline(string objectName)
    {
        return objectName.Contains("Polyline", StringComparison.OrdinalIgnoreCase);
    }

    static bool IsAnnotation(string objectName)
    {
        return objectName.Contains("Text", StringComparison.OrdinalIgnoreCase) ||
               objectName.Contains("BlockReference", StringComparison.OrdinalIgnoreCase);
    }

    static bool LooksLikeRoughness(string text, string blockName)
    {
        var merged = text + " " + blockName;
        return Regex.IsMatch(merged, @"\bR[az]\s*\d", RegexOptions.IgnoreCase) ||
               merged.Contains("粗糙", StringComparison.OrdinalIgnoreCase) ||
               merged.Contains("其余", StringComparison.OrdinalIgnoreCase) ||
               merged.Contains("rough", StringComparison.OrdinalIgnoreCase);
    }

    static bool LooksLikeGeometricTolerance(string text, string blockName)
    {
        var merged = text + " " + blockName;
        return merged.Contains("位置度", StringComparison.OrdinalIgnoreCase) ||
               merged.Contains("同轴度", StringComparison.OrdinalIgnoreCase) ||
               merged.Contains("平行度", StringComparison.OrdinalIgnoreCase) ||
               merged.Contains("垂直度", StringComparison.OrdinalIgnoreCase) ||
               merged.Contains("圆跳动", StringComparison.OrdinalIgnoreCase) ||
               merged.Contains("geotol", StringComparison.OrdinalIgnoreCase) ||
               Regex.IsMatch(merged, @"[⊥∥⌖◎⌯⌭]");
    }

    static bool LooksLikeDatum(string text, string blockName)
    {
        var merged = text + " " + blockName;
        return merged.Contains("基准", StringComparison.OrdinalIgnoreCase) ||
               merged.Contains("datum", StringComparison.OrdinalIgnoreCase);
    }

    static List<double> ExtractNumbers(string text)
    {
        return Regex.Matches(text, @"[+-]?\d+(?:\.\d+)?")
            .Select(match => double.TryParse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var value) ? value : (double?)null)
            .Where(value => value != null)
            .Select(value => value!.Value)
            .ToList();
    }

    static string CleanAcadText(string text)
    {
        if (string.IsNullOrWhiteSpace(text))
            return "";
        var value = text.Replace("%%C", "Φ", StringComparison.OrdinalIgnoreCase)
            .Replace("%%D", "°", StringComparison.OrdinalIgnoreCase)
            .Replace("%%P", "±", StringComparison.OrdinalIgnoreCase);
        value = Regex.Replace(value, @"\\A\d+;", "");
        value = Regex.Replace(value, @"\\H[^;]+;", "");
        value = Regex.Replace(value, @"\\S([^;]+)\^([^;]*);", "$1/$2");
        value = Regex.Replace(value, @"\\S([^;]+);", "$1");
        value = Regex.Replace(value, @"[{}]", "");
        value = value.Replace(@"\P", " ");
        return Regex.Replace(value, @"\s+", " ").Trim();
    }

    static void AddPoint(Dictionary<string, object?> details, string name, object? value)
    {
        var point = DoubleList(value);
        if (point.Count > 0)
            details[name] = point;
    }

    static void AddPointFromProperties(
        Dictionary<string, object?> details,
        string name,
        object entity,
        List<string> missingFields,
        params string[] propertyNames)
    {
        foreach (var propertyName in propertyNames)
        {
            var point = DoubleList(GetProperty(entity, propertyName));
            if (point.Count == 0)
                continue;

            details[name] = point;
            details[$"{name}_source_property"] = propertyName;
            return;
        }

        missingFields.Add(name);
    }

    static void ApplyDimensionDefinitionPoints(Dictionary<string, object?> details, DimensionDefinitionPoints definitionPoints)
    {
        ApplyDimensionDefinitionPoint(details, "xline1_point", "dxf_group_13", definitionPoints.Group13);
        ApplyDimensionDefinitionPoint(details, "xline2_point", "dxf_group_14", definitionPoints.Group14);
        ApplyDimensionDefinitionPoint(details, "dim_line_point", "dxf_group_10", definitionPoints.Group10);
        ApplyDimensionDefinitionPoint(details, "text_position", "dxf_group_11", definitionPoints.Group11);
        ApplyDimensionDefinitionPoint(details, "center", "dxf_group_15", definitionPoints.Group15);
        ApplyDimensionDefinitionPoint(details, "chord_point", "dxf_group_16", definitionPoints.Group16);

        if (details.TryGetValue("dimension_point_missing_fields", out var value) && value is List<string> missing)
        {
            missing.RemoveAll(field => details.ContainsKey(field));
            if (missing.Count == 0)
                details.Remove("dimension_point_missing_fields");
        }
    }

    static void ApplyDimensionDefinitionPoint(
        Dictionary<string, object?> details,
        string field,
        string source,
        List<double>? point)
    {
        if (point is not { Count: >= 2 })
            return;

        details[field] = point;
        details[$"{field}_source_property"] = source;
    }

    static void EnsureSplineEndpoint(
        Dictionary<string, object?> details,
        string name,
        List<double> fitPoints,
        List<double> controlPoints,
        bool takeFirst)
    {
        if (details.TryGetValue(name, out var value) && DoubleList(value).Count >= 2)
            return;

        var point = EndpointFromFlatPoints(fitPoints, takeFirst);
        if (point.Count < 2)
            point = EndpointFromFlatPoints(controlPoints, takeFirst);
        if (point.Count >= 2)
            details[name] = point;
    }

    static List<double> EndpointFromFlatPoints(List<double> values, bool takeFirst)
    {
        if (values.Count < 2)
            return new List<double>();

        var stride = values.Count % 3 == 0 ? 3 : 2;
        if (values.Count < stride)
            return new List<double>();

        var index = takeFirst ? 0 : values.Count - stride;
        return new List<double> { values[index], values[index + 1] };
    }

    static List<double> DoubleList(object? value)
    {
        if (value == null)
            return new List<double>();
        if (value is List<double> list)
            return list;
        if (value is double[] doubles)
            return doubles.ToList();
        if (value is object[] objects)
            return objects.Select(ToDouble).Where(item => item != null).Select(item => item!.Value).ToList();
        if (value is Array array)
        {
            var result = new List<double>();
            foreach (var item in array)
            {
                if (ToDouble(item) is { } number)
                    result.Add(number);
            }
            return result;
        }
        return new List<double>();
    }

    static List<double> ReadPolylineBulges(object entity, int coordinateCount)
    {
        var result = new List<double>();
        var vertexCount = coordinateCount % 3 == 0 ? coordinateCount / 3 : coordinateCount / 2;
        for (var i = 0; i < Math.Max(0, vertexCount); i++)
        {
            var bulge = ToDouble(TryInvoke(entity, "GetBulge", [i])) ?? 0.0;
            result.Add(bulge);
        }
        return result;
    }

    static Dictionary<string, object?> Merge(Dictionary<string, object?> common, Dictionary<string, object?> details)
    {
        var result = new Dictionary<string, object?>(common, StringComparer.OrdinalIgnoreCase);
        foreach (var item in details)
            result[item.Key] = item.Value;
        return result;
    }

    static string ReadString(object obj, string name) => Convert.ToString(GetProperty(obj, name), CultureInfo.InvariantCulture) ?? "";

    static int? ToInt(object? value)
    {
        try { return value == null ? null : Convert.ToInt32(value, CultureInfo.InvariantCulture); }
        catch { return null; }
    }

    static double? ToDouble(object? value)
    {
        try { return value == null ? null : Convert.ToDouble(value, CultureInfo.InvariantCulture); }
        catch { return null; }
    }

    static bool? ToBool(object? value)
    {
        try { return value == null ? null : Convert.ToBoolean(value, CultureInfo.InvariantCulture); }
        catch { return null; }
    }

    static void ReleaseCom(object obj)
    {
        try
        {
            if (Marshal.IsComObject(obj))
                Marshal.FinalReleaseComObject(obj);
        }
        catch
        {
            // Best effort.
        }
    }

    static object? TryGetActiveComObject(string progId)
    {
        try
        {
            var hr = CLSIDFromProgID(progId, out var clsid);
            if (hr < 0)
                Marshal.ThrowExceptionForHR(hr);
            return GetActiveObject(ref clsid, IntPtr.Zero);
        }
        catch
        {
            return null;
        }
    }

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

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

sealed record SectionSymbol(
    string Label,
    string MatchedText,
    string FullText,
    BoundingBox Box,
    Dictionary<string, object?> Annotation)
{
    public Dictionary<string, object?> ToDictionary()
    {
        return new Dictionary<string, object?>
        {
            ["label"] = Label,
            ["matched_text"] = MatchedText,
            ["full_text"] = FullText,
            ["bbox"] = Box.ToDictionary(),
            ["handle"] = Annotation.TryGetValue("handle", out var handle) ? handle : null,
            ["object_name"] = Annotation.TryGetValue("object_name", out var objectName) ? objectName : null,
            ["layer"] = Annotation.TryGetValue("layer", out var layer) ? layer : null
        };
    }
}

sealed record ViewRegion(HashSet<PrimitiveNode> Nodes, BoundingBox Box, List<string> Handles);

sealed class CliOptions
{
    public string DrawingPath { get; set; } = "";
    public string OutputDir { get; set; } = Path.Combine(Environment.CurrentDirectory, "runtime", "dwg-draft", "latest-extraction");
    public string ProgId { get; set; } = "AutoCAD.Application.23.1";
    public bool Visible { get; set; } = true;
    public bool CloseAfterExtract { get; set; }
    public int MaxEntities { get; set; } = 100_000;
    public string MainViewSeedMode { get; set; } = "upper-left";
    public string MainViewReferenceSelectionPath { get; set; } = "";
    public string SaveMainViewOnlyDwgPath { get; set; } = "";
    public string SectionViewProbeDirection { get; set; } = "down";
    public string ReuseExtractionDir { get; set; } = "";
    public bool AllowStartAutoCad { get; set; }

    public static CliOptions Parse(string[] args)
    {
        var options = new CliOptions();
        for (var i = 0; i < args.Length; i++)
        {
            var key = args[i];
            var value = i + 1 < args.Length ? args[i + 1] : "";
            switch (key)
            {
                case "--drawing-path":
                    options.DrawingPath = Path.GetFullPath(value.Trim().Trim('"'));
                    i++;
                    break;
                case "--output-dir":
                    options.OutputDir = Path.GetFullPath(value.Trim().Trim('"'));
                    i++;
                    break;
                case "--prog-id":
                    options.ProgId = value;
                    i++;
                    break;
                case "--visible":
                    options.Visible = bool.TryParse(value, out var visible) ? visible : options.Visible;
                    i++;
                    break;
                case "--close-after-extract":
                    options.CloseAfterExtract = bool.TryParse(value, out var close) && close;
                    i++;
                    break;
                case "--max-entities":
                    options.MaxEntities = int.TryParse(value, out var max) ? max : options.MaxEntities;
                    i++;
                    break;
                case "--main-view-seed-mode":
                    options.MainViewSeedMode = value.Trim();
                    i++;
                    break;
                case "--main-view-reference-selection":
                    options.MainViewReferenceSelectionPath = Path.GetFullPath(value.Trim().Trim('"'));
                    i++;
                    break;
                case "--save-main-view-only-dwg-path":
                    options.SaveMainViewOnlyDwgPath = Path.GetFullPath(value.Trim().Trim('"'));
                    i++;
                    break;
                case "--section-view-probe-direction":
                    options.SectionViewProbeDirection = NormalizeSectionViewProbeDirection(value);
                    i++;
                    break;
                case "--reuse-extraction-dir":
                    options.ReuseExtractionDir = Path.GetFullPath(value.Trim().Trim('"'));
                    i++;
                    break;
                case "--allow-start-autocad":
                    options.AllowStartAutoCad = bool.TryParse(value, out var allowStart) && allowStart;
                    i++;
                    break;
            }
        }
        return options;
    }

    static string NormalizeSectionViewProbeDirection(string value)
    {
        var direction = value.Trim();
        if (string.Equals(direction, "up", StringComparison.OrdinalIgnoreCase))
            return "up";
        if (string.Equals(direction, "down", StringComparison.OrdinalIgnoreCase))
            return "down";
        throw new ArgumentException($"--section-view-probe-direction only supports up/down: {value}");
    }
}

readonly record struct DeleteResult(int DeletedCount);

sealed record SplineEndpoint(List<double> Start, List<double> End, string Source);

sealed record DimensionDefinitionPoints(
    List<double>? Group13,
    List<double>? Group14,
    List<double>? Group10,
    List<double>? Group11,
    List<double>? Group15,
    List<double>? Group16);

static class StaRunner
{
    public static T Run<T>(Func<T> action)
    {
        T? result = default;
        Exception? error = null;
        var thread = new Thread(() =>
        {
            try { result = action(); }
            catch (Exception ex) { error = ex; }
        });
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
        thread.Join();
        if (error != null)
            throw error;
        return result!;
    }
}

static class JsonDefaults
{
    public static JsonSerializerOptions Options { get; } = new()
    {
        WriteIndented = true,
        PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
        DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower
    };
}

sealed class DocumentExtraction
{
    public int ModelSpaceCount { get; set; }
    public int PaperSpaceCount { get; set; }
    public Dictionary<string, int> TypeCounts { get; set; } = new(StringComparer.OrdinalIgnoreCase);
    public List<Dictionary<string, object?>> EntitiesCommon { get; set; } = new();
    public List<Dictionary<string, object?>> Geometry { get; set; } = new();
    public List<Dictionary<string, object?>> NormalizedGeometry { get; set; } = new();
    public List<Dictionary<string, object?>> MergedLines { get; set; } = new();
    public List<Dictionary<string, object?>> GeometryOverlaps { get; set; } = new();
    public Dictionary<string, object?> NormalizationSummary { get; set; } = new(StringComparer.OrdinalIgnoreCase);
    public List<Dictionary<string, object?>> MainViewGeometry { get; set; } = new();
    public List<Dictionary<string, object?>> MainViewNormalizedGeometry { get; set; } = new();
    public List<Dictionary<string, object?>> MainViewMergedLines { get; set; } = new();
    public List<Dictionary<string, object?>> MainViewOverlaps { get; set; } = new();
    public Dictionary<string, object?> MainViewSignature { get; set; } = new(StringComparer.OrdinalIgnoreCase);
    public Dictionary<string, object?> BottomViewSignature { get; set; } = new(StringComparer.OrdinalIgnoreCase);
    public List<Dictionary<string, object?>> SectionCutSymbols { get; set; } = new();
    public Dictionary<string, object?> MainViewSelection { get; set; } = new(StringComparer.OrdinalIgnoreCase);
    public Dictionary<string, object?> ViewRegions { get; set; } = new(StringComparer.OrdinalIgnoreCase);
    public List<Dictionary<string, object?>> Annotations { get; set; } = new();
    public List<Dictionary<string, object?>> Dimensions { get; set; } = new();
    public List<Dictionary<string, object?>> DimensionalTolerances { get; set; } = new();
    public List<Dictionary<string, object?>> Roughness { get; set; } = new();
    public List<Dictionary<string, object?>> GeometricTolerances { get; set; } = new();
    public List<Dictionary<string, object?>> Datums { get; set; } = new();
    public List<Dictionary<string, object?>> UnknownEntities { get; set; } = new();
}

sealed class ExtractionResult
{
    public bool Ok { get; set; }
    public string Source { get; set; } = "";
    public string ProgId { get; set; } = "";
    public bool Started { get; set; }
    public string DrawingPath { get; set; } = "";
    public string ExportDir { get; set; } = "";
    public string ManifestPath { get; set; } = "";
    public int EntityCount { get; set; }
    public Dictionary<string, int> TypeCounts { get; set; } = new(StringComparer.OrdinalIgnoreCase);
    public List<CategoryFile> CategoryFiles { get; set; } = new();
    public string Message { get; set; } = "";
}

sealed class Manifest
{
    public bool Ok { get; set; }
    public string Schema { get; set; } = "";
    public string Source { get; set; } = "";
    public string ProgId { get; set; } = "";
    public bool StartedAutoCad { get; set; }
    public string DrawingPath { get; set; } = "";
    public DateTime ExportedAtUtc { get; set; }
    public int ModelSpaceCount { get; set; }
    public int PaperSpaceCount { get; set; }
    public int EntityCount { get; set; }
    public Dictionary<string, int> TypeCounts { get; set; } = new(StringComparer.OrdinalIgnoreCase);
    public List<CategoryFile> CategoryFiles { get; set; } = new();
}

sealed class CategoryFile
{
    public string Category { get; set; } = "";
    public string File { get; set; } = "";
    public string FullPath { get; set; } = "";
    public int ItemCount { get; set; }
    public bool Exists { get; set; }
}

sealed class LinePrimitive
{
    public int Index { get; init; }
    public string Handle { get; init; } = "";
    public string Layer { get; init; } = "";
    public string Linetype { get; init; } = "";
    public int? Color { get; init; }
    public int? Lineweight { get; init; }
    public string StyleKey { get; init; } = "";
    public Dictionary<string, object?> Style { get; init; } = new(StringComparer.OrdinalIgnoreCase);
    public List<string> SourceHandles { get; init; } = new();
    public double StartX { get; init; }
    public double StartY { get; init; }
    public double EndX { get; init; }
    public double EndY { get; init; }
    public double Ux { get; init; }
    public double Uy { get; init; }
    public double Nx { get; init; }
    public double Ny { get; init; }
    public double T0 { get; init; }
    public double T1 { get; init; }
    public double Offset { get; init; }
    public double Length { get; init; }
    public double AngleDeg { get; init; }

    public static LinePrimitive? FromDictionary(Dictionary<string, object?> item)
    {
        var start = ReadPoint(item, "start");
        var end = ReadPoint(item, "end");
        if (start.Count < 2 || end.Count < 2)
            return null;

        var dx = end[0] - start[0];
        var dy = end[1] - start[1];
        var length = Math.Sqrt(dx * dx + dy * dy);
        if (length <= 1e-9)
            return null;

        var ux = dx / length;
        var uy = dy / length;
        var nx = -uy;
        var ny = ux;
        var angle = NormalizeAngleDeg(Math.Atan2(dy, dx) * 180.0 / Math.PI);
        var handles = ReadStringList(item, "source_handles");
        var handle = ReadString(item, "handle");
        if (handles.Count == 0 && !string.IsNullOrWhiteSpace(handle))
            handles.Add(handle);

        return new LinePrimitive
        {
            Index = ReadInt(item, "index") ?? 0,
            Handle = handle,
            Layer = ReadString(item, "layer"),
            Linetype = ReadString(item, "linetype"),
            Color = ReadInt(item, "color"),
            Lineweight = ReadInt(item, "lineweight"),
            StyleKey = ReadString(item, "style_key"),
            Style = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
            {
                ["space"] = ReadString(item, "space"),
                ["layer"] = ReadString(item, "layer"),
                ["linetype"] = ReadString(item, "linetype"),
                ["color"] = ReadInt(item, "color"),
                ["lineweight"] = ReadInt(item, "lineweight")
            },
            SourceHandles = handles,
            StartX = start[0],
            StartY = start[1],
            EndX = end[0],
            EndY = end[1],
            Ux = ux,
            Uy = uy,
            Nx = nx,
            Ny = ny,
            T0 = start[0] * ux + start[1] * uy,
            T1 = end[0] * ux + end[1] * uy,
            Offset = start[0] * nx + start[1] * ny,
            Length = length,
            AngleDeg = angle
        };
    }

    public double SignedDistanceToLine(double x, double y) => x * Nx + y * Ny - Offset;

    public List<double> PointAt(double t)
    {
        return new List<double> { Ux * t + Nx * Offset, Uy * t + Ny * Offset };
    }

    static double NormalizeAngleDeg(double angle)
    {
        angle %= 180.0;
        if (angle < 0)
            angle += 180.0;
        return angle;
    }

    static List<double> ReadPoint(Dictionary<string, object?> item, string key)
    {
        if (!item.TryGetValue(key, out var value) || value is not IEnumerable<object> objects)
        {
            if (value is IEnumerable<double> doubles)
                return doubles.ToList();
            return new List<double>();
        }

        return objects
            .Select(value => value == null ? (double?)null : Convert.ToDouble(value, CultureInfo.InvariantCulture))
            .Where(value => value != null)
            .Select(value => value!.Value)
            .ToList();
    }

    static string ReadString(Dictionary<string, object?> item, string key)
    {
        return item.TryGetValue(key, out var value) ? Convert.ToString(value, CultureInfo.InvariantCulture) ?? "" : "";
    }

    static int? ReadInt(Dictionary<string, object?> item, string key)
    {
        try
        {
            return item.TryGetValue(key, out var value) && value != null
                ? Convert.ToInt32(value, CultureInfo.InvariantCulture)
                : null;
        }
        catch
        {
            return null;
        }
    }

    static List<string> ReadStringList(Dictionary<string, object?> item, string key)
    {
        if (!item.TryGetValue(key, out var value) || value == null)
            return new List<string>();
        if (value is IEnumerable<string> strings)
            return strings.Where(text => !string.IsNullOrWhiteSpace(text)).ToList();
        if (value is IEnumerable<object> objects)
            return objects.Select(obj => Convert.ToString(obj, CultureInfo.InvariantCulture) ?? "").Where(text => !string.IsNullOrWhiteSpace(text)).ToList();
        return new List<string>();
    }
}

sealed record LineInterval(LinePrimitive Line, double Start, double End);

sealed record NormalizedGeometrySet(
    List<Dictionary<string, object?>> Normalized,
    List<Dictionary<string, object?>> MergedLines,
    List<Dictionary<string, object?>> Overlaps,
    Dictionary<string, object?> Summary);

sealed class PrimitiveNode
{
    public string Source { get; init; } = "";
    public string Kind { get; init; } = "";
    public Dictionary<string, object?> Data { get; init; } = new(StringComparer.OrdinalIgnoreCase);
    public BoundingBox Box { get; init; }
    public List<string> SourceHandles { get; init; } = new();
    public List<List<double>> AnchorPoints { get; init; } = new();

    public static PrimitiveNode? FromRawGeometry(Dictionary<string, object?> item)
    {
        var objectName = ReadString(item, "object_name");
        var handle = ReadString(item, "handle");
        var kind = RawKind(objectName);
        var box = TryReadRawBox(item, kind);
        if (box == null)
            return null;

        return new PrimitiveNode
        {
            Source = "raw-geometry",
            Kind = kind,
            Data = item,
            Box = box.Value,
            SourceHandles = string.IsNullOrWhiteSpace(handle) ? new List<string>() : new List<string> { handle },
            AnchorPoints = ReadRawAnchorPoints(item, kind, box.Value)
        };
    }

    public static PrimitiveNode? FromDictionary(Dictionary<string, object?> item)
    {
        var kind = ReadString(item, "kind");
        var box = TryReadBox(item, kind);
        if (box == null)
            return null;

        return new PrimitiveNode
        {
            Source = item.ContainsKey("merge_type") ? "merged-line" : "normalized-geometry",
            Kind = kind,
            Data = item,
            Box = box.Value,
            SourceHandles = ReadStringList(item, "source_handles"),
            AnchorPoints = ReadAnchorPoints(item, kind)
        };
    }

    static string RawKind(string objectName)
    {
        if (objectName.Contains("Polyline", StringComparison.OrdinalIgnoreCase))
            return "polyline";
        if (objectName.Contains("Circle", StringComparison.OrdinalIgnoreCase))
            return "circle";
        if (objectName.Contains("Arc", StringComparison.OrdinalIgnoreCase))
            return "arc";
        if (objectName.Contains("Spline", StringComparison.OrdinalIgnoreCase))
            return "spline";
        if (objectName.Contains("Line", StringComparison.OrdinalIgnoreCase))
            return "line";
        if (objectName.Contains("Hatch", StringComparison.OrdinalIgnoreCase))
            return "hatch";
        return "geometry";
    }

    static BoundingBox? TryReadRawBox(Dictionary<string, object?> item, string kind)
    {
        if (TryReadEntityBox(item, "bbox") is { } entityBox)
            return entityBox;

        if (string.Equals(kind, "line", StringComparison.OrdinalIgnoreCase))
        {
            var start = ReadPoint(item, "start");
            var end = ReadPoint(item, "end");
            return BoundingBox.FromPoints(new[] { start, end });
        }

        if (string.Equals(kind, "polyline", StringComparison.OrdinalIgnoreCase))
        {
            var coordinates = ReadDoubleList(item, "coordinates");
            var stride = coordinates.Count % 3 == 0 ? 3 : 2;
            var points = new List<List<double>>();
            for (var i = 0; i + stride - 1 < coordinates.Count; i += stride)
                points.Add(new List<double> { coordinates[i], coordinates[i + 1] });
            return BoundingBox.FromPoints(points);
        }

        if (string.Equals(kind, "circle", StringComparison.OrdinalIgnoreCase) ||
            string.Equals(kind, "arc", StringComparison.OrdinalIgnoreCase))
        {
            var center = ReadPoint(item, "center");
            var radius = ReadDouble(item, "radius");
            if (center.Count < 2 || radius == null)
                return BoundingBox.FromPoints(new[] { ReadPoint(item, "start"), ReadPoint(item, "end") });
            return new BoundingBox(center[0] - radius.Value, center[1] - radius.Value, center[0] + radius.Value, center[1] + radius.Value);
        }

        if (string.Equals(kind, "spline", StringComparison.OrdinalIgnoreCase))
        {
            return BoundingBox.FromPoints(new[] { ReadPoint(item, "start"), ReadPoint(item, "end") });
        }

        return null;
    }

    static BoundingBox? TryReadEntityBox(Dictionary<string, object?> item, string key)
    {
        if (!item.TryGetValue(key, out var value) || value == null)
            return null;
        if (value is Dictionary<string, object?> dict)
        {
            var minX = ReadDouble(dict, "min_x");
            var minY = ReadDouble(dict, "min_y");
            var maxX = ReadDouble(dict, "max_x");
            var maxY = ReadDouble(dict, "max_y");
            if (minX != null && minY != null && maxX != null && maxY != null)
                return new BoundingBox(minX.Value, minY.Value, maxX.Value, maxY.Value);
        }
        return null;
    }

    static List<List<double>> ReadRawAnchorPoints(Dictionary<string, object?> item, string kind, BoundingBox box)
    {
        var points = new List<List<double>>();
        if (string.Equals(kind, "polyline", StringComparison.OrdinalIgnoreCase))
        {
            var coordinates = ReadDoubleList(item, "coordinates");
            var stride = coordinates.Count % 3 == 0 ? 3 : 2;
            for (var i = 0; i + stride - 1 < coordinates.Count; i += stride)
                points.Add(new List<double> { coordinates[i], coordinates[i + 1] });
        }
        else
        {
            foreach (var key in new[] { "start", "end", "center" })
            {
                var point = ReadPoint(item, key);
                if (point.Count >= 2)
                    points.Add(point);
            }
        }

        if (points.Count == 0)
            points.Add(new List<double> { box.CenterX, box.CenterY });

        return points;
    }

    static BoundingBox? TryReadBox(Dictionary<string, object?> item, string kind)
    {
        if (string.Equals(kind, "line", StringComparison.OrdinalIgnoreCase))
        {
            var start = ReadPoint(item, "start");
            var end = ReadPoint(item, "end");
            return BoundingBox.FromPoints(new[] { start, end });
        }

        if (string.Equals(kind, "circle", StringComparison.OrdinalIgnoreCase))
        {
            var center = ReadPoint(item, "center");
            var radius = ReadDouble(item, "radius");
            if (center.Count < 2 || radius == null)
                return null;
            return new BoundingBox(center[0] - radius.Value, center[1] - radius.Value, center[0] + radius.Value, center[1] + radius.Value);
        }

        if (string.Equals(kind, "arc", StringComparison.OrdinalIgnoreCase))
        {
            var points = new List<List<double>>
            {
                ReadPoint(item, "start"),
                ReadPoint(item, "end")
            }.Where(point => point.Count >= 2).ToList();

            var center = ReadPoint(item, "center");
            var radius = ReadDouble(item, "radius");
            if (center.Count >= 2 && radius != null)
                return new BoundingBox(center[0] - radius.Value, center[1] - radius.Value, center[0] + radius.Value, center[1] + radius.Value);

            return BoundingBox.FromPoints(points);
        }

        if (string.Equals(kind, "spline", StringComparison.OrdinalIgnoreCase))
        {
            var start = ReadPoint(item, "start");
            var end = ReadPoint(item, "end");
            return BoundingBox.FromPoints(new[] { start, end });
        }

        return null;
    }

    static List<List<double>> ReadAnchorPoints(Dictionary<string, object?> item, string kind)
    {
        var points = new List<List<double>>();
        foreach (var key in new[] { "start", "end", "center" })
        {
            var point = ReadPoint(item, key);
            if (point.Count >= 2)
                points.Add(point);
        }

        if (points.Count == 0)
        {
            var box = TryReadBox(item, kind);
            if (box != null)
                points.Add(new List<double> { box.Value.CenterX, box.Value.CenterY });
        }

        return points;
    }

    static List<double> ReadPoint(Dictionary<string, object?> item, string key)
    {
        if (!item.TryGetValue(key, out var value) || value == null)
            return new List<double>();
        if (value is IEnumerable<double> doubles)
            return doubles.ToList();
        if (value is IEnumerable<object> objects)
        {
            return objects
                .Select(obj => obj == null ? (double?)null : Convert.ToDouble(obj, CultureInfo.InvariantCulture))
                .Where(value => value != null)
                .Select(value => value!.Value)
                .ToList();
        }
        return new List<double>();
    }

    static string ReadString(Dictionary<string, object?> item, string key)
    {
        return item.TryGetValue(key, out var value) ? Convert.ToString(value, CultureInfo.InvariantCulture) ?? "" : "";
    }

    static double? ReadDouble(Dictionary<string, object?> item, string key)
    {
        try
        {
            return item.TryGetValue(key, out var value) && value != null
                ? Convert.ToDouble(value, CultureInfo.InvariantCulture)
                : null;
        }
        catch
        {
            return null;
        }
    }

    static List<double> ReadDoubleList(Dictionary<string, object?> item, string key)
    {
        if (!item.TryGetValue(key, out var value) || value == null)
            return new List<double>();
        if (value is IEnumerable<double> doubles)
            return doubles.ToList();
        if (value is IEnumerable<object> objects)
        {
            return objects
                .Select(obj => obj == null ? (double?)null : Convert.ToDouble(obj, CultureInfo.InvariantCulture))
                .Where(value => value != null)
                .Select(value => value!.Value)
                .ToList();
        }
        return new List<double>();
    }

    static List<string> ReadStringList(Dictionary<string, object?> item, string key)
    {
        if (!item.TryGetValue(key, out var value) || value == null)
            return new List<string>();
        if (value is IEnumerable<string> strings)
            return strings.Where(text => !string.IsNullOrWhiteSpace(text)).ToList();
        if (value is IEnumerable<object> objects)
            return objects.Select(obj => Convert.ToString(obj, CultureInfo.InvariantCulture) ?? "").Where(text => !string.IsNullOrWhiteSpace(text)).ToList();
        return new List<string>();
    }
}

readonly record struct BoundingBox(double MinX, double MinY, double MaxX, double MaxY)
{
    public double Width => MaxX - MinX;
    public double Height => MaxY - MinY;
    public double CenterX => (MinX + MaxX) / 2.0;
    public double CenterY => (MinY + MaxY) / 2.0;

    public static BoundingBox? FromPoints(IEnumerable<List<double>> points)
    {
        var valid = points.Where(point => point.Count >= 2).ToList();
        if (valid.Count == 0)
            return null;
        return new BoundingBox(
            valid.Min(point => point[0]),
            valid.Min(point => point[1]),
            valid.Max(point => point[0]),
            valid.Max(point => point[1]));
    }

    public static BoundingBox Union(IEnumerable<BoundingBox> boxes)
    {
        var list = boxes.ToList();
        if (list.Count == 0)
            return new BoundingBox(0, 0, 0, 0);
        return new BoundingBox(
            list.Min(box => box.MinX),
            list.Min(box => box.MinY),
            list.Max(box => box.MaxX),
            list.Max(box => box.MaxY));
    }

    public BoundingBox Expand(double value) => new(MinX - value, MinY - value, MaxX + value, MaxY + value);

    public bool Contains(double x, double y) => x >= MinX && x <= MaxX && y >= MinY && y <= MaxY;

    public bool ContainsBox(BoundingBox other) =>
        other.MinX >= MinX &&
        other.MaxX <= MaxX &&
        other.MinY >= MinY &&
        other.MaxY <= MaxY;

    public double DistanceTo(BoundingBox other)
    {
        var dx = Math.Max(0.0, Math.Max(other.MinX - MaxX, MinX - other.MaxX));
        var dy = Math.Max(0.0, Math.Max(other.MinY - MaxY, MinY - other.MaxY));
        return Math.Sqrt(dx * dx + dy * dy);
    }

    public Dictionary<string, object?> ToDictionary()
    {
        return new Dictionary<string, object?>
        {
            ["min_x"] = MinX,
            ["min_y"] = MinY,
            ["max_x"] = MaxX,
            ["max_y"] = MaxY,
            ["center_x"] = CenterX,
            ["center_y"] = CenterY
        };
    }
}
