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

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

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

    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        WriteIndented = true,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
        PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
        DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower
    };

    private static int Main(string[] args)
    {
        try
        {
            var request = CandidateRequest.Parse(args);
            request.ModelPath = NormalizePath(request.ModelPath);
            request.OutputDir = NormalizePath(request.OutputDir);
            request.DrawingTemplatePath = NormalizePath(request.DrawingTemplatePath);
            request.RankingJsonPath = NormalizePath(request.RankingJsonPath);
            request.StudentSignaturePath = NormalizePath(request.StudentSignaturePath);
            request.ReferenceSignaturePath = NormalizePath(request.ReferenceSignaturePath);
            request.StudentViewRegionsPath = NormalizePath(request.StudentViewRegionsPath);
            request.ReferenceViewRegionsPath = NormalizePath(request.ReferenceViewRegionsPath);
            request.OutputDwgPath = NormalizePath(request.OutputDwgPath);
            request.SectionCutSymbolsPath = NormalizePath(request.SectionCutSymbolsPath);

            if (!request.UseActiveModel && (string.IsNullOrWhiteSpace(request.ModelPath) || !File.Exists(request.ModelPath)))
                throw new FileNotFoundException($"模型文件不存在: {request.ModelPath}");
            if (!request.UseActiveModel &&
                !request.ModelPath.EndsWith(".SLDASM", StringComparison.OrdinalIgnoreCase) &&
                !request.ModelPath.EndsWith(".SLDPRT", StringComparison.OrdinalIgnoreCase))
                throw new ArgumentException("--model 必须是 .SLDASM 或 .SLDPRT 文件。");

            if (string.IsNullOrWhiteSpace(request.OutputDir))
                request.OutputDir = Path.Combine(System.Environment.CurrentDirectory, "runtime", "solidworks-view-candidates", Path.GetFileNameWithoutExtension(request.ModelPath));
            Directory.CreateDirectory(request.OutputDir);

            var sw = ConnectSolidWorks();
            var result = Generate(sw, request);
            WriteJson(Path.Combine(request.OutputDir, "candidate-result.json"), result);
            WriteJson(Path.Combine(request.OutputDir, "candidate-views.json"), result.Candidates);

            Console.WriteLine(JsonSerializer.Serialize(result, JsonOptions));
            return result.Ok ? 0 : 1;
        }
        catch (Exception ex)
        {
            Console.Error.WriteLine(ex);
            return 1;
        }
    }

    private static SldWorks ConnectSolidWorks()
    {
        try
        {
            var hr = CLSIDFromProgID("SldWorks.Application", out var clsid);
            if (hr < 0)
                Marshal.ThrowExceptionForHR(hr);
            var active = (SldWorks)GetActiveObject(ref clsid, IntPtr.Zero);
            active.Visible = true;
            return active;
        }
        catch
        {
            var type = Type.GetTypeFromProgID("SldWorks.Application")
                ?? throw new InvalidOperationException("无法创建 SolidWorks COM 对象。");
            var sw = (SldWorks)(Activator.CreateInstance(type)
                ?? throw new InvalidOperationException("SolidWorks COM 创建结果为空。"));
            sw.Visible = true;
            return sw;
        }
    }

    private static CandidateResult Generate(SldWorks sw, CandidateRequest request)
    {
        var started = DateTimeOffset.Now;
        var model = ResolveModel(sw, request);
        request.ModelPath = NormalizePath(Safe(() => model.GetPathName(), request.ModelPath));
        if (string.IsNullOrWhiteSpace(request.ModelPath))
            throw new InvalidOperationException("无法确定模型路径，CreateDrawViewFromModelView3 需要模型文件路径。");

        var mathUtility = sw.GetMathUtility()
            ?? throw new InvalidOperationException("SolidWorks GetMathUtility 返回空。");

        if (request.ExportActiveDrawingDwg)
        {
            var exportDrawingContext = ResolveDrawing(sw, request);
            return ExportActiveDrawingDwg(exportDrawingContext, request, started);
        }

        if (request.ListDrawingViews)
        {
            var listDrawingContext = ResolveDrawing(sw, request);
            return ListDrawingViews(listDrawingContext, request, started);
        }

        if (request.AlignMainViewOrigin)
        {
            var alignDrawingContext = ResolveDrawing(sw, request);
            return AlignPlacedMainViewToSignatureOrigin(alignDrawingContext, request, started);
        }

        if (request.AlignProjectedViewsToRegions)
        {
            var alignDrawingContext = ResolveDrawing(sw, request);
            return AlignProjectedViewToDwgRegionCenter(alignDrawingContext, request, started);
        }

        if (request.AlignSectionViewsToRegions)
        {
            var alignDrawingContext = ResolveDrawing(sw, request);
            return AlignSectionViewsToDwgRegionCenters(alignDrawingContext, request, started);
        }

        if (request.PlaceProjectedView)
        {
            var projectedDrawingContext = ResolveDrawing(sw, request);
            return PlaceProjectedView(projectedDrawingContext, request, started);
        }

        if (request.PlaceSectionViews)
        {
            var sectionDrawingContext = ResolveDrawing(sw, request);
            return PlaceSectionViews(sectionDrawingContext, request, started);
        }

        if (request.PlaceMainView && request.CandidateIndex == null)
            request.CandidateIndex = ReadBestCandidateIndex(request.RankingJsonPath);

        var candidates = BuildCandidates(request.ViewNamePrefix);
        if (request.CandidateIndex != null)
            candidates = candidates.Where(candidate => candidate.Index == request.CandidateIndex.Value).ToList();
        if (candidates.Count == 0)
            throw new InvalidOperationException($"没有找到候选视图: index={request.CandidateIndex}");
        if (request.PlaceMainView)
            RenameCandidates(candidates, request.MainViewNamePrefix);
        CreateNamedViews(model, mathUtility, candidates, request.KeepNamedViews);

        var drawingContext = ResolveDrawing(sw, request);
        var drawingModel = drawingContext.Model;
        var drawing = drawingContext.Drawing;

        if (request.ExportCandidateDwgsSequential)
        {
            CleanupCandidateDrawingViews(drawingModel, drawing, request.ViewNamePrefix);
            ExportCandidateDwgsSequential(drawingModel, drawing, request.ModelPath, candidates, request);

            if (!request.KeepNamedViews)
                DeleteNamedViews(model, candidates);

            return new CandidateResult
            {
                Ok = true,
                Message = "已串行导出 24 个候选视图 DWG；每次只插入并保存一个候选视图。",
                ModelPath = request.ModelPath,
                DrawingTemplatePath = drawingContext.TemplatePath,
                DrawingPath = drawingContext.DrawingPath,
                UsedActiveDrawing = request.UseActiveDrawing,
                OutputDir = request.OutputDir,
                StartedAt = started,
                CompletedAt = DateTimeOffset.Now,
                Candidates = candidates,
                Artifacts = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                {
                    ["candidate_dwg_dir"] = CandidateDwgDirectory(request),
                    ["candidate_dwg_count"] = candidates.Count(candidate => candidate.DwgSaveOk && File.Exists(candidate.DwgPath)).ToString(CultureInfo.InvariantCulture)
                },
                SourceApis =
                [
                    "IModelView.Orientation3",
                    "IModelDoc2.NameView",
                    "IDrawingDoc.CreateDrawViewFromModelView3",
                    "IModelDocExtension.SaveAs",
                    "IModelDocExtension.SelectByID2",
                    "IModelDocExtension.DeleteSelection2",
                    "IModelDoc2.DeleteNamedView"
                ]
            };
        }

        if (request.PlaceMainView)
        {
            var candidate = candidates.Single();
            if (request.ReplaceExistingMainView)
                CleanupCandidateDrawingViews(drawingModel, drawing, request.MainViewNamePrefix);
            var view = InsertSingleCandidateView(drawing, request.ModelPath, candidate, request)
                ?? throw new InvalidOperationException($"插入主视图失败: {candidate.Name}");
            Safe(() => drawingModel.ForceRebuild3(false), false);
            candidate.DrawingViewName = Safe(() => view.GetName2(), candidate.DrawingViewName);
            candidate.OrientationName = Safe(() => view.GetOrientationName(), candidate.OrientationName);
            candidate.Position = DoubleList(Safe(() => view.Position, null));
            candidate.ScaleDecimal = Safe(() => view.ScaleDecimal, 0.0);
            candidate.UseSheetScale = Safe(() => view.UseSheetScale, 0);

            return new CandidateResult
            {
                Ok = true,
                Message = "已在当前 SolidWorks 工程图中放置最优主视图。",
                ModelPath = request.ModelPath,
                DrawingTemplatePath = drawingContext.TemplatePath,
                DrawingPath = drawingContext.DrawingPath,
                UsedActiveDrawing = request.UseActiveDrawing,
                OutputDir = request.OutputDir,
                StartedAt = started,
                CompletedAt = DateTimeOffset.Now,
                Candidates = candidates,
                Artifacts = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                {
                    ["placed_main_view_name"] = candidate.DrawingViewName,
                    ["placed_orientation_name"] = candidate.OrientationName,
                    ["candidate_index"] = candidate.Index.ToString(CultureInfo.InvariantCulture),
                    ["direction_name"] = candidate.DirectionName,
                    ["roll_degrees"] = candidate.RollDeg.ToString(CultureInfo.InvariantCulture),
                    ["scale_decimal"] = candidate.ScaleDecimal.ToString(CultureInfo.InvariantCulture)
                },
                SourceApis =
                [
                    "IModelView.Orientation3",
                    "IModelDoc2.NameView",
                    "IDrawingDoc.CreateDrawViewFromModelView3",
                    "IModelDocExtension.SelectByID2",
                    "IModelDocExtension.DeleteSelection2"
                ]
            };
        }

        if (DateTimeOffset.Now.Ticks >= 0)
        throw new InvalidOperationException("当前流程只支持 --export-candidate-dwgs-sequential：每次插入一个候选视图、另存为一个 DWG、删除该视图，再交给 AutoCAD 提取和排序。旧的 SolidWorks 工程图 polyline 直接签名/排序方法已废弃。");

        return new CandidateResult
        {
            Ok = true,
            Message = "已生成 24 个 SolidWorks 候选主视图。",
            ModelPath = request.ModelPath,
            DrawingTemplatePath = drawingContext.TemplatePath,
            DrawingPath = drawingContext.DrawingPath,
            UsedActiveDrawing = request.UseActiveDrawing,
            OutputDir = request.OutputDir,
            StartedAt = started,
            CompletedAt = DateTimeOffset.Now,
            Candidates = candidates,
            SourceApis =
            [
                "IModelView.Orientation3",
                "IModelDoc2.NameView",
                "IDrawingDoc.CreateDrawViewFromModelView3",
                "ISldWorks.IGetFirstDocument2",
                "IModelDoc2.DeleteNamedView"
            ]
        };
    }

    private static ModelDoc2 ResolveModel(SldWorks sw, CandidateRequest request)
    {
        if (request.UseActiveModel)
        {
            var active = sw.IActiveDoc2 as ModelDoc2;
            if (active != null && IsPartOrAssembly(active))
                return active;

            var openModel = FindOpenModel(sw);
            if (openModel != null)
                return openModel;

            throw new InvalidOperationException("没有找到已打开的零件或装配体。请先打开模型，或使用 --model 指定路径。");
        }

        return OpenModel(sw, request.ModelPath);
    }

    private static ModelDoc2 OpenModel(SldWorks sw, string modelPath)
    {
        var extension = Path.GetExtension(modelPath);
        var type = extension.Equals(".SLDASM", StringComparison.OrdinalIgnoreCase)
            ? (int)swDocumentTypes_e.swDocASSEMBLY
            : (int)swDocumentTypes_e.swDocPART;
        var errors = 0;
        var warnings = 0;
        var model = sw.OpenDoc6(
            modelPath,
            type,
            (int)swOpenDocOptions_e.swOpenDocOptions_Silent,
            "",
            ref errors,
            ref warnings) as ModelDoc2;
        if (model == null)
            throw new InvalidOperationException($"打开模型失败: errors={errors}, warnings={warnings}");

        Safe(() =>
        {
            var activateErrors = 0;
            sw.ActivateDoc3(model.GetTitle(), false, 0, ref activateErrors);
            return activateErrors;
        }, 0);
        return model;
    }

    private static DrawingContext ResolveDrawing(SldWorks sw, CandidateRequest request)
    {
        if (request.UseActiveDrawing)
        {
            var drawingModel = FindOpenDrawing(sw)
                ?? throw new InvalidOperationException("没有找到已打开的工程图。请先打开 .SLDDRW，再运行 --use-active-drawing。");
            return new DrawingContext(
                drawingModel,
                drawingModel as DrawingDoc ?? throw new InvalidOperationException("已打开文档不是 DrawingDoc。"),
                "",
                Safe(() => drawingModel.GetPathName(), ""));
        }

        var templatePath = ResolveTemplatePath(sw, request);
        Safe(() =>
        {
            sw.PreSelectDwgTemplateSize((int)swDwgPaperSizes_e.swDwgPaperA2size, "");
            return true;
        }, false);
        var drawingModelNew = sw.NewDocument(
                templatePath,
                (int)swDwgPaperSizes_e.swDwgPaperA2size,
                request.SheetWidthM,
                request.SheetHeightM) as ModelDoc2
            ?? throw new InvalidOperationException("SolidWorks NewDocument 未返回工程图。");
        var drawing = drawingModelNew as DrawingDoc
            ?? throw new InvalidOperationException("新建文档不是 DrawingDoc。");
        return new DrawingContext(drawingModelNew, drawing, templatePath, Safe(() => drawingModelNew.GetPathName(), ""));
    }

    private static ModelDoc2? FindOpenDrawing(SldWorks sw)
    {
        var active = sw.IActiveDoc2 as ModelDoc2;
        if (active != null && Safe(() => active.GetType(), 0) == (int)swDocumentTypes_e.swDocDRAWING)
            return active;

        var doc = sw.IGetFirstDocument2();
        while (doc != null)
        {
            if (Safe(() => doc.GetType(), 0) == (int)swDocumentTypes_e.swDocDRAWING)
                return doc;
            doc = Safe(() => doc.IGetNext(), null);
        }
        return null;
    }

    private static ModelDoc2? FindOpenModel(SldWorks sw)
    {
        var doc = sw.IGetFirstDocument2();
        while (doc != null)
        {
            if (IsPartOrAssembly(doc))
                return doc;
            doc = Safe(() => doc.IGetNext(), null);
        }
        return null;
    }

    private static bool IsPartOrAssembly(ModelDoc2 doc)
    {
        var type = Safe(() => doc.GetType(), 0);
        return type == (int)swDocumentTypes_e.swDocPART || type == (int)swDocumentTypes_e.swDocASSEMBLY;
    }

    private static string ResolveTemplatePath(SldWorks sw, CandidateRequest request)
    {
        if (!string.IsNullOrWhiteSpace(request.DrawingTemplatePath))
        {
            if (!File.Exists(request.DrawingTemplatePath))
                throw new FileNotFoundException($"工程图模板不存在: {request.DrawingTemplatePath}");
            return request.DrawingTemplatePath;
        }

        var templatePath = sw.GetDocumentTemplate((int)swDocumentTypes_e.swDocDRAWING, "", 0, request.SheetWidthM, request.SheetHeightM);
        if (string.IsNullOrWhiteSpace(templatePath) || !File.Exists(templatePath))
            throw new FileNotFoundException($"无法解析 SolidWorks 工程图模板: {templatePath}");
        return templatePath;
    }

    private static List<ViewCandidate> BuildCandidates(string prefix)
    {
        var directions = new[]
        {
            ("px", new[] { 1.0, 0.0, 0.0 }),
            ("nx", new[] { -1.0, 0.0, 0.0 }),
            ("py", new[] { 0.0, 1.0, 0.0 }),
            ("ny", new[] { 0.0, -1.0, 0.0 }),
            ("pz", new[] { 0.0, 0.0, 1.0 }),
            ("nz", new[] { 0.0, 0.0, -1.0 })
        };
        var rolls = new[] { 0, 90, 180, 270 };
        var result = new List<ViewCandidate>();
        var index = 0;
        foreach (var (directionName, direction) in directions)
        {
            foreach (var roll in rolls)
            {
                index++;
                var name = $"{prefix}_{index:00}_{directionName}_r{roll}";
                var basis = BuildBasis(direction, roll);
                result.Add(new ViewCandidate
                {
                    Index = index,
                    Name = name,
                    DirectionName = directionName,
                    RollDeg = roll,
                    ViewDirection = direction.ToList(),
                    BasisU = basis.U.ToList(),
                    BasisV = basis.V.ToList(),
                    BasisD = basis.D.ToList()
                });
            }
        }
        return result;
    }

    private static void RenameCandidates(List<ViewCandidate> candidates, string prefix)
    {
        foreach (var candidate in candidates)
            candidate.Name = $"{prefix}_{candidate.Index:00}_{candidate.DirectionName}_r{candidate.RollDeg}";
    }

    private static int ReadBestCandidateIndex(string rankingJsonPath)
    {
        if (string.IsNullOrWhiteSpace(rankingJsonPath))
            throw new ArgumentException("--place-main-view 需要同时传 --candidate-index，或传 --ranking-json 自动读取 rank 1。");
        if (!File.Exists(rankingJsonPath))
            throw new FileNotFoundException($"排序结果文件不存在: {rankingJsonPath}");

        using var document = JsonDocument.Parse(File.ReadAllText(rankingJsonPath));
        if (!document.RootElement.TryGetProperty("ranking", out var ranking) ||
            ranking.ValueKind != JsonValueKind.Array ||
            ranking.GetArrayLength() == 0)
            throw new InvalidOperationException($"排序结果中没有 ranking 数组: {rankingJsonPath}");

        var best = ranking.EnumerateArray()
            .OrderBy(item => item.TryGetProperty("rank", out var rank) && rank.TryGetInt32(out var value) ? value : int.MaxValue)
            .First();

        if (!best.TryGetProperty("index", out var indexElement) || !indexElement.TryGetInt32(out var index))
            throw new InvalidOperationException($"排序第一名缺少 index 字段: {rankingJsonPath}");
        return index;
    }

    private static CandidateResult AlignPlacedMainViewToSignatureOrigin(
        DrawingContext drawingContext,
        CandidateRequest request,
        DateTimeOffset started)
    {
        var studentOrigin = ReadSignatureOrigin(request.StudentSignaturePath, "student");
        if (string.IsNullOrWhiteSpace(request.ReferenceSignaturePath))
            throw new ArgumentException("--align-main-view-origin 只保留 DWG 对比路径，必须提供 --reference-signature。该文件应来自 SW 工程图导出 DWG 后的 AutoCAD 提取结果。");
        var referenceOrigin = ReadSignatureOrigin(request.ReferenceSignaturePath, "reference");
        var view = FindMainDrawingView(drawingContext.Drawing, request)
            ?? throw new InvalidOperationException($"没有找到需要平移的主视图。prefix={request.MainViewNamePrefix}, orientation={request.MainViewOrientationName}");
        var viewName = Safe(() => Convert.ToString(view.GetName2(), CultureInfo.InvariantCulture) ?? "", "");
        var orientationName = Safe(() => Convert.ToString(view.GetOrientationName(), CultureInfo.InvariantCulture) ?? "", "");

        var deltaMmX = studentOrigin.X - referenceOrigin.X;
        var deltaMmY = studentOrigin.Y - referenceOrigin.Y;
        var appliedDeltaMmX = ShouldApplyAxis(request.AlignmentAxis, "x") ? deltaMmX : 0.0;
        var appliedDeltaMmY = ShouldApplyAxis(request.AlignmentAxis, "y") ? deltaMmY : 0.0;
        var deltaM = new[] { appliedDeltaMmX / 1000.0, appliedDeltaMmY / 1000.0 };

        var before = DoubleList(Safe<object?>(() => view.Position, null));
        if (before.Count < 2)
            throw new InvalidOperationException($"无法读取主视图 Position: {viewName}");

        var after = new[] { before[0] + deltaM[0], before[1] + deltaM[1] };
        Safe(() =>
        {
            view.Position = after;
            return true;
        }, false);
        Safe(() => drawingContext.Model.ForceRebuild3(false), false);
        var actualAfter = DoubleList(Safe<object?>(() => view.Position, null));

        return new CandidateResult
        {
            Ok = true,
            Message = "已按学生图左侧竖直线最上点整体平移 SolidWorks 主视图。",
            ModelPath = request.ModelPath,
            DrawingTemplatePath = drawingContext.TemplatePath,
            DrawingPath = drawingContext.DrawingPath,
            UsedActiveDrawing = request.UseActiveDrawing,
            OutputDir = request.OutputDir,
            StartedAt = started,
            CompletedAt = DateTimeOffset.Now,
            Artifacts = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                ["main_view_name"] = viewName,
                ["main_view_orientation_name"] = orientationName,
                ["student_origin_mm"] = $"{studentOrigin.X.ToString(CultureInfo.InvariantCulture)},{studentOrigin.Y.ToString(CultureInfo.InvariantCulture)}",
                ["reference_origin_mm"] = $"{referenceOrigin.X.ToString(CultureInfo.InvariantCulture)},{referenceOrigin.Y.ToString(CultureInfo.InvariantCulture)}",
                ["reference_origin_source"] = request.ReferenceSignaturePath,
                ["delta_mm"] = $"{deltaMmX.ToString(CultureInfo.InvariantCulture)},{deltaMmY.ToString(CultureInfo.InvariantCulture)}",
                ["alignment_axis"] = request.AlignmentAxis,
                ["applied_delta_mm"] = $"{appliedDeltaMmX.ToString(CultureInfo.InvariantCulture)},{appliedDeltaMmY.ToString(CultureInfo.InvariantCulture)}",
                ["delta_m"] = $"{deltaM[0].ToString(CultureInfo.InvariantCulture)},{deltaM[1].ToString(CultureInfo.InvariantCulture)}",
                ["position_before_m"] = $"{before[0].ToString(CultureInfo.InvariantCulture)},{before[1].ToString(CultureInfo.InvariantCulture)}",
                ["position_after_m"] = actualAfter.Count >= 2
                    ? $"{actualAfter[0].ToString(CultureInfo.InvariantCulture)},{actualAfter[1].ToString(CultureInfo.InvariantCulture)}"
                    : $"{after[0].ToString(CultureInfo.InvariantCulture)},{after[1].ToString(CultureInfo.InvariantCulture)}"
            },
            SourceApis =
            [
                "IView.Position",
                "IView.GetOrientationName",
                "IView.GetName2",
                "IModelDoc2.ForceRebuild3"
            ]
        };
    }

    private static bool ShouldApplyAxis(string axis, string component)
    {
        if (string.IsNullOrWhiteSpace(axis))
            return true;
        if (string.Equals(axis, "xy", StringComparison.OrdinalIgnoreCase))
            return true;
        return string.Equals(axis, component, StringComparison.OrdinalIgnoreCase);
    }

    private static CandidateResult AlignProjectedViewToDwgRegionCenter(
        DrawingContext drawingContext,
        CandidateRequest request,
        DateTimeOffset started)
    {
        if (string.IsNullOrWhiteSpace(request.StudentViewRegionsPath))
            throw new ArgumentException("--align-projected-views-to-regions requires --student-view-regions.");
        if (string.IsNullOrWhiteSpace(request.ReferenceViewRegionsPath))
            throw new ArgumentException("--align-projected-views-to-regions requires --reference-view-regions.");

        var direction = NormalizeProjectedDirection(request.ProjectedViewDirection);
        var regionKind = ProjectedDirectionToRegionKind(direction);
        var student = ReadOrthographicViewRegionCenter(request.StudentViewRegionsPath, regionKind, "student");
        var reference = ReadOrthographicViewRegionCenter(request.ReferenceViewRegionsPath, regionKind, "reference");
        var viewName = FirstNonEmpty(
            request.ProjectedViewName,
            direction == "right" ? "agent4_right_projected_view" : "agent4_bottom_projected_view");
        var view = FindDrawingView(drawingContext.Drawing, viewName)
            ?? throw new InvalidOperationException($"Projected drawing view not found: {viewName}");

        var deltaMmX = student.CenterX - reference.CenterX;
        var deltaMmY = student.CenterY - reference.CenterY;
        var appliedDeltaMmX = ShouldApplyAxis(request.AlignmentAxis, "x") ? deltaMmX : 0.0;
        var appliedDeltaMmY = ShouldApplyAxis(request.AlignmentAxis, "y") ? deltaMmY : 0.0;
        var before = DoubleList(Safe<object?>(() => view.Position, null));
        if (before.Count < 2)
            throw new InvalidOperationException($"Cannot read projected view Position: {viewName}");

        Safe(() =>
        {
            view.PositionLocked = false;
            return true;
        }, false);
        var after = new[] { before[0] + appliedDeltaMmX / 1000.0, before[1] + appliedDeltaMmY / 1000.0 };
        Safe(() =>
        {
            view.Position = after;
            return true;
        }, false);
        Safe(() => drawingContext.Model.ForceRebuild3(false), false);
        var actualAfter = DoubleList(Safe<object?>(() => view.Position, null));

        var reportPath = Path.Combine(request.OutputDir, "aligned-projected-view.json");
        WriteJson(reportPath, new Dictionary<string, object?>
        {
            ["direction"] = direction,
            ["region_kind"] = regionKind,
            ["view_name"] = viewName,
            ["student_view_regions"] = request.StudentViewRegionsPath,
            ["reference_view_regions"] = request.ReferenceViewRegionsPath,
            ["alignment_axis"] = request.AlignmentAxis,
            ["student_center_mm"] = new[] { student.CenterX, student.CenterY },
            ["reference_center_mm"] = new[] { reference.CenterX, reference.CenterY },
            ["delta_mm"] = new[] { deltaMmX, deltaMmY },
            ["applied_delta_mm"] = new[] { appliedDeltaMmX, appliedDeltaMmY },
            ["position_before_m"] = before,
            ["position_after_m"] = actualAfter.Count >= 2 ? actualAfter : after,
            ["kept_projection_relation"] = true
        });

        return new CandidateResult
        {
            Ok = true,
            Message = "Aligned projected drawing view to DWG-extracted orthographic region center.",
            ModelPath = request.ModelPath,
            DrawingTemplatePath = drawingContext.TemplatePath,
            DrawingPath = drawingContext.DrawingPath,
            UsedActiveDrawing = request.UseActiveDrawing,
            OutputDir = request.OutputDir,
            StartedAt = started,
            CompletedAt = DateTimeOffset.Now,
            Artifacts = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                ["student_view_regions"] = request.StudentViewRegionsPath,
                ["reference_view_regions"] = request.ReferenceViewRegionsPath,
                ["aligned_projected_view"] = reportPath,
                ["direction"] = direction,
                ["region_kind"] = regionKind,
                ["view_name"] = viewName,
                ["delta_mm"] = $"{deltaMmX.ToString(CultureInfo.InvariantCulture)},{deltaMmY.ToString(CultureInfo.InvariantCulture)}",
                ["applied_delta_mm"] = $"{appliedDeltaMmX.ToString(CultureInfo.InvariantCulture)},{appliedDeltaMmY.ToString(CultureInfo.InvariantCulture)}"
            },
            SourceApis =
            [
                "IView.Position",
                "IView.GetName2",
                "IModelDoc2.ForceRebuild3"
            ]
        };
    }

    private static CandidateResult AlignSectionViewsToDwgRegionCenters(
        DrawingContext drawingContext,
        CandidateRequest request,
        DateTimeOffset started)
    {
        if (string.IsNullOrWhiteSpace(request.StudentViewRegionsPath))
            throw new ArgumentException("--align-section-views-to-regions requires --student-view-regions.");
        if (string.IsNullOrWhiteSpace(request.ReferenceViewRegionsPath))
            throw new ArgumentException("--align-section-views-to-regions requires --reference-view-regions.");

        var studentCenters = ReadSectionViewRegionCenters(request.StudentViewRegionsPath, "student");
        var referenceCenters = ReadSectionViewRegionCenters(request.ReferenceViewRegionsPath, "reference");
        if (!string.IsNullOrWhiteSpace(request.SectionLabel))
        {
            var requestedLabel = NormalizeSectionRegionLabel(request.SectionLabel);
            studentCenters = studentCenters
                .Where(item => string.Equals(NormalizeSectionRegionLabel(item.Label), requestedLabel, StringComparison.OrdinalIgnoreCase))
                .ToList();
            referenceCenters = referenceCenters
                .Where(item => string.Equals(NormalizeSectionRegionLabel(item.Label), requestedLabel, StringComparison.OrdinalIgnoreCase))
                .ToList();
        }

        var referenceByLabel = referenceCenters
            .GroupBy(item => NormalizeSectionRegionLabel(item.Label), StringComparer.OrdinalIgnoreCase)
            .ToDictionary(group => group.Key, group => group.First(), StringComparer.OrdinalIgnoreCase);
        var matches = studentCenters
            .Select(student => new
            {
                Student = student,
                Reference = referenceByLabel.TryGetValue(NormalizeSectionRegionLabel(student.Label), out var reference) ? reference : null
            })
            .Where(item => item.Reference != null)
            .ToList();
        if (matches.Count == 0)
            throw new InvalidOperationException($"No matching section view labels between {request.StudentViewRegionsPath} and {request.ReferenceViewRegionsPath}");

        var averageDeltaMmX = matches.Average(item => item.Student.CenterX - item.Reference!.CenterX);
        var averageDeltaMmY = matches.Average(item => item.Student.CenterY - item.Reference!.CenterY);
        var deltasByLabel = matches.ToDictionary(
            item => NormalizeSectionRegionLabel(item.Student.Label),
            item =>
            {
                var deltaMmX = item.Student.CenterX - item.Reference!.CenterX;
                var deltaMmY = item.Student.CenterY - item.Reference.CenterY;
                var appliedDeltaMmX = ShouldApplyAxis(request.AlignmentAxis, "x") ? deltaMmX : 0.0;
                var appliedDeltaMmY = ShouldApplyAxis(request.AlignmentAxis, "y") ? deltaMmY : 0.0;
                return new SectionRegionDelta(deltaMmX, deltaMmY, appliedDeltaMmX, appliedDeltaMmY);
            },
            StringComparer.OrdinalIgnoreCase);
        var matchedLabelKeys = matches
            .Select(item => NormalizeSectionRegionLabel(item.Student.Label))
            .ToHashSet(StringComparer.OrdinalIgnoreCase);

        var moved = new List<Dictionary<string, object?>>();
        var viewObj = drawingContext.Drawing.GetFirstView();
        while (viewObj is View view)
        {
            var viewName = Safe(() => view.GetName2(), "");
            var type = Safe(() => view.Type, 0);
            var matchedLabel = matchedLabelKeys.FirstOrDefault(label =>
                IsLikelySectionViewForLabel(viewName, SectionViewLabelToken(label)));
            if (type == (int)swDrawingViewTypes_e.swDrawingSectionView &&
                !string.IsNullOrWhiteSpace(matchedLabel) &&
                deltasByLabel.TryGetValue(matchedLabel, out var delta))
            {
                var before = DoubleList(Safe<object?>(() => view.Position, null));
                if (before.Count >= 2)
                {
                    var after = new[] { before[0] + delta.AppliedDeltaMmX / 1000.0, before[1] + delta.AppliedDeltaMmY / 1000.0 };
                    Safe(() =>
                    {
                        view.PositionLocked = false;
                        return true;
                    }, false);
                    var removedAlignment = Safe(() =>
                    {
                        view.RemoveAlignment();
                        return true;
                    }, false);
                    Safe(() =>
                    {
                        view.Position = after;
                        return true;
                    }, false);
                    var actualAfter = DoubleList(Safe<object?>(() => view.Position, null));
                    moved.Add(new Dictionary<string, object?>
                    {
                        ["label"] = matchedLabel,
                        ["view_name"] = viewName,
                        ["delta_mm"] = new[] { delta.DeltaMmX, delta.DeltaMmY },
                        ["applied_delta_mm"] = new[] { delta.AppliedDeltaMmX, delta.AppliedDeltaMmY },
                        ["remove_alignment_called"] = removedAlignment,
                        ["position_before_m"] = before,
                        ["position_after_m"] = actualAfter.Count >= 2 ? actualAfter : after
                    });
                }
            }

            viewObj = view.GetNextView();
        }

        if (moved.Count == 0)
            throw new InvalidOperationException($"No SolidWorks section drawing views matched labels: {string.Join(",", matchedLabelKeys)}");

        Safe(() => drawingContext.Model.ForceRebuild3(false), false);
        var reportPath = Path.Combine(request.OutputDir, "aligned-section-views.json");
        WriteJson(reportPath, new Dictionary<string, object?>
        {
            ["student_view_regions"] = request.StudentViewRegionsPath,
            ["reference_view_regions"] = request.ReferenceViewRegionsPath,
            ["alignment_axis"] = request.AlignmentAxis,
            ["matched_section_count"] = matches.Count,
            ["average_delta_mm"] = new[] { averageDeltaMmX, averageDeltaMmY },
            ["matched_regions"] = matches.Select(item => new Dictionary<string, object?>
            {
                ["label"] = item.Student.Label,
                ["student_center_mm"] = new[] { item.Student.CenterX, item.Student.CenterY },
                ["reference_center_mm"] = new[] { item.Reference!.CenterX, item.Reference.CenterY },
                ["delta_mm"] = new[] { item.Student.CenterX - item.Reference.CenterX, item.Student.CenterY - item.Reference.CenterY }
            }).ToList(),
            ["moved_views"] = moved
        });

        return new CandidateResult
        {
            Ok = true,
            Message = "Aligned SolidWorks section drawing views to DWG-extracted section region centers.",
            ModelPath = request.ModelPath,
            DrawingTemplatePath = drawingContext.TemplatePath,
            DrawingPath = drawingContext.DrawingPath,
            UsedActiveDrawing = request.UseActiveDrawing,
            OutputDir = request.OutputDir,
            StartedAt = started,
            CompletedAt = DateTimeOffset.Now,
            Artifacts = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                ["student_view_regions"] = request.StudentViewRegionsPath,
                ["reference_view_regions"] = request.ReferenceViewRegionsPath,
                ["aligned_section_views"] = reportPath,
                ["matched_section_count"] = matches.Count.ToString(CultureInfo.InvariantCulture),
                ["moved_view_count"] = moved.Count.ToString(CultureInfo.InvariantCulture),
                ["average_delta_mm"] = $"{averageDeltaMmX.ToString(CultureInfo.InvariantCulture)},{averageDeltaMmY.ToString(CultureInfo.InvariantCulture)}"
            },
            SourceApis =
            [
                "IView.Position",
                "IView.GetName2",
                "IView.Type",
                "IModelDoc2.ForceRebuild3"
            ]
        };
    }

    private static List<SectionRegionCenter> ReadSectionViewRegionCenters(string path, string label)
    {
        if (!File.Exists(path))
            throw new FileNotFoundException($"{label} view-regions file not found: {path}");

        using var document = JsonDocument.Parse(File.ReadAllText(path));
        var root = document.RootElement;
        if (!root.TryGetProperty("section_views", out var sectionViews) || sectionViews.ValueKind != JsonValueKind.Array)
            throw new InvalidOperationException($"{label} view-regions missing section_views array: {path}");

        var result = new List<SectionRegionCenter>();
        foreach (var item in sectionViews.EnumerateArray())
        {
            if (!ReadBool(item, "ok"))
                continue;
            var sectionLabel = ReadString(item, "label");
            if (string.IsNullOrWhiteSpace(sectionLabel))
                continue;
            if (!item.TryGetProperty("bbox", out var bbox) ||
                !TryReadJsonDouble(bbox, "center_x", out var centerX) ||
                !TryReadJsonDouble(bbox, "center_y", out var centerY))
                continue;
            result.Add(new SectionRegionCenter(sectionLabel, centerX, centerY));
        }

        if (result.Count == 0)
            throw new InvalidOperationException($"{label} view-regions has no ok section view centers: {path}");
        return result;
    }

    private static OrthographicRegionCenter ReadOrthographicViewRegionCenter(string path, string kind, string label)
    {
        if (!File.Exists(path))
            throw new FileNotFoundException($"{label} view-regions file not found: {path}");

        using var document = JsonDocument.Parse(File.ReadAllText(path));
        var root = document.RootElement;
        if (!root.TryGetProperty("orthographic_views", out var views) || views.ValueKind != JsonValueKind.Array)
            throw new InvalidOperationException($"{label} view-regions missing orthographic_views array: {path}");

        foreach (var item in views.EnumerateArray())
        {
            if (!ReadBool(item, "ok"))
                continue;
            if (!string.Equals(ReadString(item, "kind"), kind, StringComparison.OrdinalIgnoreCase))
                continue;
            if (!item.TryGetProperty("bbox", out var bbox) ||
                !TryReadJsonDouble(bbox, "center_x", out var centerX) ||
                !TryReadJsonDouble(bbox, "center_y", out var centerY))
                continue;
            return new OrthographicRegionCenter(kind, centerX, centerY);
        }

        throw new InvalidOperationException($"{label} view-regions has no ok orthographic region kind={kind}: {path}");
    }

    private static string NormalizeProjectedDirection(string value)
    {
        var direction = string.IsNullOrWhiteSpace(value) ? "down" : value.Trim().ToLowerInvariant();
        return direction switch
        {
            "down" or "bottom" or "俯视图" => "down",
            "right" or "右视图" => "right",
            _ => throw new ArgumentException($"unsupported projected view direction: {value}")
        };
    }

    private static string ProjectedDirectionToRegionKind(string direction) =>
        NormalizeProjectedDirection(direction) == "right"
            ? "right_of_main_view_candidate"
            : "bottom_view_candidate";

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

    private static string NormalizeSectionRegionLabel(string label)
    {
        var cleaned = Regex.Replace(label.Trim().ToUpperInvariant(), @"\s+", "", RegexOptions.CultureInvariant);
        var match = Regex.Match(cleaned, @"^([A-Z0-9]{1,4})(?:-\1)?$", RegexOptions.CultureInvariant);
        return match.Success ? $"{match.Groups[1].Value}-{match.Groups[1].Value}" : cleaned;
    }

    private static string SectionViewLabelToken(string normalizedLabel)
    {
        var index = normalizedLabel.IndexOf('-', StringComparison.Ordinal);
        return index > 0 ? normalizedLabel[..index] : normalizedLabel;
    }

    private static CandidateResult ListDrawingViews(
        DrawingContext drawingContext,
        CandidateRequest request,
        DateTimeOffset started)
    {
        var views = new List<Dictionary<string, object?>>();
        var viewObj = drawingContext.Drawing.GetFirstView();
        while (viewObj is View view)
        {
            var name = Safe(() => view.GetName2(), "");
            var orientationName = Safe(() => view.GetOrientationName(), "");
            views.Add(new Dictionary<string, object?>
            {
                ["name"] = name,
                ["orientation_name"] = orientationName,
                ["type"] = Safe(() => view.Type, 0),
                ["position_m"] = DoubleList(Safe<object?>(() => view.Position, null)),
                ["outline_m"] = DoubleList(Safe<object?>(() => view.GetOutline(), null)),
                ["scale_decimal"] = Safe(() => view.ScaleDecimal, 0.0),
                ["use_sheet_scale"] = Safe(() => view.UseSheetScale, 0)
            });
            viewObj = view.GetNextView();
        }

        var outputPath = Path.Combine(request.OutputDir, "drawing-views.json");
        WriteJson(outputPath, views);
        return new CandidateResult
        {
            Ok = true,
            Message = "Listed drawing views from the current SolidWorks drawing.",
            ModelPath = request.ModelPath,
            DrawingTemplatePath = drawingContext.TemplatePath,
            DrawingPath = drawingContext.DrawingPath,
            UsedActiveDrawing = request.UseActiveDrawing,
            OutputDir = request.OutputDir,
            StartedAt = started,
            CompletedAt = DateTimeOffset.Now,
            Artifacts = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                ["drawing_views"] = outputPath,
                ["drawing_view_count"] = views.Count.ToString(CultureInfo.InvariantCulture)
            },
            SourceApis =
            [
                "IDrawingDoc.GetFirstView",
                "IView.GetName2",
                "IView.GetOrientationName",
                "IView.Position",
                "IView.GetOutline"
            ]
        };
    }

    private static CandidateResult ExportActiveDrawingDwg(
        DrawingContext drawingContext,
        CandidateRequest request,
        DateTimeOffset started)
    {
        var dwgPath = request.OutputDwgPath;
        if (string.IsNullOrWhiteSpace(dwgPath))
        {
            var baseName = !string.IsNullOrWhiteSpace(drawingContext.DrawingPath)
                ? Path.GetFileNameWithoutExtension(drawingContext.DrawingPath)
                : "active-solidworks-drawing";
            dwgPath = Path.Combine(request.OutputDir, SanitizeFileName(baseName) + ".dwg");
        }

        var rebuildAttempt = 0;
        var maxRebuildAttempts = Math.Max(0, request.CandidateRebuildRetryCount);
        var save = new SaveResult(false, 0, 0);
        while (maxRebuildAttempts == 0 || rebuildAttempt < maxRebuildAttempts)
        {
            rebuildAttempt++;
            Safe(() => drawingContext.Model.ForceRebuild3(false), false);
            WaitBeforeNextCandidateAttempt(request);

            save = SaveAsWithRetries(drawingContext.Model, dwgPath, request.SaveRetryCount, request.SaveRetryDelayMs);
            if (save.Ok)
                break;

            Console.Error.WriteLine(
                $"当前工程图 DWG 导出失败，准备重建后重试：attempt={rebuildAttempt}, errors={save.Errors}, warnings={save.Warnings}");
        }

        return new CandidateResult
        {
            Ok = save.Ok,
            Message = save.Ok ? "已导出当前 SolidWorks 工程图 DWG。" : "当前 SolidWorks 工程图 DWG 导出失败。",
            ModelPath = request.ModelPath,
            DrawingTemplatePath = drawingContext.TemplatePath,
            DrawingPath = drawingContext.DrawingPath,
            UsedActiveDrawing = request.UseActiveDrawing,
            OutputDir = request.OutputDir,
            StartedAt = started,
            CompletedAt = DateTimeOffset.Now,
            Artifacts = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                ["dwg_path"] = dwgPath,
                ["dwg_save_ok"] = save.Ok.ToString(CultureInfo.InvariantCulture),
                ["dwg_save_errors"] = save.Errors.ToString(CultureInfo.InvariantCulture),
                ["dwg_save_warnings"] = save.Warnings.ToString(CultureInfo.InvariantCulture),
                ["dwg_rebuild_attempts"] = rebuildAttempt.ToString(CultureInfo.InvariantCulture),
                ["dwg_rebuild_retry_limit"] = maxRebuildAttempts == 0 ? "unlimited" : maxRebuildAttempts.ToString(CultureInfo.InvariantCulture)
            },
            SourceApis =
            [
                "IModelDocExtension.SaveAs",
                "IModelDoc2.SaveAs4",
                "IModelDoc2.ForceRebuild3"
            ]
        };
    }

    private static CandidateResult PlaceProjectedView(
        DrawingContext drawingContext,
        CandidateRequest request,
        DateTimeOffset started)
    {
        if (string.IsNullOrWhiteSpace(request.MainViewOrientationName))
            throw new ArgumentException("--place-projected-view requires --main-view-orientation-name with the parent drawing view name or orientation name.");
        if (request.ProjectedViewXM == null || request.ProjectedViewYM == null)
            throw new ArgumentException("--place-projected-view requires --projected-view-x-m and --projected-view-y-m.");

        var parentView = FindMainDrawingView(drawingContext.Drawing, request)
            ?? throw new InvalidOperationException($"Parent drawing view not found: {request.MainViewOrientationName}");
        var parentName = Safe(() => parentView.GetName2(), "");
        var parentOrientation = Safe(() => parentView.GetOrientationName(), "");
        var parentPosition = DoubleList(Safe<object?>(() => parentView.Position, null));

        if (request.ReplaceExistingProjectedView && !string.IsNullOrWhiteSpace(request.ProjectedViewName))
        {
            DeleteDrawingView(drawingContext.Model, new ViewCandidate
            {
                DrawingViewName = request.ProjectedViewName,
                OrientationName = request.ProjectedViewName,
                Name = request.ProjectedViewName
            });
        }

        drawingContext.Model.ClearSelection2(true);
        var selected = Safe(() => drawingContext.Model.Extension.SelectByID2(parentName, "DRAWINGVIEW", 0, 0, 0, false, 0, null, 0), false);
        if (!selected)
            selected = Safe(() => drawingContext.Model.Extension.SelectByID2(request.MainViewOrientationName, "DRAWINGVIEW", 0, 0, 0, false, 0, null, 0), false);
        if (!selected)
            throw new InvalidOperationException($"Failed to select parent drawing view: {parentName}");

        var view = drawingContext.Drawing.CreateUnfoldedViewAt3(
            request.ProjectedViewXM.Value,
            request.ProjectedViewYM.Value,
            0,
            false) as View
            ?? throw new InvalidOperationException("CreateUnfoldedViewAt3 returned null.");

        if (!string.IsNullOrWhiteSpace(request.ProjectedViewName))
            Safe(() => view.SetName2(request.ProjectedViewName), false);

        Safe(() => drawingContext.Model.ForceRebuild3(false), false);
        var viewName = Safe(() => view.GetName2(), "");
        var viewOrientation = Safe(() => view.GetOrientationName(), "");
        var viewPosition = DoubleList(Safe<object?>(() => view.Position, null));

        return new CandidateResult
        {
            Ok = true,
            Message = "Created a projected drawing view from the current main view.",
            ModelPath = request.ModelPath,
            DrawingTemplatePath = drawingContext.TemplatePath,
            DrawingPath = drawingContext.DrawingPath,
            UsedActiveDrawing = request.UseActiveDrawing,
            OutputDir = request.OutputDir,
            StartedAt = started,
            CompletedAt = DateTimeOffset.Now,
            Artifacts = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                ["parent_view_name"] = parentName,
                ["parent_view_orientation_name"] = parentOrientation,
                ["parent_view_position_m"] = parentPosition.Count >= 2
                    ? $"{parentPosition[0].ToString(CultureInfo.InvariantCulture)},{parentPosition[1].ToString(CultureInfo.InvariantCulture)}"
                    : "",
                ["projected_view_name"] = viewName,
                ["projected_view_orientation_name"] = viewOrientation,
                ["projected_view_type"] = Safe(() => view.Type, 0).ToString(CultureInfo.InvariantCulture),
                ["projected_view_position_m"] = viewPosition.Count >= 2
                    ? $"{viewPosition[0].ToString(CultureInfo.InvariantCulture)},{viewPosition[1].ToString(CultureInfo.InvariantCulture)}"
                    : "",
                ["requested_position_m"] = $"{request.ProjectedViewXM.Value.ToString(CultureInfo.InvariantCulture)},{request.ProjectedViewYM.Value.ToString(CultureInfo.InvariantCulture)}",
                ["projection_direction"] = request.ProjectedViewDirection
            },
            SourceApis =
            [
                "IModelDocExtension.SelectByID2",
                "IDrawingDoc.CreateUnfoldedViewAt3",
                "IView.SetName2",
                "IView.Position",
                "IModelDoc2.ForceRebuild3"
            ]
        };
    }

    private static CandidateResult PlaceSectionViews(
        DrawingContext drawingContext,
        CandidateRequest request,
        DateTimeOffset started)
    {
        if (string.IsNullOrWhiteSpace(request.SectionCutSymbolsPath))
            throw new ArgumentException("--place-section-views requires --section-cut-symbols.");
        if (!File.Exists(request.SectionCutSymbolsPath))
            throw new FileNotFoundException($"Section cut symbols file not found: {request.SectionCutSymbolsPath}");

        var symbols = ReadSectionCutSymbols(request.SectionCutSymbolsPath)
            .Where(symbol => symbol.Ok && string.Equals(symbol.Kind, "section_cut_symbol_pair", StringComparison.OrdinalIgnoreCase))
            .Where(symbol => string.IsNullOrWhiteSpace(request.SectionLabel) ||
                             string.Equals(symbol.Label, request.SectionLabel, StringComparison.OrdinalIgnoreCase))
            .ToList();
        if (symbols.Count == 0)
            throw new InvalidOperationException($"No section cut symbols to place. path={request.SectionCutSymbolsPath}, label={request.SectionLabel}");

        var placed = new List<Dictionary<string, object?>>();
        foreach (var symbol in symbols)
        {
            var parentView = ResolveSectionParentView(drawingContext.Drawing, request, symbol)
                ?? throw new InvalidOperationException($"Parent drawing view not found for section {symbol.Label}, view_kind={symbol.ViewKind}");
            var parentName = Safe(() => parentView.GetName2(), "");
            var parentOrientation = Safe(() => parentView.GetOrientationName(), "");
            var viewName = BuildSectionViewName(request, symbol);

            if (request.ReplaceExistingSectionViews)
            {
                DeleteExistingSectionViewsByLabel(drawingContext.Model, drawingContext.Drawing, symbol.Label);
                DeleteDrawingView(drawingContext.Model, new ViewCandidate
                {
                    DrawingViewName = viewName,
                    OrientationName = viewName,
                    Name = viewName
                });
            }

            drawingContext.Model.ClearSelection2(true);
            var activated = Safe(() => drawingContext.Drawing.ActivateView(parentName), false);
            if (!activated && !string.IsNullOrWhiteSpace(parentOrientation))
                activated = Safe(() => drawingContext.Drawing.ActivateView(parentOrientation), false);
            if (!activated)
                throw new InvalidOperationException($"Failed to activate parent drawing view: {parentName}");

            var sketchLine = BuildSectionSketchLine(parentView, symbol);
            var start = sketchLine.StartMm;
            var end = sketchLine.EndMm;
            var startM = sketchLine.StartM;
            var endM = sketchLine.EndM;
            var segment = drawingContext.Model.SketchManager.CreateLine(startM[0], startM[1], 0, endM[0], endM[1], 0)
                ?? throw new InvalidOperationException($"Failed to create section line for {symbol.Label} in {parentName}");
            var selected = Safe(() => segment.Select2(false, 0), false);
            if (!selected)
                selected = Safe(() => segment.Select(false), false);
            if (!selected)
                throw new InvalidOperationException($"Failed to select section line for {symbol.Label}");

            var placementM = ComputeSectionViewPlacement(symbol, request.SectionViewOffsetM);
            var options = BuildSectionViewOptions(symbol, request);
            var view = drawingContext.Drawing.CreateSectionViewAt5(
                placementM[0],
                placementM[1],
                0,
                symbol.Label,
                options,
                null,
                request.SectionDepthM) as View
                ?? throw new InvalidOperationException($"CreateSectionViewAt5 returned null for {symbol.Label}");

            Safe(() => view.SetName2(viewName), false);
            Safe(() => drawingContext.Model.ForceRebuild3(false), false);

            var actualName = Safe(() => view.GetName2(), viewName);
            var actualPosition = DoubleList(Safe<object?>(() => view.Position, null));
            placed.Add(new Dictionary<string, object?>
            {
                ["label"] = symbol.Label,
                ["source_parent_handle"] = symbol.ParentHandle,
                ["view_kind"] = symbol.ViewKind,
                ["parent_view_name"] = parentName,
                ["parent_view_orientation_name"] = parentOrientation,
                ["section_view_name"] = actualName,
                ["cut_line_mm"] = new Dictionary<string, object?>
                {
                    ["start"] = start,
                    ["end"] = end,
                    ["center"] = sketchLine.CenterMm,
                    ["direction"] = symbol.CutLine.Direction,
                    ["source"] = sketchLine.Source
                },
                ["cut_line_m"] = new Dictionary<string, object?>
                {
                    ["start"] = new[] { start[0] / 1000.0, start[1] / 1000.0 },
                    ["end"] = new[] { end[0] / 1000.0, end[1] / 1000.0 },
                    ["center"] = new[] { sketchLine.CenterMm[0] / 1000.0, sketchLine.CenterMm[1] / 1000.0 }
                },
                ["sketch_line_m"] = new Dictionary<string, object?>
                {
                    ["start"] = startM,
                    ["end"] = endM,
                    ["center"] = sketchLine.CenterM,
                    ["coordinate_system"] = "active_parent_drawing_view",
                    ["expected_default_arrow_direction"] = ComputeSectionLineDefaultArrowDirection(startM, endM)
                },
                ["arrow_direction"] = symbol.AverageArrowDirection,
                ["requested_position_m"] = placementM,
                ["actual_position_m"] = actualPosition,
                ["options"] = options
            });
        }

        WriteJson(Path.Combine(request.OutputDir, "placed-section-views.json"), placed);

        return new CandidateResult
        {
            Ok = true,
            Message = "Created SolidWorks section views from extracted student DWG section cut symbols.",
            ModelPath = request.ModelPath,
            DrawingTemplatePath = drawingContext.TemplatePath,
            DrawingPath = drawingContext.DrawingPath,
            UsedActiveDrawing = request.UseActiveDrawing,
            OutputDir = request.OutputDir,
            StartedAt = started,
            CompletedAt = DateTimeOffset.Now,
            Artifacts = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                ["section_cut_symbols"] = request.SectionCutSymbolsPath,
                ["section_count"] = placed.Count.ToString(CultureInfo.InvariantCulture),
                ["placed_section_views"] = Path.Combine(request.OutputDir, "placed-section-views.json")
            },
            SourceApis =
            [
                "IDrawingDoc.ActivateView",
                "ISketchManager.CreateLine",
                "ISketchSegment.Select2",
                "IDrawingDoc.CreateSectionViewAt5",
                "IView.SetName2",
                "IModelDoc2.ForceRebuild3"
            ]
        };
    }

    private static void DeleteExistingSectionViewsByLabel(ModelDoc2 drawingModel, DrawingDoc drawing, string label)
    {
        if (string.IsNullOrWhiteSpace(label))
            return;

        var toDelete = new List<string>();
        var viewObj = drawing.GetFirstView();
        while (viewObj is View view)
        {
            var name = Safe(() => view.GetName2(), "");
            var type = Safe(() => view.Type, 0);
            if (type == (int)swDrawingViewTypes_e.swDrawingSectionView &&
                IsLikelySectionViewForLabel(name, label))
            {
                toDelete.Add(name);
            }

            viewObj = view.GetNextView();
        }

        foreach (var name in toDelete.Distinct(StringComparer.OrdinalIgnoreCase))
        {
            DeleteDrawingView(drawingModel, new ViewCandidate
            {
                DrawingViewName = name,
                OrientationName = name,
                Name = name
            });
        }
    }

    private static bool IsLikelySectionViewForLabel(string viewName, string label)
    {
        if (string.IsNullOrWhiteSpace(viewName) || string.IsNullOrWhiteSpace(label))
            return false;
        var normalized = Regex.Replace(viewName, @"\s+", "", RegexOptions.CultureInvariant).ToUpperInvariant();
        var token = label.Trim().ToUpperInvariant();
        return normalized.Contains($"{token}-{token}", StringComparison.OrdinalIgnoreCase) ||
               normalized.EndsWith(token, StringComparison.OrdinalIgnoreCase);
    }

    private static View? ResolveSectionParentView(DrawingDoc drawing, CandidateRequest request, SectionCutSymbol symbol)
    {
        if (string.Equals(symbol.ViewKind, "bottom_view_candidate", StringComparison.OrdinalIgnoreCase) &&
            !string.IsNullOrWhiteSpace(request.ProjectedViewName))
        {
            return FindDrawingView(drawing, request.ProjectedViewName);
        }

        if (string.Equals(symbol.ViewKind, "main_view", StringComparison.OrdinalIgnoreCase))
            return FindMainDrawingView(drawing, request);

        return FindMainDrawingView(drawing, request)
            ?? (!string.IsNullOrWhiteSpace(request.ProjectedViewName) ? FindDrawingView(drawing, request.ProjectedViewName) : null);
    }

    private static View? FindDrawingView(DrawingDoc drawing, string nameOrOrientation)
    {
        var viewObj = drawing.GetFirstView();
        while (viewObj is View view)
        {
            var name = Safe(() => view.GetName2(), "");
            var orientationName = Safe(() => view.GetOrientationName(), "");
            if (string.Equals(name, nameOrOrientation, StringComparison.OrdinalIgnoreCase) ||
                string.Equals(orientationName, nameOrOrientation, StringComparison.OrdinalIgnoreCase))
                return view;
            viewObj = view.GetNextView();
        }

        return null;
    }

    private static string BuildSectionViewName(CandidateRequest request, SectionCutSymbol symbol)
    {
        var label = string.IsNullOrWhiteSpace(symbol.Label) ? "section" : symbol.Label.Trim();
        return $"{request.SectionViewNamePrefix}_{label}";
    }

    private static SectionSketchLine BuildSectionSketchLine(View parentView, SectionCutSymbol symbol)
    {
        var center = symbol.CutLine.Center.Count >= 2
            ? symbol.CutLine.Center
            : [
                (symbol.CutLine.Start[0] + symbol.CutLine.End[0]) / 2.0,
                (symbol.CutLine.Start[1] + symbol.CutLine.End[1]) / 2.0
            ];
        var centerM = new[] { center[0] / 1000.0, center[1] / 1000.0 };
        var parentPosition = DoubleList(Safe<object?>(() => parentView.Position, null));
        var parentX = parentPosition.Count >= 2 ? parentPosition[0] : 0.0;
        var parentY = parentPosition.Count >= 2 ? parentPosition[1] : 0.0;
        var outline = DoubleList(Safe<object?>(() => parentView.GetOutline(), null));
        if (outline.Count >= 4)
        {
            var minX = Math.Min(outline[0], outline[2]);
            var maxX = Math.Max(outline[0], outline[2]);
            var minY = Math.Min(outline[1], outline[3]);
            var maxY = Math.Max(outline[1], outline[3]);
            if (string.Equals(symbol.CutLine.Direction, "horizontal", StringComparison.OrdinalIgnoreCase))
            {
                var sheetStart = new[] { minX, centerM[1] };
                var sheetEnd = new[] { maxX, centerM[1] };
                return BuildSectionSketchLineFromSheetPoints(
                    sheetStart,
                    sheetEnd,
                    center,
                    centerM,
                    parentX,
                    parentY,
                    symbol,
                    "cut_line_center_through_parent_view_outline_parent_local");
            }

            var verticalSheetStart = new[] { centerM[0], maxY };
            var verticalSheetEnd = new[] { centerM[0], minY };
            return BuildSectionSketchLineFromSheetPoints(
                verticalSheetStart,
                verticalSheetEnd,
                center,
                centerM,
                parentX,
                parentY,
                symbol,
                "cut_line_center_through_parent_view_outline_parent_local");
        }

        return BuildSectionSketchLineFromSheetPoints(
            [symbol.CutLine.Start[0] / 1000.0, symbol.CutLine.Start[1] / 1000.0],
            [symbol.CutLine.End[0] / 1000.0, symbol.CutLine.End[1] / 1000.0],
            center,
            centerM,
            parentX,
            parentY,
            symbol,
            "fallback_cut_line_start_end_parent_local");
    }

    private static SectionSketchLine BuildSectionSketchLineFromSheetPoints(
        double[] sheetStartM,
        double[] sheetEndM,
        List<double> centerMm,
        double[] centerM,
        double parentX,
        double parentY,
        SectionCutSymbol symbol,
        string source)
    {
        var oriented = OrientSectionSketchLineByDwgArrow(sheetStartM, sheetEndM, symbol);
        var start = oriented.StartM;
        var end = oriented.EndM;
        return new SectionSketchLine(
            [start[0] * 1000.0, start[1] * 1000.0],
            [end[0] * 1000.0, end[1] * 1000.0],
            centerMm,
            [start[0] - parentX, start[1] - parentY],
            [end[0] - parentX, end[1] - parentY],
            [centerM[0] - parentX, centerM[1] - parentY],
            oriented.Reversed ? $"{source}_reversed_to_match_dwg_arrow" : source);
    }

    private static (double[] StartM, double[] EndM, bool Reversed) OrientSectionSketchLineByDwgArrow(
        double[] startM,
        double[] endM,
        SectionCutSymbol symbol)
    {
        var arrow = Normalize2(symbol.AverageArrowDirection);
        if (arrow == null)
            return (startM, endM, false);

        var defaultDirection = ComputeSectionLineDefaultArrowDirection(startM, endM);
        if (defaultDirection == null)
            return (startM, endM, false);

        var dot = defaultDirection[0] * arrow[0] + defaultDirection[1] * arrow[1];
        return dot < 0
            ? (endM, startM, true)
            : (startM, endM, false);
    }

    private static double[]? ComputeSectionLineDefaultArrowDirection(double[] startM, double[] endM)
    {
        var dx = endM[0] - startM[0];
        var dy = endM[1] - startM[1];
        var length = Math.Sqrt(dx * dx + dy * dy);
        if (length <= 1e-9)
            return null;

        // SolidWorks follows the section line orientation; use the left normal
        // as the automatic direction and reverse the line when DWG arrows disagree.
        return [-dy / length, dx / length];
    }

    private static double[]? Normalize2(IReadOnlyList<double> vector)
    {
        if (vector.Count < 2)
            return null;
        var x = vector[0];
        var y = vector[1];
        var length = Math.Sqrt(x * x + y * y);
        return length <= 1e-9 ? null : [x / length, y / length];
    }

    private static double[] ComputeSectionViewPlacement(SectionCutSymbol symbol, double offsetM)
    {
        var center = symbol.CutLine.Center.Count >= 2
            ? symbol.CutLine.Center
            : [
                (symbol.CutLine.Start[0] + symbol.CutLine.End[0]) / 2.0,
                (symbol.CutLine.Start[1] + symbol.CutLine.End[1]) / 2.0
            ];
        var midX = center[0] / 1000.0;
        var midY = center[1] / 1000.0;
        var arrow = symbol.AverageArrowDirection;
        var ax = arrow.Count >= 2 ? arrow[0] : 0.0;
        var ay = arrow.Count >= 2 ? arrow[1] : 0.0;

        if (string.Equals(symbol.CutLine.Direction, "vertical", StringComparison.OrdinalIgnoreCase))
            return [midX + Math.Sign(ax == 0 ? 1.0 : ax) * offsetM, midY];
        if (string.Equals(symbol.CutLine.Direction, "horizontal", StringComparison.OrdinalIgnoreCase))
            return [midX, midY + Math.Sign(ay == 0 ? 1.0 : ay) * offsetM];

        var length = Math.Sqrt(ax * ax + ay * ay);
        if (length <= 1e-9)
            return [midX + offsetM, midY];
        return [midX + ax / length * offsetM, midY + ay / length * offsetM];
    }

    private static int BuildSectionViewOptions(SectionCutSymbol symbol, CandidateRequest request)
    {
        var options = (int)swCreateSectionViewAtOptions_e.swCreateSectionView_ScaleWithModel;
        if (request.SectionViewNotAligned)
            options |= (int)swCreateSectionViewAtOptions_e.swCreateSectionView_NotAligned;
        if (ShouldChangeSectionDirection(symbol, request))
            options |= (int)swCreateSectionViewAtOptions_e.swCreateSectionView_ChangeDirection;
        return options;
    }

    private static bool ShouldChangeSectionDirection(SectionCutSymbol symbol, CandidateRequest request)
    {
        if (request.SectionViewChangeDirection != null)
            return request.SectionViewChangeDirection.Value;

        return false;
    }

    private static List<SectionCutSymbol> ReadSectionCutSymbols(string path)
    {
        using var document = JsonDocument.Parse(File.ReadAllText(path));
        if (document.RootElement.ValueKind != JsonValueKind.Array)
            throw new InvalidOperationException($"Section cut symbols JSON root must be an array: {path}");

        var result = new List<SectionCutSymbol>();
        foreach (var element in document.RootElement.EnumerateArray())
        {
            var symbol = new SectionCutSymbol
            {
                Ok = ReadBool(element, "ok"),
                Kind = ReadString(element, "kind"),
                Label = ReadString(element, "label"),
                ParentHandle = ReadString(element, "parent_handle"),
                ViewKind = ReadString(element, "view_kind"),
                CutLine = ReadSectionCutLine(element)
            };
            symbol.AverageArrowDirection = ReadAverageArrowDirection(element);
            result.Add(symbol);
        }

        return result;
    }

    private static SectionCutLine ReadSectionCutLine(JsonElement element)
    {
        if (!element.TryGetProperty("cut_line", out var cutLine))
            throw new InvalidOperationException("Section cut symbol is missing cut_line.");
        return new SectionCutLine
        {
            Start = ReadPoint(cutLine, "start"),
            End = ReadPoint(cutLine, "end"),
            Center = ReadPoint(cutLine, "center"),
            Direction = ReadString(cutLine, "direction")
        };
    }

    private static List<double> ReadAverageArrowDirection(JsonElement element)
    {
        if (!element.TryGetProperty("markers", out var markers) || markers.ValueKind != JsonValueKind.Array)
            return [0.0, 0.0];

        var vectors = markers.EnumerateArray()
            .Where(marker => marker.TryGetProperty("arrow_direction", out var direction) && direction.ValueKind == JsonValueKind.Array)
            .Select(marker => ReadPoint(marker, "arrow_direction"))
            .Where(point => point.Count >= 2)
            .ToList();
        if (vectors.Count == 0)
            return [0.0, 0.0];

        return
        [
            vectors.Average(vector => vector[0]),
            vectors.Average(vector => vector[1])
        ];
    }

    private static bool ReadBool(JsonElement element, string propertyName)
    {
        return element.TryGetProperty(propertyName, out var value) &&
               value.ValueKind == JsonValueKind.True;
    }

    private static string ReadString(JsonElement element, string propertyName)
    {
        return element.TryGetProperty(propertyName, out var value) && value.ValueKind == JsonValueKind.String
            ? value.GetString() ?? ""
            : "";
    }

    private static List<double> ReadPoint(JsonElement element, string propertyName)
    {
        if (!element.TryGetProperty(propertyName, out var value) || value.ValueKind != JsonValueKind.Array)
            return [];
        return value.EnumerateArray()
            .Where(item => item.ValueKind == JsonValueKind.Number)
            .Select(item => item.GetDouble())
            .ToList();
    }

    private static (double X, double Y) ReadSignatureOrigin(string path, string label)
    {
        if (string.IsNullOrWhiteSpace(path))
            throw new ArgumentException($"缺少 {label} signature path。");
        if (!File.Exists(path))
            throw new FileNotFoundException($"{label} signature 不存在: {path}");

        using var document = JsonDocument.Parse(File.ReadAllText(path));
        if (!document.RootElement.TryGetProperty("relative_line_coordinates", out var relative) ||
            !relative.TryGetProperty("origin", out var origin) ||
            !origin.TryGetProperty("x", out var xElement) ||
            !origin.TryGetProperty("y", out var yElement))
            throw new InvalidOperationException($"{label} signature 缺少 relative_line_coordinates.origin: {path}");

        return (xElement.GetDouble(), yElement.GetDouble());
    }

    private static View? FindMainDrawingView(DrawingDoc drawing, CandidateRequest request)
    {
        var viewObj = drawing.GetFirstView();
        while (viewObj is View view)
        {
            var name = Safe(() => view.GetName2(), "");
            var orientationName = Safe(() => view.GetOrientationName(), "");
            if (!string.IsNullOrWhiteSpace(request.MainViewOrientationName))
            {
                if (string.Equals(orientationName, request.MainViewOrientationName, StringComparison.OrdinalIgnoreCase) ||
                    string.Equals(name, request.MainViewOrientationName, StringComparison.OrdinalIgnoreCase))
                    return view;
            }
            else if (LooksLikeCandidateView(name, orientationName, request.MainViewNamePrefix))
            {
                return view;
            }

            viewObj = view.GetNextView();
        }

        return null;
    }

    private static void CreateNamedViews(ModelDoc2 model, object mathUtility, List<ViewCandidate> candidates, bool keepNamedViews)
    {
        var activeView = model.IActiveView as ModelView ?? model.ActiveView as ModelView
            ?? throw new InvalidOperationException("模型没有可用 ActiveView，无法设置 Orientation3。");

        foreach (var candidate in candidates)
        {
            if (keepNamedViews)
                Safe(() => model.DeleteNamedView(candidate.Name), false);
            else
                Safe(() => model.DeleteNamedView(candidate.Name), false);

            var transformData = new double[]
            {
                candidate.BasisU[0], candidate.BasisU[1], candidate.BasisU[2],
                candidate.BasisV[0], candidate.BasisV[1], candidate.BasisV[2],
                candidate.BasisD[0], candidate.BasisD[1], candidate.BasisD[2],
                0.0, 0.0, 0.0,
                1.0,
                0.0, 0.0, 0.0
            };

            dynamic utility = mathUtility;
            MathTransform transform = utility.CreateTransform(transformData);
            activeView.Orientation3 = transform;
            model.GraphicsRedraw2();
            model.NameView(candidate.Name);
        }
    }

    private static void ExportCandidateDwgsSequential(
        ModelDoc2 drawingModel,
        DrawingDoc drawing,
        string modelPath,
        List<ViewCandidate> candidates,
        CandidateRequest request)
    {
        var dwgDir = CandidateDwgDirectory(request);
        Directory.CreateDirectory(dwgDir);

        foreach (var candidate in candidates)
        {
            var dwgPath = Path.Combine(dwgDir, $"{candidate.Index:00}_{SanitizeFileName(candidate.Name)}.dwg");
            ExportSingleCandidateDwgUntilSaved(drawingModel, drawing, modelPath, candidate, request, dwgPath);
        }

        if (!request.KeepExportedCandidateView)
            CleanupCandidateDrawingViews(drawingModel, drawing, request.ViewNamePrefix);
    }

    private static void ExportSingleCandidateDwgUntilSaved(
        ModelDoc2 drawingModel,
        DrawingDoc drawing,
        string modelPath,
        ViewCandidate candidate,
        CandidateRequest request,
        string dwgPath)
    {
        var rebuildAttempt = 0;
        var maxRebuildAttempts = Math.Max(0, request.CandidateRebuildRetryCount);

        while (maxRebuildAttempts == 0 || rebuildAttempt < maxRebuildAttempts)
        {
            rebuildAttempt++;
            candidate.DwgRebuildAttempts = rebuildAttempt;
            candidate.DwgPath = dwgPath;

            CleanupCandidateDrawingViews(drawingModel, drawing, request.ViewNamePrefix);
            var view = InsertSingleCandidateView(drawing, modelPath, candidate, request);
            if (view == null)
            {
                candidate.DwgSaveOk = false;
                candidate.DwgSaveErrors = -1;
                candidate.DwgSaveWarnings = 0;
                candidate.DwgSaveMessage = $"第 {rebuildAttempt} 轮重建失败：CreateDrawViewFromModelView3 返回空。";
                WaitBeforeNextCandidateAttempt(request);
                continue;
            }

            Safe(() => drawingModel.ForceRebuild3(false), false);
            WaitBeforeNextCandidateAttempt(request);

            var saveResult = SaveAsWithRetries(drawingModel, dwgPath, request.SaveRetryCount, request.SaveRetryDelayMs);
            candidate.DwgSaveOk = saveResult.Ok;
            candidate.DwgSaveErrors = saveResult.Errors;
            candidate.DwgSaveWarnings = saveResult.Warnings;

            if (saveResult.Ok)
            {
                candidate.DwgSaveMessage = $"第 {rebuildAttempt} 轮重建后保存成功。";
                if (!request.KeepExportedCandidateView)
                    DeleteDrawingView(drawingModel, candidate);
                return;
            }

            candidate.DwgSaveMessage = $"第 {rebuildAttempt} 轮重建后保存失败：errors={saveResult.Errors}, warnings={saveResult.Warnings}。";
            Console.Error.WriteLine(
                $"候选 {candidate.Index:00} 保存失败，准备重建后重试：attempt={rebuildAttempt}, errors={saveResult.Errors}, warnings={saveResult.Warnings}");

            if (!request.KeepExportedCandidateView)
                DeleteDrawingView(drawingModel, candidate);

            WaitBeforeNextCandidateAttempt(request);
        }

        candidate.DwgSaveOk = false;
        candidate.DwgSaveMessage = $"达到候选视图重建重试上限 {maxRebuildAttempts}，仍未保存成功。";
        throw new InvalidOperationException($"候选 {candidate.Index:00} DWG 保存失败，已达到重建重试上限 {maxRebuildAttempts}。");
    }

    private static void WaitBeforeNextCandidateAttempt(CandidateRequest request)
    {
        if (request.SaveRetryDelayMs > 0)
            Thread.Sleep(request.SaveRetryDelayMs);
    }

    private static View? InsertSingleCandidateView(DrawingDoc drawing, string modelPath, ViewCandidate candidate, CandidateRequest request)
    {
        var x = request.SingleViewXM ?? request.SheetWidthM / 2.0;
        var y = request.SingleViewYM ?? request.SheetHeightM / 2.0;
        var view = drawing.CreateDrawViewFromModelView3(modelPath, candidate.Name, x, y, 0);
        if (view == null)
        {
            candidate.Inserted = false;
            candidate.InsertError = "CreateDrawViewFromModelView3 returned null.";
            return null;
        }

        if (request.ViewScale > 0)
            Safe(() => { view.UseSheetScale = 0; view.ScaleDecimal = request.ViewScale; return true; }, false);

        candidate.Inserted = true;
        candidate.DrawingViewName = Safe(() => view.GetName2(), "");
        candidate.OrientationName = Safe(() => view.GetOrientationName(), "");
        candidate.Position = DoubleList(Safe(() => view.Position, null));
        candidate.ScaleDecimal = Safe(() => view.ScaleDecimal, 0.0);
        candidate.UseSheetScale = Safe(() => view.UseSheetScale, 0);
        return view;
    }

    private static string CandidateDwgDirectory(CandidateRequest request)
    {
        return string.IsNullOrWhiteSpace(request.CandidateDwgDir)
            ? Path.Combine(request.OutputDir, "candidate-dwgs")
            : request.CandidateDwgDir;
    }

    private static void CleanupCandidateDrawingViews(ModelDoc2 drawingModel, DrawingDoc drawing, string prefix)
    {
        var toDelete = new List<ViewCandidate>();
        var viewObj = drawing.GetFirstView();
        while (viewObj is View view)
        {
            var name = Safe(() => view.GetName2(), "");
            var orientationName = Safe(() => view.GetOrientationName(), "");
            if (LooksLikeCandidateView(name, orientationName, prefix))
            {
                toDelete.Add(new ViewCandidate
                {
                    DrawingViewName = name,
                    OrientationName = orientationName
                });
            }
            viewObj = view.GetNextView();
        }

        foreach (var candidate in toDelete)
            DeleteDrawingView(drawingModel, candidate);
    }

    private static bool LooksLikeCandidateView(string viewName, string orientationName, string prefix)
    {
        return (!string.IsNullOrWhiteSpace(orientationName) && orientationName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) ||
               (!string.IsNullOrWhiteSpace(viewName) && viewName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
    }

    private static bool DeleteDrawingView(ModelDoc2 drawingModel, ViewCandidate candidate)
    {
        var names = new[]
        {
            candidate.DrawingViewName,
            candidate.OrientationName,
            candidate.Name
        };

        foreach (var name in names.Where(item => !string.IsNullOrWhiteSpace(item)).Distinct(StringComparer.OrdinalIgnoreCase))
        {
            drawingModel.ClearSelection2(true);
            var selected = Safe(() => drawingModel.Extension.SelectByID2(name, "DRAWINGVIEW", 0, 0, 0, false, 0, null, 0), false);
            if (!selected)
                continue;
            var deleted = Safe(() => drawingModel.Extension.DeleteSelection2(0), false);
            drawingModel.ClearSelection2(true);
            if (deleted)
                return true;
        }

        return false;
    }

    private static SaveResult SaveAs(ModelDoc2 model, string path)
    {
        var errors = 0;
        var warnings = 0;
        Directory.CreateDirectory(Path.GetDirectoryName(path) ?? System.Environment.CurrentDirectory);
        var ok = Safe(() => model.SaveAs4(
            path,
            (int)swSaveAsVersion_e.swSaveAsCurrentVersion,
            (int)swSaveAsOptions_e.swSaveAsOptions_Silent,
            ref errors,
            ref warnings), false);
        if (!ok && !File.Exists(path))
        {
            errors = 0;
            warnings = 0;
            ok = model.Extension.SaveAs(
                path,
                (int)swSaveAsVersion_e.swSaveAsCurrentVersion,
                (int)swSaveAsOptions_e.swSaveAsOptions_Silent,
                null,
                ref errors,
                ref warnings);
        }
        return new SaveResult(ok || File.Exists(path), errors, warnings);
    }

    private static SaveResult SaveAsWithRetries(ModelDoc2 model, string path, int retryCount, int retryDelayMs)
    {
        retryCount = Math.Max(1, retryCount);
        retryDelayMs = Math.Max(0, retryDelayMs);
        var last = new SaveResult(false, 0, 0);
        for (var attempt = 1; attempt <= retryCount; attempt++)
        {
            Safe(() => model.ForceRebuild3(false), false);
            if (attempt > 1 && retryDelayMs > 0)
                Thread.Sleep(retryDelayMs);

            last = SaveAs(model, path);
            if (last.Ok)
                return last;
        }
        return last;
    }

    private static void DeleteNamedViews(ModelDoc2 model, List<ViewCandidate> candidates)
    {
        foreach (var candidate in candidates)
            Safe(() => model.DeleteNamedView(candidate.Name), false);
    }

    private static (double[] U, double[] V, double[] D) BuildBasis(double[] direction, double rollDeg)
    {
        var d = Normalize(direction);
        var worldUp = Math.Abs(Dot(d, [0.0, 1.0, 0.0])) > 0.92
            ? new[] { 0.0, 0.0, 1.0 }
            : new[] { 0.0, 1.0, 0.0 };
        var u = Normalize(Cross(worldUp, d));
        var v = Normalize(Cross(d, u));

        var rad = rollDeg * Math.PI / 180.0;
        var cos = Math.Cos(rad);
        var sin = Math.Sin(rad);
        var rolledU = Add(Scale(u, cos), Scale(v, sin));
        var rolledV = Add(Scale(u, -sin), Scale(v, cos));
        return (Normalize(rolledU), Normalize(rolledV), d);
    }

    private static double[] Normalize(double[] v)
    {
        var norm = Math.Sqrt(Dot(v, v));
        if (norm <= 1e-12)
            return [0, 0, 1];
        return [v[0] / norm, v[1] / norm, v[2] / norm];
    }

    private static double Dot(double[] a, double[] b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];

    private static double[] Cross(double[] a, double[] b)
    {
        return
        [
            a[1] * b[2] - a[2] * b[1],
            a[2] * b[0] - a[0] * b[2],
            a[0] * b[1] - a[1] * b[0]
        ];
    }

    private static double[] Add(double[] a, double[] b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];

    private static double[] Scale(double[] a, double k) => [a[0] * k, a[1] * k, a[2] * k];

    private static List<double> DoubleList(object? value)
    {
        if (value == null)
            return [];
        if (value is double[] doubles)
            return doubles.ToList();
        if (value is object[] objects)
            return objects.SelectMany(DoubleList).ToList();
        if (value is Array array)
        {
            var result = new List<double>();
            foreach (var item in array)
            {
                if (item == null)
                    continue;
                if (double.TryParse(Convert.ToString(item, CultureInfo.InvariantCulture), NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed))
                    result.Add(parsed);
            }
            return result;
        }
        return [];
    }

    private static string NormalizePath(string? path)
    {
        if (string.IsNullOrWhiteSpace(path))
            return "";
        return Path.GetFullPath(System.Environment.ExpandEnvironmentVariables(path.Trim().Trim('"')));
    }

    private static string SanitizeFileName(string name)
    {
        var value = Path.GetFileNameWithoutExtension(name);
        if (string.IsNullOrWhiteSpace(value))
            value = "solidworks_view_candidates";
        foreach (var c in Path.GetInvalidFileNameChars())
            value = value.Replace(c, '_');
        return value;
    }

    private static string FirstNonEmpty(params string?[] values)
    {
        foreach (var value in values)
        {
            if (!string.IsNullOrWhiteSpace(value))
                return value.Trim();
        }
        return "";
    }

    private static void WriteJson(string path, object data)
    {
        Directory.CreateDirectory(Path.GetDirectoryName(path) ?? System.Environment.CurrentDirectory);
        File.WriteAllText(path, JsonSerializer.Serialize(data, JsonOptions));
    }

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

internal sealed class CandidateRequest
{
    public string ModelPath { get; set; } = "";
    public string DrawingTemplatePath { get; set; } = "";
    public string OutputDir { get; set; } = "";
    public string CandidateDwgDir { get; set; } = "";
    public string RankingJsonPath { get; set; } = "";
    public string StudentSignaturePath { get; set; } = "";
    public string ReferenceSignaturePath { get; set; } = "";
    public string StudentViewRegionsPath { get; set; } = "";
    public string ReferenceViewRegionsPath { get; set; } = "";
    public string OutputDwgPath { get; set; } = "";
    public string SectionCutSymbolsPath { get; set; } = "";
    public string ViewNamePrefix { get; set; } = "agent4_candidate";
    public string MainViewNamePrefix { get; set; } = "agent4_main_view";
    public string MainViewOrientationName { get; set; } = "";
    public string AlignmentAxis { get; set; } = "xy";
    public string ProjectedViewName { get; set; } = "agent4_bottom_projected_view";
    public string ProjectedViewDirection { get; set; } = "down";
    public string SectionLabel { get; set; } = "";
    public string SectionViewNamePrefix { get; set; } = "agent4_section_view";
    public double SheetWidthM { get; set; } = 0.594;
    public double SheetHeightM { get; set; } = 0.420;
    public int? CandidateIndex { get; set; }
    public double ViewScale { get; set; } = 1.0;
    public double? SingleViewXM { get; set; }
    public double? SingleViewYM { get; set; }
    public double? ProjectedViewXM { get; set; }
    public double? ProjectedViewYM { get; set; }
    public double SectionViewOffsetM { get; set; } = 0.18;
    public double SectionDepthM { get; set; } = 0.0;
    public int SaveRetryCount { get; set; } = 3;
    public int SaveRetryDelayMs { get; set; } = 1000;
    public int CandidateRebuildRetryCount { get; set; } = 0;
    public bool KeepNamedViews { get; set; }
    public bool UseActiveDrawing { get; set; }
    public bool UseActiveModel { get; set; }
    public bool ExportCandidateDwgsSequential { get; set; }
    public bool ExportActiveDrawingDwg { get; set; }
    public bool ListDrawingViews { get; set; }
    public bool KeepExportedCandidateView { get; set; }
    public bool PlaceMainView { get; set; }
    public bool PlaceProjectedView { get; set; }
    public bool PlaceSectionViews { get; set; }
    public bool AlignMainViewOrigin { get; set; }
    public bool AlignProjectedViewsToRegions { get; set; }
    public bool AlignSectionViewsToRegions { get; set; }
    public bool ReplaceExistingMainView { get; set; } = true;
    public bool ReplaceExistingProjectedView { get; set; } = true;
    public bool ReplaceExistingSectionViews { get; set; } = true;
    public bool SectionViewNotAligned { get; set; }
    public bool? SectionViewChangeDirection { get; set; }

    public static CandidateRequest Parse(string[] args)
    {
        var request = new CandidateRequest();
        for (var i = 0; i < args.Length; i++)
        {
            var arg = args[i];
            string Next() => i + 1 < args.Length ? args[++i] : throw new ArgumentException($"缺少参数值: {arg}");
            switch (arg)
            {
                case "--request-json":
                    request = JsonSerializer.Deserialize<CandidateRequest>(File.ReadAllText(Next()), new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
                        ?? new CandidateRequest();
                    break;
                case "--model":
                    request.ModelPath = Next();
                    break;
                case "--template":
                    request.DrawingTemplatePath = Next();
                    break;
                case "--output-dir":
                    request.OutputDir = Next();
                    break;
                case "--candidate-dwg-dir":
                    request.CandidateDwgDir = Next();
                    break;
                case "--ranking-json":
                    request.RankingJsonPath = Next();
                    break;
                case "--student-signature":
                    request.StudentSignaturePath = Next();
                    break;
                case "--reference-signature":
                    request.ReferenceSignaturePath = Next();
                    break;
                case "--student-view-regions":
                    request.StudentViewRegionsPath = Next();
                    break;
                case "--reference-view-regions":
                    request.ReferenceViewRegionsPath = Next();
                    break;
                case "--dwg":
                    request.OutputDwgPath = Next();
                    break;
                case "--section-cut-symbols":
                    request.SectionCutSymbolsPath = Next();
                    break;
                case "--view-name-prefix":
                    request.ViewNamePrefix = Next();
                    break;
                case "--main-view-name-prefix":
                    request.MainViewNamePrefix = Next();
                    break;
                case "--main-view-orientation-name":
                    request.MainViewOrientationName = Next();
                    break;
                case "--alignment-axis":
                    request.AlignmentAxis = Next();
                    break;
                case "--projected-view-name":
                    request.ProjectedViewName = Next();
                    break;
                case "--projected-view-direction":
                    request.ProjectedViewDirection = Next();
                    break;
                case "--section-label":
                    request.SectionLabel = Next();
                    break;
                case "--section-view-name-prefix":
                    request.SectionViewNamePrefix = Next();
                    break;
                case "--sheet-width-m":
                    request.SheetWidthM = double.Parse(Next(), CultureInfo.InvariantCulture);
                    break;
                case "--sheet-height-m":
                    request.SheetHeightM = double.Parse(Next(), CultureInfo.InvariantCulture);
                    break;
                case "--candidate-index":
                    request.CandidateIndex = int.Parse(Next(), CultureInfo.InvariantCulture);
                    break;
                case "--view-scale":
                    request.ViewScale = double.Parse(Next(), CultureInfo.InvariantCulture);
                    break;
                case "--single-view-x-m":
                    request.SingleViewXM = double.Parse(Next(), CultureInfo.InvariantCulture);
                    break;
                case "--single-view-y-m":
                    request.SingleViewYM = double.Parse(Next(), CultureInfo.InvariantCulture);
                    break;
                case "--projected-view-x-m":
                    request.ProjectedViewXM = double.Parse(Next(), CultureInfo.InvariantCulture);
                    break;
                case "--projected-view-y-m":
                    request.ProjectedViewYM = double.Parse(Next(), CultureInfo.InvariantCulture);
                    break;
                case "--section-view-offset-m":
                    request.SectionViewOffsetM = double.Parse(Next(), CultureInfo.InvariantCulture);
                    break;
                case "--section-depth-m":
                    request.SectionDepthM = double.Parse(Next(), CultureInfo.InvariantCulture);
                    break;
                case "--save-retry-count":
                    request.SaveRetryCount = int.Parse(Next(), CultureInfo.InvariantCulture);
                    break;
                case "--save-retry-delay-ms":
                    request.SaveRetryDelayMs = int.Parse(Next(), CultureInfo.InvariantCulture);
                    break;
                case "--candidate-rebuild-retry-count":
                    request.CandidateRebuildRetryCount = int.Parse(Next(), CultureInfo.InvariantCulture);
                    break;
                case "--keep-named-views":
                    request.KeepNamedViews = true;
                    break;
                case "--use-active-drawing":
                    request.UseActiveDrawing = true;
                    break;
                case "--use-active-model":
                    request.UseActiveModel = true;
                    break;
                case "--export-candidate-dwgs-sequential":
                    request.ExportCandidateDwgsSequential = true;
                    break;
                case "--export-active-drawing-dwg":
                    request.ExportActiveDrawingDwg = true;
                    break;
                case "--list-drawing-views":
                    request.ListDrawingViews = true;
                    break;
                case "--keep-exported-candidate-view":
                    request.KeepExportedCandidateView = true;
                    break;
                case "--place-main-view":
                    request.PlaceMainView = true;
                    break;
                case "--place-projected-view":
                    request.PlaceProjectedView = true;
                    break;
                case "--place-section-views":
                    request.PlaceSectionViews = true;
                    break;
                case "--align-main-view-origin":
                    request.AlignMainViewOrigin = true;
                    break;
                case "--align-projected-views-to-regions":
                case "--align-orthographic-views-to-regions":
                    request.AlignProjectedViewsToRegions = true;
                    break;
                case "--align-section-views-to-regions":
                    request.AlignSectionViewsToRegions = true;
                    break;
                case "--keep-existing-main-view":
                    request.ReplaceExistingMainView = false;
                    break;
                case "--keep-existing-projected-view":
                    request.ReplaceExistingProjectedView = false;
                    break;
                case "--keep-existing-section-views":
                    request.ReplaceExistingSectionViews = false;
                    break;
                case "--section-view-not-aligned":
                    request.SectionViewNotAligned = true;
                    break;
                case "--section-view-change-direction":
                    request.SectionViewChangeDirection = true;
                    break;
                case "--section-view-keep-direction":
                    request.SectionViewChangeDirection = false;
                    break;
                default:
                    throw new ArgumentException($"未知参数: {arg}");
            }
        }
        return request;
    }
}

internal sealed class CandidateResult
{
    public bool Ok { get; set; }
    public string Message { get; set; } = "";
    public string ModelPath { get; set; } = "";
    public string DrawingTemplatePath { get; set; } = "";
    public string DrawingPath { get; set; } = "";
    public bool UsedActiveDrawing { get; set; }
    public string OutputDir { get; set; } = "";
    public DateTimeOffset StartedAt { get; set; }
    public DateTimeOffset CompletedAt { get; set; }
    public List<ViewCandidate> Candidates { get; set; } = [];
    public Dictionary<string, string> Artifacts { get; set; } = new(StringComparer.OrdinalIgnoreCase);
    public List<string> SourceApis { get; set; } = [];
}

internal sealed class ViewCandidate
{
    public int Index { get; set; }
    public string Name { get; set; } = "";
    public string DirectionName { get; set; } = "";
    public int RollDeg { get; set; }
    public List<double> ViewDirection { get; set; } = [];
    public List<double> BasisU { get; set; } = [];
    public List<double> BasisV { get; set; } = [];
    public List<double> BasisD { get; set; } = [];
    public bool Inserted { get; set; }
    public string InsertError { get; set; } = "";
    public string DrawingViewName { get; set; } = "";
    public string OrientationName { get; set; } = "";
    public List<double> Position { get; set; } = [];
    public double ScaleDecimal { get; set; }
    public int UseSheetScale { get; set; }
    public string DwgPath { get; set; } = "";
    public bool DwgSaveOk { get; set; }
    public int DwgSaveErrors { get; set; }
    public int DwgSaveWarnings { get; set; }
    public int DwgRebuildAttempts { get; set; }
    public string DwgSaveMessage { get; set; } = "";
}

internal sealed record DrawingContext(ModelDoc2 Model, DrawingDoc Drawing, string TemplatePath, string DrawingPath);

internal sealed record SaveResult(bool Ok, int Errors, int Warnings);

internal sealed class SectionCutSymbol
{
    public bool Ok { get; set; }
    public string Kind { get; set; } = "";
    public string Label { get; set; } = "";
    public string ParentHandle { get; set; } = "";
    public string ViewKind { get; set; } = "";
    public SectionCutLine CutLine { get; set; } = new();
    public List<double> AverageArrowDirection { get; set; } = [];
}

internal sealed class SectionCutLine
{
    public List<double> Start { get; set; } = [];
    public List<double> End { get; set; } = [];
    public List<double> Center { get; set; } = [];
    public string Direction { get; set; } = "";
}

internal sealed record SectionRegionCenter(string Label, double CenterX, double CenterY);

internal sealed record OrthographicRegionCenter(string Kind, double CenterX, double CenterY);

internal sealed record SectionRegionDelta(double DeltaMmX, double DeltaMmY, double AppliedDeltaMmX, double AppliedDeltaMmY);

internal sealed record SectionSketchLine(
    List<double> StartMm,
    List<double> EndMm,
    List<double> CenterMm,
    double[] StartM,
    double[] EndM,
    double[] CenterM,
    string Source);
