using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;

var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile(
    Path.Combine(builder.Environment.ContentRootPath, "backend-csharp", "Agent4.Api", "appsettings.Local.json"),
    optional: true,
    reloadOnChange: true);
builder.WebHost.UseUrls(builder.Configuration["urls"] ?? "http://127.0.0.1:7200");

builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
    options.SerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
    options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
});

builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(policy =>
    {
        policy
            .WithOrigins(
                "http://localhost:5173",
                "http://127.0.0.1:5173",
                "http://localhost:5174",
                "http://127.0.0.1:5174",
                "http://localhost:5175",
                "http://127.0.0.1:5175")
            .AllowAnyHeader()
            .AllowAnyMethod()
            .AllowCredentials();
    });
});

builder.Services.AddSingleton<AppPaths>();
builder.Services.AddSingleton<KnowledgeRepository>();
builder.Services.AddSingleton<SessionStore>();
builder.Services.AddSingleton<LessonBuilder>();
builder.Services.AddSingleton<TeachingLanguageDesigner>();
builder.Services.AddSingleton<TeachingIntroductionBuilder>();
builder.Services.AddSingleton<FeatureGrader>();
builder.Services.AddSingleton<SolidWorksExtractor>();
builder.Services.AddSingleton<SolidWorksConnectionService>();
builder.Services.AddMechanicalDiagnostics();
builder.Services.AddDwgDraft();
builder.Services.AddDrawingAssemblyDiagnostics();

var app = builder.Build();
app.UseCors();
app.MapMechanicalDiagnosticApi();
app.MapDwgDraftApi();
app.MapDrawingAssemblyDiagnosticApi();

app.MapGet("/health", (KnowledgeRepository knowledge, AppPaths paths) => new
{
    ok = true,
    service = "Agent4 C# SolidWorks Learning API",
    standards = knowledge.ListStandards().Count,
    backend = "csharp",
    software_paths = paths.ReadProjectPaths(),
});

app.MapGet("/api/diagnostic-flows", (AppPaths paths) => Results.Ok(DiagnosticFlowCatalog.Build(paths)));

app.MapPost("/api/knowledge/reload", (KnowledgeRepository knowledge) =>
{
    knowledge.Reload();
    return Results.Ok(new { ok = true, standards = knowledge.ListStandards().Count });
});

app.MapGet("/api/standards", (KnowledgeRepository knowledge, string? query, string? kind) =>
{
    var items = knowledge.ListStandards(query ?? "", kind ?? "");
    return Results.Ok(new { items });
});

app.MapGet("/api/standards/{standardId}", (KnowledgeRepository knowledge, string standardId) =>
{
    return knowledge.TryGet(standardId, out var standard)
        ? Results.Ok(standard)
        : Results.NotFound(new { detail = $"standard answer not found: {standardId}" });
});

app.MapGet("/api/teaching/design/{standardId}", (
    KnowledgeRepository knowledge,
    TeachingLanguageDesigner designer,
    string standardId) =>
{
    if (!knowledge.TryGet(standardId, out var standard))
        return Results.NotFound(new { detail = $"standard answer not found: {standardId}" });

    return Results.Ok(designer.Design(standard));
});

app.MapGet("/api/teaching/introduction/{standardId}", (
    KnowledgeRepository knowledge,
    TeachingIntroductionBuilder introductionBuilder,
    string standardId) =>
{
    if (!knowledge.TryGet(standardId, out var standard))
        return Results.NotFound(new { detail = $"standard answer not found: {standardId}" });

    return Results.Ok(introductionBuilder.Build(standard));
});

app.MapPost("/api/teaching/introduction/{standardId}/open-standard-part", (
    KnowledgeRepository knowledge,
    TeachingIntroductionBuilder introductionBuilder,
    SolidWorksConnectionService solidWorks,
    string standardId) =>
{
    if (!knowledge.TryGet(standardId, out var standard))
        return Results.NotFound(new { detail = $"standard answer not found: {standardId}" });

    var introduction = introductionBuilder.Build(standard);
    if (string.IsNullOrWhiteSpace(introduction.StandardPartPath) || !File.Exists(introduction.StandardPartPath))
        return Results.NotFound(new { detail = $"standard part file not found: {introduction.StandardPartPath}" });

    try
    {
        var result = solidWorks.OpenDocument(introduction.StandardPartPath);
        return Results.Ok(new { ok = result.Ok, message = result.Message, part_path = introduction.StandardPartPath });
    }
    catch (Exception ex)
    {
        return Results.Problem($"打开标准零件失败：{ex.Message}", statusCode: 500);
    }
});

app.MapPost("/api/teaching/introduction/{standardId}/highlight/{targetId}", (
    KnowledgeRepository knowledge,
    TeachingIntroductionBuilder introductionBuilder,
    SolidWorksConnectionService solidWorks,
    string standardId,
    string targetId) =>
{
    if (!knowledge.TryGet(standardId, out var standard))
        return Results.NotFound(new { detail = $"standard answer not found: {standardId}" });

    var introduction = introductionBuilder.Build(standard);
    if (string.IsNullOrWhiteSpace(introduction.StandardPartPath) || !File.Exists(introduction.StandardPartPath))
        return Results.NotFound(new { detail = $"standard part file not found: {introduction.StandardPartPath}" });

    var target = introduction.HighlightTargets.FirstOrDefault(item => string.Equals(item.Id, targetId, StringComparison.OrdinalIgnoreCase));
    if (target == null)
        return Results.NotFound(new { detail = $"highlight target not found: {targetId}" });

    try
    {
        return Results.Ok(solidWorks.HighlightTarget(introduction.StandardPartPath, target));
    }
    catch (Exception ex)
    {
        return Results.Problem($"标准零件高亮失败：{ex.Message}", statusCode: 500);
    }
});

app.MapPost("/api/teaching/sessions", (
    StartSessionRequest request,
    KnowledgeRepository knowledge,
    SessionStore sessions,
    LessonBuilder lessonBuilder,
    SolidWorksConnectionService solidWorks) =>
{
    if (!knowledge.TryGet(request.StandardId, out var standard))
        return Results.NotFound(new { detail = $"standard answer not found: {request.StandardId}" });

    try
    {
        solidWorks.EnsureConnected();
        return Results.Ok(sessions.Start("teaching", standard, lessonBuilder));
    }
    catch (Exception ex)
    {
        return Results.Problem($"SolidWorks 启动或连接失败：{ex.Message}", statusCode: 500);
    }
});

app.MapPost("/api/testing/sessions", (
    StartSessionRequest request,
    KnowledgeRepository knowledge,
    SessionStore sessions,
    LessonBuilder lessonBuilder,
    SolidWorksConnectionService solidWorks) =>
{
    if (!knowledge.TryGet(request.StandardId, out var standard))
        return Results.NotFound(new { detail = $"standard answer not found: {request.StandardId}" });

    try
    {
        solidWorks.EnsureConnected();
        return Results.Ok(sessions.Start("testing", standard, lessonBuilder));
    }
    catch (Exception ex)
    {
        return Results.Problem($"SolidWorks 启动或连接失败：{ex.Message}", statusCode: 500);
    }
});

app.MapPost("/api/solidworks/connect", (SolidWorksConnectionService solidWorks) =>
{
    try
    {
        return Results.Ok(solidWorks.EnsureConnected());
    }
    catch (Exception ex)
    {
        return Results.Problem($"SolidWorks 启动或连接失败：{ex.Message}", statusCode: 500);
    }
});

app.MapGet("/api/solidworks/active-state", (SolidWorksConnectionService solidWorks) =>
{
    return Results.Ok(solidWorks.GetActiveDocumentStatus());
});

app.MapGet("/api/sessions/{sessionId}", (SessionStore sessions, string sessionId) =>
{
    return sessions.TryGet(sessionId, out var session)
        ? Results.Ok(session)
        : Results.NotFound(new { detail = $"session not found: {sessionId}" });
});

app.MapPost("/api/teaching/sessions/{sessionId}/observations", (
    string sessionId,
    ObservationRequest request,
    SessionStore sessions,
    FeatureGrader grader) =>
{
    if (!sessions.TryGet(sessionId, out var session))
        return Results.NotFound(new { detail = $"session not found: {sessionId}" });

    var observed = SkillFlow.NormalizeSteps(JsonSerializer.SerializeToElement(new { steps = request.Steps }));
    var result = sessions.SubmitTeachingObservation(session, observed, grader);
    return Results.Ok(result);
});

app.MapPost("/api/teaching/sessions/{sessionId}/audit-active-part", async (
    string sessionId,
    SessionStore sessions,
    SolidWorksExtractor extractor,
    FeatureGrader grader) =>
{
    if (!sessions.TryGet(sessionId, out var session))
        return Results.NotFound(new { detail = $"session not found: {sessionId}" });

    try
    {
        var observed = await extractor.AuditActivePartAsync();
        var currentIndex = session.CurrentStepIndex;
        var submitted = currentIndex < observed.Count
            ? new List<SkillStep> { observed[currentIndex] }
            : new List<SkillStep>();
        var result = sessions.SubmitTeachingObservation(session, submitted, grader);
        return Results.Ok(new
        {
            observed_steps = observed,
            submitted_steps = submitted,
            result,
        });
    }
    catch (Exception ex)
    {
        return Results.Problem(ex.Message, statusCode: 500);
    }
});

app.MapPost("/api/teaching/sessions/{sessionId}/poll-active-part", async (
    string sessionId,
    SessionStore sessions,
    SolidWorksExtractor extractor,
    FeatureGrader grader,
    SolidWorksConnectionService solidWorks) =>
{
    if (!sessions.TryGet(sessionId, out var session))
        return Results.NotFound(new { detail = $"session not found: {sessionId}" });

    try
    {
        var documentStatus = solidWorks.GetActiveDocumentStatus();
        var documentIssue = sessions.CheckActiveDocumentForCurrentStep(session, documentStatus);
        if (documentIssue != null)
        {
            if (documentIssue.Ok)
                sessions.AcceptCurrentStepFromLiveState(session, documentIssue);
            return Results.Ok(new
            {
                observed_step_count = 0,
                submitted_steps = new List<SkillStep>(),
                result = documentIssue,
                session_status = session.Status,
                current_step_index = session.CurrentStepIndex,
                active_document = documentStatus,
            });
        }

        var observed = await extractor.AuditActivePartAsync();
        var currentIndex = session.CurrentStepIndex;
        var submitted = currentIndex < observed.Count
            ? new List<SkillStep> { observed[currentIndex] }
            : new List<SkillStep>();
        var result = sessions.PollTeachingObservation(session, submitted, grader);
        return Results.Ok(new
        {
            observed_step_count = observed.Count,
            submitted_steps = submitted,
            result,
            session_status = session.Status,
            current_step_index = session.CurrentStepIndex,
            active_document = documentStatus,
        });
    }
    catch (Exception ex)
    {
        return Results.Problem(ex.Message, statusCode: 500);
    }
});

app.MapPost("/api/teaching/sessions/{sessionId}/audit-part", async (
    string sessionId,
    AuditPartRequest request,
    SessionStore sessions,
    SolidWorksExtractor extractor,
    FeatureGrader grader) =>
{
    if (!sessions.TryGet(sessionId, out var session))
        return Results.NotFound(new { detail = $"session not found: {sessionId}" });

    try
    {
        var observed = await extractor.AuditPartAsync(request.PartPath);
        var submitted = observed.Count > 0 ? new List<SkillStep> { observed[^1] } : new List<SkillStep>();
        var result = sessions.SubmitTeachingObservation(session, submitted, grader);
        return Results.Ok(new { observed_steps = observed, result });
    }
    catch (Exception ex)
    {
        return Results.Problem(ex.Message, statusCode: 500);
    }
});

app.MapPost("/api/testing/sessions/{sessionId}/submit", async (
    string sessionId,
    SubmitTestRequest request,
    SessionStore sessions,
    SolidWorksExtractor extractor,
    FeatureGrader grader) =>
{
    if (!sessions.TryGet(sessionId, out var session))
        return Results.NotFound(new { detail = $"session not found: {sessionId}" });

    try
    {
        var observed = string.IsNullOrWhiteSpace(request.PartPath)
            ? SkillFlow.NormalizeSteps(JsonSerializer.SerializeToElement(new { steps = request.Steps }))
            : await extractor.AuditPartAsync(request.PartPath);
        var report = sessions.SubmitTest(session, observed, grader);
        return Results.Ok(report);
    }
    catch (Exception ex)
    {
        return Results.Problem(ex.Message, statusCode: 500);
    }
});

app.Run();

static class DiagnosticFlowCatalog
{
    public static object Build(AppPaths paths) => new
    {
        schema = "agent4.diagnostic_flow_catalog.v1",
        boundary_doc = Doc(paths.DiagnosticFlowBoundariesDoc),
        flows = new[]
        {
            new
            {
                id = "model_part",
                label = "三维零件查错",
                scope = "single_sldprt_part_design",
                service = "mechanical-diagnostics",
                frontend_target = "part",
                endpoints = new[]
                {
                    "POST /api/mechanical-diagnostics/diagnose/start",
                    "GET /api/mechanical-diagnostics/diagnose/status/{jobId}",
                    "POST /api/mechanical-diagnostics/interfaces/finalize-existing",
                    "POST /api/mechanical-diagnostics/diagnose/finalize-existing",
                    "POST /api/mechanical-diagnostics/diagnose/load-existing"
                },
                required_inputs = new[] { "model_path(.SLDPRT)", "part_function_profile" },
                responsibilities = new[]
                {
                    "提取单零件 B-rep、特征组和高亮图片",
                    "生成局部语义子图",
                    "检索零件设计禁忌规则",
                    "判断零件自身结构设计问题"
                },
                forbidden = new[]
                {
                    "不得生成二维图纸公差结论",
                    "不得替代装配体接触/配合分析"
                },
                key_outputs = new[]
                {
                    "mechanical_semantic_graph.json",
                    "local_subgraph_collection.json",
                    "final_ai_diagnostic_report.md"
                },
                docs = new[] { Doc(paths.ModelDiagnosticReadmeDoc), Doc(paths.MechanicalDiagnosticContractDoc), Doc(paths.MechanicalDiagnosticUsageDoc) }
            },
            new
            {
                id = "model_assembly",
                label = "三维装配体查错",
                scope = "sldasm_assembly_function_and_nonstandard_part_checks",
                service = "mechanical-diagnostics",
                frontend_target = "assembly",
                endpoints = new[]
                {
                    "POST /api/mechanical-diagnostics/diagnose/start",
                    "GET /api/mechanical-diagnostics/diagnose/status/{jobId}",
                    "POST /api/mechanical-diagnostics/interfaces/finalize-existing",
                    "POST /api/mechanical-diagnostics/diagnose/finalize-existing",
                    "POST /api/mechanical-diagnostics/diagnose/load-existing"
                },
                required_inputs = new[] { "model_path(.SLDASM)", "product_description", "purchased_component_hints" },
                responsibilities = new[]
                {
                    "提取装配体 B-rep、mate、有限接触面和组件图片证据",
                    "导出 physical_interfaces 接触高亮图片",
                    "VLM 结合接触高亮图、B-rep、mate 和零件功能生成接触面/接口功能描述",
                    "生成零件整体功能、接口功能、连接方式和功能组",
                    "按功能组判断装配关系问题",
                    "对组内非标零件继承装配上下文后执行零件自身查错"
                },
                forbidden = new[]
                {
                    "不得只靠 SolidWorks mate 替代 B-rep 接触分析",
                    "不得把接触存在直接等价为二维公差具体代号",
                    "不得检查外购件/标准件内部结构，除非显式要求"
                },
                key_outputs = new[]
                {
                    "diagnostic_context/component_function_introductions.json",
                    "diagnostic_context/interface_anchor_analysis.json",
                    "diagnostic_context/interface_function_descriptions.json",
                    "diagnostic_context/functional_group_descriptions.json",
                    "functional_group_evidence/manifest.json",
                    "mechanical_semantic_graph.json",
                    "final_ai_diagnostic_report.md"
                },
                docs = new[] { Doc(paths.ModelDiagnosticReadmeDoc), Doc(paths.MechanicalDiagnosticContractDoc), Doc(paths.MechanicalDiagnosticUsageDoc) }
            },
            new
            {
                id = "drawing_part",
                label = "二维零件图查错",
                scope = "single_part_dwg_format_check_currently",
                service = "drawing-part-diagnostics",
                frontend_target = "drawing-part",
                endpoints = new[]
                {
                    "POST /api/drawing-part-diagnostics/run-full",
                    "GET /api/drawing-part-diagnostics/latest",
                    "GET /api/drawing-part-diagnostics/image"
                },
                required_inputs = new[] { "student_dwg_path" },
                responsibilities = new[]
                {
                    "抽取学生零件 DWG 结构化图元",
                    "按程序化规则检查线型、线宽、中心线、虚线、尺寸线、引出线、剖面线等格式问题",
                    "生成格式问题证据图片和报告"
                },
                forbidden = new[]
                {
                    "当前格式查错不走 AI",
                    "不得调用三维装配体接触语义",
                    "不得和二维装配图公差接口混用"
                },
                key_outputs = new[] { "format_issues.json", "final/final_report.md", "analysis_result.json" },
                docs = new[] { Doc(paths.DrawingDiagnosticReadmeDoc), Doc(paths.DiagnosticFlowBoundariesDoc) }
            },
            new
            {
                id = "drawing_assembly",
                label = "二维装配图查错",
                scope = "assembly_dwg_annotation_representation_tolerance_binding",
                service = "drawing-assembly-diagnostics",
                frontend_target = "drawing-assembly",
                endpoints = new[]
                {
                    "POST /api/drawing-assembly-diagnostics/run-full",
                    "POST /api/drawing-assembly-diagnostics/build-interface-drawing-context",
                    "POST /api/drawing-assembly-diagnostics/build-tolerance-context",
                    "POST /api/drawing-assembly-diagnostics/assess-tolerances",
                    "GET /api/drawing-assembly-diagnostics/latest",
                    "GET /api/drawing-assembly-diagnostics/image"
                },
                required_inputs = new[] { "student_dwg_path", "model_path or mechanical_context_dir", "ownership_context_dir when reusing existing ownership" },
                responsibilities = new[]
                {
                    "抽取学生装配 DWG",
                    "复用标准视图/线条归属流水线产物",
                    "识别公差标注作用的二维线条/边界",
                    "按线条归属、视图位置和三维 B-rep 接触面绑定二维公差与三维接口",
                    "多视图汇总同一接口标注",
                    "生成多标、漏标、错标候选",
                    "AI 结合固定公差知识和三维接触功能语义做最终判断"
                },
                forbidden = new[]
                {
                    "不得跳过二维线条归属和多视图汇总，直接用公差文本匹配三维直径",
                    "不得修改三维接触语义，只能作为证据使用",
                    "不得让三维装配体流程直接输出二维公差结论"
                },
                key_outputs = new[]
                {
                    "supplement/tolerance_assessment_context.json",
                    "supplement/roughness_assessment_context.json",
                    "supplement/assembly_function_context.json",
                    "supplement/supplemental_facts.json",
                    "tolerance_assessment/tolerance_diagnostic_result.json",
                    "tolerance_assessment/tolerance_diagnostic_report.md",
                    "final/final_report.md"
                },
                docs = new[] { Doc(paths.DrawingDiagnosticReadmeDoc), Doc(paths.DrawingAssemblyToleranceFlowDoc), Doc(paths.DiagnosticFlowBoundariesDoc) }
            }
        }
    };

    static object Doc(string path) => new
    {
        path,
        exists = File.Exists(path),
        title = File.Exists(path)
            ? File.ReadLines(path).FirstOrDefault(line => line.StartsWith("#", StringComparison.Ordinal))?.TrimStart('#', ' ')
            : ""
    };
}

sealed class AppPaths
{
    public string ApiRoot { get; } = AppContext.BaseDirectory;
    public string Agent4Root { get; }
    public string WorkspaceRoot { get; }
    public string StandardsRoot => Path.Combine(Agent4Root, "data", "standards");
    public string KnowledgeRoot => Path.Combine(Agent4Root, "data", "knowledge");
    public string ReducerFrameworkPath => Path.Combine(KnowledgeRoot, "reducer_three_layer_framework.json");
    public string ManifestPath => Path.Combine(StandardsRoot, "manifest.json");
    public string RuntimeDir => Path.Combine(Agent4Root, "runtime");
    public string PartFeatureAuditProject => Path.Combine(Agent4Root, "tools", "modeling-process", "PartFeatureAudit", "PartFeatureAudit.csproj");
    public string SectionBrepExtractorProject => Path.Combine(Agent4Root, "tools", "model-diagnostics", "SectionBrepExtractor", "SectionBrepExtractor.csproj");
    public string BrepFaultProbeProject => Path.Combine(Agent4Root, "tools", "model-diagnostics", "BrepFaultProbe", "BrepFaultProbe.csproj");
    public string DiagnosticFlowBoundariesDoc => Path.Combine(Agent4Root, "tools", "DIAGNOSTIC_FLOW_BOUNDARIES.md");
    public string DrawingDiagnosticReadmeDoc => Path.Combine(Agent4Root, "tools", "drawing-diagnostics", "README.md");
    public string DrawingAssemblyToleranceFlowDoc => Path.Combine(Agent4Root, "tools", "drawing-diagnostics", "ASSEMBLY_TOLERANCE_DIAGNOSTIC_FLOW.md");
    public string ModelDiagnosticReadmeDoc => Path.Combine(Agent4Root, "tools", "model-diagnostics", "README.md");
    public string MechanicalDiagnosticContractDoc => Path.Combine(Agent4Root, "tools", "model-diagnostics", "MECHANICAL_DIAGNOSTIC_CONTRACT.md");
    public string MechanicalDiagnosticUsageDoc => Path.Combine(Agent4Root, "tools", "model-diagnostics", "MECHANICAL_DIAGNOSTIC_USAGE.md");
    public string MechanicalKnowledgeIndexRoot => Path.Combine(Agent4Root, "机械设计禁忌1000例_知识库提取", "diagnostic_index");
    public string ProjectPathsFile => System.Environment.GetEnvironmentVariable("AGENT4_PROJECT_PATHS_FILE")
        ?? Path.Combine(Agent4Root, "project_paths.json");
    public string ReducerModelDir => System.Environment.GetEnvironmentVariable("AGENT4_REDUCER_MODEL_DIR")
        ?? ReadProjectPathValue("reducer_model_dir");
    public string SolidWorksExe => System.Environment.GetEnvironmentVariable("AGENT4_SOLIDWORKS_EXE")
        ?? ReadProjectPathValue("solidworks_exe");

    public AppPaths()
    {
        var configuredRoot = System.Environment.GetEnvironmentVariable("AGENT4_ROOT");
        if (!string.IsNullOrWhiteSpace(configuredRoot) && Directory.Exists(configuredRoot))
        {
            Agent4Root = Path.GetFullPath(configuredRoot);
            WorkspaceRoot = Directory.GetParent(Agent4Root)?.FullName ?? Agent4Root;
            return;
        }

        var dir = new DirectoryInfo(AppContext.BaseDirectory);
        while (dir != null &&
               (!File.Exists(Path.Combine(dir.FullName, "README.md")) ||
                !Directory.Exists(Path.Combine(dir.FullName, "data"))))
            dir = dir.Parent;

        Agent4Root = dir?.FullName ?? Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", ".."));
        WorkspaceRoot = Directory.GetParent(Agent4Root)?.FullName ?? Agent4Root;
    }

    public Dictionary<string, object?> ReadProjectPaths()
    {
        if (!File.Exists(ProjectPathsFile))
            return new Dictionary<string, object?>();

        try
        {
            using var doc = JsonDocument.Parse(File.ReadAllText(ProjectPathsFile));
            return doc.RootElement.EnumerateObject().ToDictionary(p => p.Name, p => (object?)p.Value.ToString());
        }
        catch
        {
            return new Dictionary<string, object?>();
        }
    }

    string ReadProjectPathValue(string key)
    {
        if (!File.Exists(ProjectPathsFile))
            return "";

        try
        {
            using var doc = JsonDocument.Parse(File.ReadAllText(ProjectPathsFile));
            return doc.RootElement.TryGetProperty(key, out var value) ? value.ToString() : "";
        }
        catch
        {
            return "";
        }
    }
}

sealed class SolidWorksConnectionService
{
    readonly AppPaths _paths;
    readonly object _lock = new();

    public SolidWorksConnectionService(AppPaths paths)
    {
        _paths = paths;
    }

    public SolidWorksConnectionResult EnsureConnected()
    {
        lock (_lock)
        {
            var alreadyRunning = TryConnectSta();
            if (alreadyRunning.Ok)
                return alreadyRunning;

            var exe = _paths.SolidWorksExe;
            if (string.IsNullOrWhiteSpace(exe))
                throw new FileNotFoundException("project_paths.json 中没有配置 solidworks_exe。");
            if (!File.Exists(exe))
                throw new FileNotFoundException($"SolidWorks 启动路径不存在：{exe}");

            Process.Start(new ProcessStartInfo
            {
                FileName = exe,
                UseShellExecute = true,
                WorkingDirectory = Path.GetDirectoryName(exe) ?? _paths.Agent4Root,
            });

            var deadline = DateTime.UtcNow.AddSeconds(45);
            Exception? lastError = null;
            while (DateTime.UtcNow < deadline)
            {
                Thread.Sleep(1500);
                var connected = TryConnectSta();
                if (connected.Ok)
                    return connected with { Started = true };
                if (!string.IsNullOrWhiteSpace(connected.Message))
                    lastError = new InvalidOperationException(connected.Message);
            }

            throw new TimeoutException("已尝试启动 SolidWorks，但 45 秒内没有连接成功。" + (lastError == null ? "" : $" 最后错误：{lastError.Message}"));
        }
    }

    public SolidWorksConnectionResult OpenDocument(string path)
    {
        if (string.IsNullOrWhiteSpace(path))
            throw new ArgumentException("标准零件路径为空。", nameof(path));
        if (!File.Exists(path))
            throw new FileNotFoundException($"标准零件文件不存在：{path}");

        var connected = EnsureConnected();
        Exception? error = null;
        SolidWorksConnectionResult result = connected;
        var thread = new Thread(() =>
        {
            try
            {
                var hr = CLSIDFromProgID("SldWorks.Application", out var clsid);
                if (hr < 0)
                    Marshal.ThrowExceptionForHR(hr);

                var appObj = GetActiveObject(ref clsid, IntPtr.Zero);
                var app = appObj as SldWorks;
                if (app == null)
                    throw new InvalidOperationException("已连接 SolidWorks，但无法获取 SldWorks 应用对象。");

                app.Visible = true;
                var extension = Path.GetExtension(path).ToLowerInvariant();
                var docType = extension == ".sldasm"
                    ? (int)swDocumentTypes_e.swDocASSEMBLY
                    : extension == ".slddrw"
                        ? (int)swDocumentTypes_e.swDocDRAWING
                        : (int)swDocumentTypes_e.swDocPART;
                var errors = 0;
                var warnings = 0;
                var model = app.OpenDoc6(
                    path,
                    docType,
                    (int)swOpenDocOptions_e.swOpenDocOptions_Silent,
                    "",
                    ref errors,
                    ref warnings);
                if (model == null || errors != 0)
                    throw new InvalidOperationException($"SolidWorks 打开文档失败，errors={errors}, warnings={warnings}");

                result = connected with { Message = $"已打开标准零件：{Path.GetFileName(path)}" };
                Marshal.ReleaseComObject(model);
                Marshal.ReleaseComObject(appObj);
            }
            catch (Exception ex)
            {
                error = ex;
            }
        });
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
        thread.Join();

        if (error != null)
            throw error;
        return result;
    }

    public HighlightResult HighlightTarget(string partPath, HighlightTarget target)
    {
        if (target == null)
            throw new ArgumentNullException(nameof(target));
        if (string.IsNullOrWhiteSpace(partPath))
            throw new ArgumentException("标准零件路径为空。", nameof(partPath));
        if (!File.Exists(partPath))
            throw new FileNotFoundException($"标准零件文件不存在：{partPath}");

        EnsureConnected();

        HighlightResult result = new(false, "", "", $"没有找到可高亮对象：{target.Label}", 0, target.Description);
        Exception? error = null;
        var thread = new Thread(() =>
        {
            try
            {
                var hr = CLSIDFromProgID("SldWorks.Application", out var clsid);
                if (hr < 0)
                    Marshal.ThrowExceptionForHR(hr);

                var appObj = GetActiveObject(ref clsid, IntPtr.Zero);
                var app = appObj as SldWorks;
                if (app == null)
                    throw new InvalidOperationException("已连接 SolidWorks，但无法获取 SldWorks 应用对象。");

                app.Visible = true;
                var doc = GetOpenPartDocument(app, partPath);
                if (doc == null)
                    throw new InvalidOperationException("SolidWorks 当前没有打开标准零件。");
                if (doc.GetType() != (int)swDocumentTypes_e.swDocPART)
                    throw new InvalidOperationException("当前活动文档不是零件，无法做功能面高亮。");

                doc.ClearSelection2(true);
                var matched = TrySelectMatchingFace(doc, target, out var faceResult);
                if (matched)
                {
                    doc.GraphicsRedraw2();
                    result = faceResult;
                }
                else if (TrySelectSourceFeature(doc, target.SourceFeature, out var featureName))
                {
                    doc.GraphicsRedraw2();
                    result = new HighlightResult(
                        true,
                        "feature",
                        featureName,
                        $"面级匹配不稳定，已回退高亮来源特征：{featureName}。讲解对象：{target.Label}。",
                        0,
                        target.Description);
                }
                else
                {
                    result = new HighlightResult(
                        false,
                        "none",
                        "",
                        $"没有匹配到 {target.Label}。面级候选分数={faceResult.Score:0.###}，来源特征={target.SourceFeature}。",
                        faceResult.Score,
                        target.Description);
                }

                Marshal.ReleaseComObject(doc);
                Marshal.ReleaseComObject(appObj);
            }
            catch (Exception ex)
            {
                error = ex;
            }
        });
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
        thread.Join();

        if (error != null)
            throw error;
        return result;
    }

    static ModelDoc2? GetOpenPartDocument(SldWorks app, string partPath)
    {
        var active = app.IActiveDoc2 ?? InvokeCom(app, "ActiveDoc") as ModelDoc2;
        if (active != null && SamePath(ReadDocumentPath(active), partPath))
            return active;

        var errors = 0;
        var warnings = 0;
        var model = app.OpenDoc6(
            partPath,
            (int)swDocumentTypes_e.swDocPART,
            (int)swOpenDocOptions_e.swOpenDocOptions_Silent,
            "",
            ref errors,
            ref warnings);
        if (model == null || errors != 0)
            throw new InvalidOperationException($"SolidWorks 打开标准零件失败，errors={errors}, warnings={warnings}");

        return model;
    }

    static string ReadDocumentPath(ModelDoc2 doc)
    {
        try
        {
            return doc.GetPathName() ?? "";
        }
        catch
        {
            return Convert.ToString(InvokeCom(doc, "GetPathName")) ?? "";
        }
    }

    static bool SamePath(string left, string right)
    {
        if (string.IsNullOrWhiteSpace(left) || string.IsNullOrWhiteSpace(right))
            return false;
        try
        {
            return string.Equals(Path.GetFullPath(left), Path.GetFullPath(right), StringComparison.OrdinalIgnoreCase);
        }
        catch
        {
            return string.Equals(left, right, StringComparison.OrdinalIgnoreCase);
        }
    }

    public SolidWorksDocumentStatus GetActiveDocumentStatus()
    {
        SolidWorksDocumentStatus result = new(false, false, 0, "none", "", "尚未连接 SolidWorks。", false, "", "", 0, "", "", new(), new(), new());
        Exception? error = null;
        var thread = new Thread(() =>
        {
            try
            {
                var hr = CLSIDFromProgID("SldWorks.Application", out var clsid);
                if (hr < 0)
                    Marshal.ThrowExceptionForHR(hr);

                var appObj = GetActiveObject(ref clsid, IntPtr.Zero);
                var app = appObj as SldWorks;
                var doc = app?.IActiveDoc2 ?? InvokeCom(appObj, "ActiveDoc") as ModelDoc2;
                if (doc == null)
                {
                    result = new SolidWorksDocumentStatus(true, false, 0, "none", "", "SolidWorks 已连接，但当前没有打开任何文档。", false, "", "", 0, "", "", new(), new(), new());
                    Marshal.ReleaseComObject(appObj);
                    return;
                }

                var typeCode = doc.GetType();
                var title = doc.GetTitle() ?? "";
                var sketchStatus = ReadActiveSketchStatus(doc);
                result = new SolidWorksDocumentStatus(
                    true,
                    true,
                    typeCode,
                    DocumentTypeName(typeCode),
                    title,
                    "已读取当前 SolidWorks 文档。",
                    sketchStatus.Active,
                    sketchStatus.Name,
                    sketchStatus.ReferenceName,
                    sketchStatus.ReferenceType,
                    sketchStatus.ReferenceComType,
                    sketchStatus.ReferenceDebug,
                    sketchStatus.SelectedObjects,
                    sketchStatus.Circles,
                    sketchStatus.Dimensions);
                Marshal.ReleaseComObject(doc);
                Marshal.ReleaseComObject(appObj);
            }
            catch (Exception ex)
            {
                error = ex;
            }
        });
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
        thread.Join();

        return error == null ? result : new SolidWorksDocumentStatus(false, false, 0, "none", "", error.Message, false, "", "", 0, "", "", new(), new(), new());
    }

    static ActiveSketchStatus ReadActiveSketchStatus(ModelDoc2 doc)
    {
        var selectedObjects = ReadSelectedObjects(doc);
        try
        {
            var sketchManager = doc.SketchManager;
            var activeSketch = sketchManager?.ActiveSketch;
            if (activeSketch == null)
                return new ActiveSketchStatus(false, "", "", 0, "", "SketchManager.ActiveSketch returned null.", selectedObjects, new(), new());

            var sketchName = "";
            var sketchFeature = InvokeCom(activeSketch, "GetFeature") as Feature;
            if (sketchFeature != null)
                sketchName = sketchFeature.Name ?? "";

            var referenceType = 0;
            var reference = activeSketch.GetReferenceEntity(ref referenceType);
            var referenceInfo = DescribeSketchReference(doc, reference, referenceType, selectedObjects);
            var circles = ReadActiveSketchCircles(activeSketch);
            var dimensions = ReadActiveSketchDimensions(activeSketch);

            return new ActiveSketchStatus(
                true,
                sketchName,
                referenceInfo.Name,
                referenceType,
                referenceInfo.ComType,
                referenceInfo.Debug,
                selectedObjects,
                circles,
                dimensions);
        }
        catch (Exception ex)
        {
            return new ActiveSketchStatus(false, "", "", 0, "", ex.Message, selectedObjects, new(), new());
        }
    }

    static List<SketchCircleInfo> ReadActiveSketchCircles(Sketch activeSketch)
    {
        var result = new List<SketchCircleInfo>();
        foreach (var segmentObj in ToObjectList(activeSketch.GetSketchSegments()))
        {
            if (segmentObj is not SketchSegment segment)
                continue;

            var segmentType = segment.GetType();
            if (segmentType != (int)swSketchSegments_e.swSketchARC || IsConstructionGeometry(segment))
                continue;

            if (segment is not SketchArc arc)
                continue;

            var centerPoint = arc.GetCenterPoint2() as SketchPoint;
            var startPoint = arc.GetStartPoint2() as SketchPoint;
            var endPoint = arc.GetEndPoint2() as SketchPoint;
            var center = ReadSketchPointMm(centerPoint);
            var start = ReadSketchPointMm(startPoint);
            var end = ReadSketchPointMm(endPoint);
            var radiusMm = arc.GetRadius() * 1000.0;
            if (radiusMm <= 0 && center != null && start != null)
                radiusMm = DistanceMm(center, start);
            if (radiusMm <= 0)
                continue;

            var isFullCircle = start == null || end == null || DistanceMm(start, end) <= 0.001;
            result.Add(new SketchCircleInfo(
                Math.Round(radiusMm * 2.0, 4),
                center == null ? null : Math.Round(center[0], 4),
                center == null ? null : Math.Round(center[1], 4),
                center == null ? null : Math.Round(center[2], 4),
                isFullCircle,
                segmentObj.GetType().FullName ?? segmentObj.GetType().Name));
        }

        return result;
    }

    static List<SketchDimensionInfo> ReadActiveSketchDimensions(Sketch activeSketch)
    {
        var result = new List<SketchDimensionInfo>();
        foreach (var dimensionObj in ToObjectList(InvokeCom(activeSketch, "GetDimensions")))
        {
            var dimension = dimensionObj as Dimension;
            if (dimension == null && dimensionObj is DisplayDimension displayDimension)
                dimension = displayDimension.GetDimension() as Dimension;
            if (dimension == null)
                dimension = InvokeCom(dimensionObj!, "GetDimension") as Dimension;
            if (dimension == null)
                continue;

            var valueMm = dimension.SystemValue * 1000.0;
            if (valueMm <= 0)
                continue;
            var name = FirstNonEmpty(
                dimension.GetNameForSelection(),
                dimension.Name);
            result.Add(new SketchDimensionInfo(name, Math.Round(valueMm, 4)));
        }

        return result;
    }

    static bool IsConstructionGeometry(SketchSegment segment)
    {
        try
        {
            return segment.ConstructionGeometry;
        }
        catch
        {
            return false;
        }
    }

    static double[]? ReadSketchPointMm(SketchPoint? point)
    {
        if (point == null)
            return null;

        var x = point.X * 1000.0;
        var y = point.Y * 1000.0;
        var z = point.Z * 1000.0;
        return new[] { x, y, z };
    }

    static SketchReferenceInfo DescribeSketchReference(
        object doc,
        object? reference,
        int referenceType,
        List<SelectedObjectInfo> selectedObjects)
    {
        if (reference == null)
        {
            var selectedPlane = selectedObjects.FirstOrDefault(x => x.IsDatumPlane);
            return selectedPlane == null
                ? new SketchReferenceInfo("", "null", $"GetReferenceEntity returned null. reference_type={referenceType}.")
                : new SketchReferenceInfo(selectedPlane.Name, "selection", $"GetReferenceEntity returned null. Fallback to selected datum plane. reference_type={referenceType}.");
        }

        var comType = reference.GetType().FullName ?? reference.GetType().Name;
        var typeName = Convert.ToString(InvokeCom(reference, "GetTypeName2")) ?? "";
        var name = FirstNonEmpty(
            Convert.ToString(InvokeCom(reference, "Name")),
            Convert.ToString(InvokeCom(reference, "Name2")));

        if (string.Equals(typeName, "RefPlane", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(name))
            return new SketchReferenceInfo(name, comType, $"GetReferenceEntity returned Feature RefPlane. reference_type={referenceType}.");

        var referenceFeature = InvokeCom(reference, "GetFeature");
        var referenceFeatureType = referenceFeature == null ? "" : Convert.ToString(InvokeCom(referenceFeature, "GetTypeName2")) ?? "";
        var referenceFeatureName = referenceFeature == null ? "" : Convert.ToString(InvokeCom(referenceFeature, "Name")) ?? "";
        if (string.Equals(referenceFeatureType, "RefPlane", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(referenceFeatureName))
            return new SketchReferenceInfo(referenceFeatureName, comType, $"GetReferenceEntity returned RefPlane object; GetFeature resolved datum plane. reference_type={referenceType}.");

        if (referenceType == 4)
        {
            var featureTreeName = FindMatchingRefPlaneFeatureName(doc, reference);
            if (!string.IsNullOrWhiteSpace(featureTreeName))
                return new SketchReferenceInfo(featureTreeName, comType, $"GetReferenceEntity returned swSelDATUMPLANES; matched RefPlane in feature tree. reference_type={referenceType}.");

            var selectedPlane = selectedObjects.FirstOrDefault(x => x.IsDatumPlane);
            if (selectedPlane != null)
                return new SketchReferenceInfo(selectedPlane.Name, comType, $"GetReferenceEntity returned swSelDATUMPLANES; fallback to selected datum plane. reference_type={referenceType}.");
        }

        if (!string.IsNullOrWhiteSpace(name))
            return new SketchReferenceInfo(name, comType, $"GetReferenceEntity returned named object. reference_type={referenceType}; type_name={typeName}.");

        if (!string.IsNullOrWhiteSpace(referenceFeatureName))
            return new SketchReferenceInfo(referenceFeatureName, comType, $"GetReferenceEntity feature fallback. reference_type={referenceType}; feature_type={referenceFeatureType}.");

        return new SketchReferenceInfo("", comType, $"Could not resolve sketch reference name. reference_type={referenceType}; type_name={typeName}; feature_type={referenceFeatureType}.");
    }

    static string FindMatchingRefPlaneFeatureName(object doc, object reference)
    {
        var targetId = ComIdentity(reference);
        if (targetId == IntPtr.Zero)
            return "";

        var feature = InvokeCom(doc, "FirstFeature");
        var guard = 0;
        while (feature != null && guard++ < 5000)
        {
            var typeName = Convert.ToString(InvokeCom(feature, "GetTypeName2")) ?? "";
            if (string.Equals(typeName, "RefPlane", StringComparison.OrdinalIgnoreCase))
            {
                if (ComIdentity(feature) == targetId)
                    return Convert.ToString(InvokeCom(feature, "Name")) ?? "";

                var specific = InvokeCom(feature, "GetSpecificFeature2");
                if (specific != null && ComIdentity(specific) == targetId)
                    return Convert.ToString(InvokeCom(feature, "Name")) ?? "";
            }

            feature = InvokeCom(feature, "GetNextFeature");
        }

        return "";
    }

    static List<SelectedObjectInfo> ReadSelectedObjects(object doc)
    {
        var result = new List<SelectedObjectInfo>();
        try
        {
            var selectionManager = InvokeCom(doc, "SelectionManager");
            if (selectionManager == null)
                return result;

            var count = Convert.ToInt32(InvokeCom(selectionManager, "GetSelectedObjectCount2", -1) ?? 0);
            for (var i = 1; i <= count; i++)
            {
                var selected = InvokeCom(selectionManager, "GetSelectedObject6", i, -1);
                var selectedType = Convert.ToInt32(InvokeCom(selectionManager, "GetSelectedObjectType3", i, -1) ?? 0);
                if (selected == null)
                {
                    result.Add(new SelectedObjectInfo(i, selectedType, "", "null", "", false));
                    continue;
                }

                var typeName = Convert.ToString(InvokeCom(selected, "GetTypeName2")) ?? "";
                var name = FirstNonEmpty(
                    Convert.ToString(InvokeCom(selected, "Name")),
                    Convert.ToString(InvokeCom(selected, "Name2")));
                var feature = InvokeCom(selected, "GetFeature");
                if (string.IsNullOrWhiteSpace(name) && feature != null)
                    name = Convert.ToString(InvokeCom(feature, "Name")) ?? "";
                var featureType = feature == null ? "" : Convert.ToString(InvokeCom(feature, "GetTypeName2")) ?? "";
                var isDatumPlane = selectedType == 4
                    || string.Equals(typeName, "RefPlane", StringComparison.OrdinalIgnoreCase)
                    || string.Equals(featureType, "RefPlane", StringComparison.OrdinalIgnoreCase);

                result.Add(new SelectedObjectInfo(
                    i,
                    selectedType,
                    name,
                    selected.GetType().FullName ?? selected.GetType().Name,
                    FirstNonEmpty(typeName, featureType),
                    isDatumPlane));
            }
        }
        catch
        {
        }

        return result;
    }

    static bool TrySelectMatchingFace(ModelDoc2 doc, HighlightTarget target, out HighlightResult result)
    {
        result = new HighlightResult(false, "face", "", "没有找到匹配的功能面。", double.MaxValue, target.Description);
        if (doc is not PartDoc part)
            return false;

        var expectedBox = target.BBoxMm;
        var expectedCenter = target.CenterMm;
        var expectedArea = target.AreaMm2;
        Face2? bestFace = null;
        var bestScore = double.MaxValue;
        var bestIndex = 0;
        var index = 0;

        try
        {
            foreach (var bodyObj in ToObjectList(part.GetBodies2((int)swBodyType_e.swAllBodies, true)))
            {
                if (bodyObj == null)
                    continue;

                foreach (var faceObj in ToObjectList(InvokeCom(bodyObj, "GetFaces")))
                {
                    if (faceObj is not Face2 face)
                        continue;

                    index++;
                    var score = ScoreFace(face, target, expectedBox, expectedCenter, expectedArea);
                    if (score < bestScore)
                    {
                        bestScore = score;
                        bestFace = face;
                        bestIndex = index;
                    }
                }
            }
        }
        catch
        {
            return false;
        }

        result = new HighlightResult(false, "face", $"face-{bestIndex}", "找到候选面，但置信度不足。", bestScore, target.Description);
        if (bestFace == null || bestScore > 18.0)
            return false;

        var selected = false;
        try
        {
            if (bestFace is Entity entity)
                selected = entity.Select4(false, null!);
        }
        catch
        {
        }

        if (!selected)
            selected = Convert.ToBoolean(InvokeCom(bestFace, "Select4", false, null!) ?? false);
        if (!selected)
            return false;

        result = new HighlightResult(
            true,
            "face",
            $"face-{bestIndex}",
            $"已高亮 {target.Label}。{target.Description}",
            bestScore,
            target.Description);
        return true;
    }

    static double ScoreFace(Face2 face, HighlightTarget target, IReadOnlyList<double> expectedBox, IReadOnlyList<double> expectedCenter, double? expectedArea)
    {
        var score = 0.0;
        var surface = "";
        try
        {
            if (face.GetSurface() is Surface faceSurface)
                surface = SurfaceKind(faceSurface);
        }
        catch
        {
        }

        if (!string.IsNullOrWhiteSpace(target.RawSurfaceType))
            score += string.Equals(surface, target.RawSurfaceType, StringComparison.OrdinalIgnoreCase) ? 0 : 100;

        var box = TryGetFaceBoxMm(face);
        if (expectedBox.Count >= 6 && box.Count >= 6)
        {
            for (var i = 0; i < 6; i++)
                score += Math.Min(25.0, Math.Abs(expectedBox[i] - box[i]));
        }
        else if (expectedCenter.Count >= 3 && box.Count >= 6)
        {
            var center = new[] { (box[0] + box[3]) / 2.0, (box[1] + box[4]) / 2.0, (box[2] + box[5]) / 2.0 };
            score += Math.Min(50.0, DistanceMm(expectedCenter.ToArray(), center));
        }
        else
        {
            score += 30.0;
        }

        if (expectedArea.HasValue)
        {
            try
            {
                var area = face.GetArea() * 1_000_000.0;
                score += Math.Min(15.0, Math.Abs(expectedArea.Value - area) / Math.Max(1.0, expectedArea.Value) * 15.0);
            }
            catch
            {
            }
        }

        return score;
    }

    static bool TrySelectSourceFeature(ModelDoc2 doc, string featureName, out string selectedName)
    {
        selectedName = "";
        if (string.IsNullOrWhiteSpace(featureName))
            return false;

        try
        {
            var feature = FindFeatureByName(doc, featureName);
            if (feature == null)
                return false;

            selectedName = feature.Name ?? featureName;
            return feature.Select2(false, 0);
        }
        catch
        {
            return false;
        }
    }

    static Feature? FindFeatureByName(ModelDoc2 doc, string featureName)
    {
        var featureObj = InvokeCom(doc, "FirstFeature");
        var guard = 0;
        while (featureObj != null && guard++ < 5000)
        {
            if (featureObj is not Feature feature)
                break;
            if (string.Equals(feature.Name, featureName, StringComparison.OrdinalIgnoreCase))
                return feature;
            featureObj = InvokeCom(feature, "GetNextFeature");
        }

        return null;
    }

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

        return "unknown";
    }

    static List<double> TryGetFaceBoxMm(Face2 face)
    {
        try
        {
            return ToDoubleList(face.GetBox()).Take(6).Select(value => Math.Round(value * 1000.0, 6)).ToList();
        }
        catch
        {
            return new List<double>();
        }
    }

    static SolidWorksConnectionResult TryConnectSta()
    {
        SolidWorksConnectionResult result = new(false, false, "", "");
        Exception? error = null;
        var thread = new Thread(() =>
        {
            try
            {
                var hr = CLSIDFromProgID("SldWorks.Application", out var clsid);
                if (hr < 0)
                    Marshal.ThrowExceptionForHR(hr);

                var app = GetActiveObject(ref clsid, IntPtr.Zero);
                var visible = InvokeCom(app, "Visible")?.ToString() ?? "";
                var revision = InvokeCom(app, "RevisionNumber")?.ToString() ?? "";
                result = new SolidWorksConnectionResult(true, false, revision, $"宸茶繛鎺?SolidWorks銆俈isible={visible}");
                Marshal.ReleaseComObject(app);
            }
            catch (Exception ex)
            {
                error = ex;
            }
        });
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
        thread.Join();

        return result.Ok ? result : new SolidWorksConnectionResult(false, false, "", error?.Message ?? "无法连接 SolidWorks。");
    }

    static object? InvokeCom(object target, string name)
    {
        try
        {
            return target.GetType().InvokeMember(
                name,
                System.Reflection.BindingFlags.GetProperty,
                null,
                target,
                Array.Empty<object>());
        }
        catch
        {
            try
            {
                return target.GetType().InvokeMember(
                    name,
                    System.Reflection.BindingFlags.InvokeMethod,
                    null,
                    target,
                    Array.Empty<object>());
            }
            catch
            {
                return null;
            }
        }
    }

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

    static object? InvokeComByRefInt(object target, string name, ref int value)
    {
        try
        {
            object[] args = { value };
            var result = target.GetType().InvokeMember(
                name,
                System.Reflection.BindingFlags.InvokeMethod,
                null,
                target,
                args);
            if (args.Length > 0 && args[0] is int updated)
                value = updated;
            return result;
        }
        catch
        {
            return null;
        }
    }

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

        return "";
    }

    static IntPtr ComIdentity(object? value)
    {
        if (value == null)
            return IntPtr.Zero;

        IntPtr unknown = IntPtr.Zero;
        try
        {
            unknown = Marshal.GetIUnknownForObject(value);
            return unknown;
        }
        catch
        {
            return IntPtr.Zero;
        }
        finally
        {
            if (unknown != IntPtr.Zero)
                Marshal.Release(unknown);
        }
    }

    static List<object?> ToObjectList(object? value)
    {
        if (value == null)
            return new List<object?>();
        if (value is object?[] objects)
            return objects.ToList();
        if (value is Array array)
        {
            var result = new List<object?>();
            foreach (var item in array)
                result.Add(item);
            return result;
        }

        return new List<object?> { value };
    }

    static List<double> ToDoubleList(object? value)
    {
        if (value == null)
            return new List<double>();
        if (value is double[] doubles)
            return doubles.ToList();
        if (value is Array array)
        {
            var result = new List<double>();
            foreach (var item in array)
            {
                try
                {
                    result.Add(Convert.ToDouble(item));
                }
                catch
                {
                }
            }
            return result;
        }

        try
        {
            return new List<double> { Convert.ToDouble(value) };
        }
        catch
        {
            return new List<double>();
        }
    }

    static int ToInt(object? value, int fallback = 0)
    {
        if (value == null)
            return fallback;
        try
        {
            return Convert.ToInt32(value);
        }
        catch
        {
            return fallback;
        }
    }

    static double ToDouble(object? value, double fallback = 0.0)
    {
        if (value == null)
            return fallback;
        try
        {
            return Convert.ToDouble(value);
        }
        catch
        {
            return fallback;
        }
    }

    static bool HasComMember(object target, string name)
    {
        return InvokeCom(target, name) != null;
    }

    static double DistanceMm(double[] a, double[] b)
    {
        if (a.Length < 3 || b.Length < 3)
            return double.MaxValue;
        var dx = a[0] - b[0];
        var dy = a[1] - b[1];
        var dz = a[2] - b[2];
        return Math.Sqrt(dx * dx + dy * dy + dz * dz);
    }

    static string DocumentTypeName(int typeCode)
    {
        return typeCode switch
        {
            1 => "part",
            2 => "assembly",
            3 => "drawing",
            _ => "unknown",
        };
    }

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

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

sealed record SolidWorksConnectionResult(bool Ok, bool Started, string Revision, string Message);

sealed record SelectedObjectInfo(
    int Index,
    int SelectionType,
    string Name,
    string ComType,
    string SolidWorksTypeName,
    bool IsDatumPlane);

sealed record SketchReferenceInfo(string Name, string ComType, string Debug);

sealed record SketchCircleInfo(
    double DiameterMm,
    double? CenterXLocalMm,
    double? CenterYLocalMm,
    double? CenterZLocalMm,
    bool IsFullCircle,
    string ComType);

sealed record SketchDimensionInfo(string Name, double ValueMm);

sealed record ActiveSketchStatus(
    bool Active,
    string Name,
    string ReferenceName,
    int ReferenceType,
    string ReferenceComType,
    string ReferenceDebug,
    List<SelectedObjectInfo> SelectedObjects,
    List<SketchCircleInfo> Circles,
    List<SketchDimensionInfo> Dimensions);

sealed record SolidWorksDocumentStatus(
    bool Connected,
    bool HasDocument,
    int TypeCode,
    string TypeName,
    string Title,
    string Message,
    bool ActiveSketch,
    string ActiveSketchName,
    string ActiveSketchReferenceName,
    int ActiveSketchReferenceType,
    string ActiveSketchReferenceComType,
    string ActiveSketchReferenceDebug,
    List<SelectedObjectInfo> SelectedObjects,
    List<SketchCircleInfo> ActiveSketchCircles,
    List<SketchDimensionInfo> ActiveSketchDimensions);

sealed class KnowledgeRepository
{
    readonly AppPaths _paths;
    readonly object _lock = new();
    Dictionary<string, StandardAnswer> _standards = new(StringComparer.OrdinalIgnoreCase);

    public KnowledgeRepository(AppPaths paths)
    {
        _paths = paths;
        Reload();
    }

    public void Reload()
    {
        var standards = new Dictionary<string, StandardAnswer>(StringComparer.OrdinalIgnoreCase);
        foreach (var item in LoadManifestStandards())
            standards[item.StandardId] = item;
        foreach (var item in LoadLocalJsonStandards())
            standards.TryAdd(item.StandardId, item);

        lock (_lock)
            _standards = standards;
    }

    public List<StandardAnswer> ListStandards(string query = "", string kind = "")
    {
        List<StandardAnswer> items;
        lock (_lock)
            items = _standards.Values.ToList();

        if (!string.IsNullOrWhiteSpace(kind) && !string.Equals(kind, "unknown", StringComparison.OrdinalIgnoreCase))
            items = items.Where(item => string.Equals(item.Kind, kind, StringComparison.OrdinalIgnoreCase)).ToList();

        if (!string.IsNullOrWhiteSpace(query))
        {
            var tokens = SearchTokens(query);
            items = items
                .Select(item => new { Score = Score(tokens, item), Item = item })
                .Where(pair => pair.Score > 0)
                .OrderByDescending(pair => pair.Score)
                .Select(pair => pair.Item)
                .ToList();
        }

        return items;
    }

    public bool TryGet(string standardId, out StandardAnswer standard)
    {
        lock (_lock)
            return _standards.TryGetValue(standardId, out standard!);
    }

    IEnumerable<StandardAnswer> LoadManifestStandards()
    {
        if (!File.Exists(_paths.ManifestPath))
            yield break;

        using var doc = JsonDocument.Parse(File.ReadAllText(_paths.ManifestPath));
        if (!doc.RootElement.TryGetProperty("entries", out var entries) || entries.ValueKind != JsonValueKind.Array)
            yield break;

        foreach (var entry in entries.EnumerateArray())
        {
            var standard = StandardFromManifestEntry(entry);
            if (standard != null)
                yield return standard;
        }
    }

    StandardAnswer? StandardFromManifestEntry(JsonElement entry)
    {
        var standardId = ReadString(entry, "standard_id");
        if (string.IsNullOrWhiteSpace(standardId))
            return null;

        var preferred = FirstNonEmpty(
            ReadString(entry, "modeling_plan_path"),
            ReadString(entry, "knowledge_skillflow_path"),
            ReadString(entry, "skill_flow_path"));
        var path = string.IsNullOrWhiteSpace(preferred) ? "" : Path.Combine(_paths.StandardsRoot, preferred);
        var loaded = !string.IsNullOrWhiteSpace(path) ? SkillFlow.LoadStandardFromFile(path) : null;

        if (loaded == null)
        {
            return new StandardAnswer
            {
                StandardId = standardId,
                Title = FirstNonEmpty(ReadString(entry, "title"), ReadString(entry, "source_base_name")),
                Kind = FirstNonEmpty(ReadString(entry, "kind"), "unknown"),
                SourcePath = path,
                SourceType = "manifest_ref",
                Summary = "Manifest standard answer reference without a readable local payload.",
                Steps = new List<SkillStep>(),
                Tags = TagsForText(ReadString(entry, "title") + " " + ReadString(entry, "category")),
            };
        }

        loaded.StandardId = standardId;
        loaded.Title = FirstNonEmpty(ReadString(entry, "title"), loaded.Title);
        loaded.Kind = FirstNonEmpty(ReadString(entry, "kind"), loaded.Kind);
        loaded.SourceType = "manifest";
        loaded.Tags = loaded.Tags.Concat(TagsForText(loaded.Title + " " + ReadString(entry, "category"))).Distinct().ToList();
        return loaded;
    }

    IEnumerable<StandardAnswer> LoadLocalJsonStandards()
    {
        if (!Directory.Exists(_paths.StandardsRoot))
            yield break;

        foreach (var path in Directory.EnumerateFiles(_paths.StandardsRoot, "*.json", SearchOption.AllDirectories))
        {
            var name = Path.GetFileName(path).ToLowerInvariant();
            if (!name.Contains("skillflow") && !name.Contains("skill_flow") && !name.Contains("modeling_plan") && !name.Contains("knowledge"))
                continue;

            var standard = SkillFlow.LoadStandardFromFile(path);
            if (standard != null)
                yield return standard;
        }
    }

    static int Score(List<string> tokens, StandardAnswer item)
    {
        var text = string.Join(" ", new[]
        {
            item.StandardId,
            item.Title,
            item.Summary,
            item.SourcePath,
            string.Join(" ", item.Tags),
            string.Join(" ", item.Steps.Take(80).Select(step => $"{step.Skill} {step.Name} {step.SourceFeature}")),
        }).ToLowerInvariant();

        return tokens.Sum(token => CountOccurrences(text, token));
    }

    static int CountOccurrences(string text, string token)
    {
        var count = 0;
        var index = 0;
        while ((index = text.IndexOf(token, index, StringComparison.OrdinalIgnoreCase)) >= 0)
        {
            count++;
            index += token.Length;
        }
        return count;
    }

    static List<string> SearchTokens(string query)
    {
        var lowered = query.ToLowerInvariant();
        var raw = lowered.Replace("_", " ").Replace("-", " ")
            .Split(' ', StringSplitOptions.RemoveEmptyEntries)
            .Where(item => item.Length >= 2)
            .ToList();

        for (var i = 0; i < lowered.Length - 1; i++)
        {
            var pair = lowered.Substring(i, 2);
            if (pair.Any(ch => ch >= '\u4e00' && ch <= '\u9fff'))
                raw.Add(pair);
        }

        return raw.Distinct().ToList();
    }

    static List<string> TagsForText(string text)
    {
        var tags = new List<string>();
        var mapping = new Dictionary<string, string>
        {
            ["齿轮"] = "gear",
            ["轴"] = "shaft",
            ["箱体"] = "housing",
            ["端盖"] = "cover",
            ["减速器"] = "reducer",
            ["装配"] = "assembly",
            ["assembly"] = "assembly",
            ["shell"] = "shell",
            ["鎶藉３"] = "shell",
            ["loft"] = "loft",
            ["鏀炬牱"] = "loft",
        };
        foreach (var (key, tag) in mapping)
            if (text.Contains(key, StringComparison.OrdinalIgnoreCase) && !tags.Contains(tag))
                tags.Add(tag);
        return tags;
    }

    static string ReadString(JsonElement element, string name)
    {
        return element.TryGetProperty(name, out var value) && value.ValueKind != JsonValueKind.Null
            ? value.ToString()
            : "";
    }

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

static class SkillFlow
{
    public static StandardAnswer? LoadStandardFromFile(string path)
    {
        try
        {
            using var doc = JsonDocument.Parse(File.ReadAllText(path));
            var steps = NormalizeSteps(doc.RootElement);
            if (steps.Count == 0)
                return null;

            var root = doc.RootElement;
            var schema = TryGetString(root, "schemaVersion");
            var kind = schema.Contains("assembly", StringComparison.OrdinalIgnoreCase) ? "assembly" : "part";
            var modelingPlan = TryGetObject(root, "modelingPlan");
            var summary = modelingPlan.HasValue ? TryGetString(modelingPlan.Value, "summary") : "";
            if (string.IsNullOrWhiteSpace(summary))
                summary = TryGetString(root, "approvedPlanText");

            var title = Path.GetFileNameWithoutExtension(path);
            return new StandardAnswer
            {
                StandardId = MakeStandardId(title),
                Title = title,
                Kind = kind,
                SourcePath = path,
                SourceType = "local_json",
                Tags = new List<string>(),
                Summary = summary,
                Steps = steps,
            };
        }
        catch
        {
            return null;
        }
    }

    public static List<SkillStep> NormalizeSteps(JsonElement payload)
    {
        JsonElement rawSteps;
        if (payload.ValueKind == JsonValueKind.Array)
        {
            rawSteps = payload;
        }
        else if (payload.ValueKind == JsonValueKind.Object && payload.TryGetProperty("steps", out var steps) && steps.ValueKind == JsonValueKind.Array)
        {
            rawSteps = steps;
        }
        else if (payload.ValueKind == JsonValueKind.Object &&
                 payload.TryGetProperty("modelingPlan", out var modelingPlan) &&
                 modelingPlan.ValueKind == JsonValueKind.Object &&
                 modelingPlan.TryGetProperty("steps", out var modelingSteps) &&
                 modelingSteps.ValueKind == JsonValueKind.Array)
        {
            rawSteps = modelingSteps;
        }
        else if (payload.ValueKind == JsonValueKind.Object &&
                 payload.TryGetProperty("modeling_plan", out var modelingPlanSnake) &&
                 modelingPlanSnake.ValueKind == JsonValueKind.Object &&
                 modelingPlanSnake.TryGetProperty("steps", out var modelingStepsSnake) &&
                 modelingStepsSnake.ValueKind == JsonValueKind.Array)
        {
            rawSteps = modelingStepsSnake;
        }
        else
        {
            return new List<SkillStep>();
        }

        var result = new List<SkillStep>();
        var index = 1;
        foreach (var item in rawSteps.EnumerateArray())
        {
            if (item.ValueKind != JsonValueKind.Object)
                continue;

            var skill = FirstNonEmpty(TryGetString(item, "skill"), TryGetString(item, "skillName"), TryGetString(item, "skill_name"));
            if (string.IsNullOrWhiteSpace(skill))
                continue;

            var args = TryGetObject(item, "args") ?? TryGetObject(item, "arguments");
            result.Add(new SkillStep
            {
                Id = FirstNonEmpty(TryGetString(item, "id"), $"step-{index:000}"),
                Order = TryGetInt(item, "step") ?? index,
                Skill = skill,
                Name = FirstNonEmpty(TryGetString(item, "name"), TryGetString(item, "source_feature"), skill),
                Description = FirstNonEmpty(TryGetString(item, "description"), TryGetString(item, "note")),
                Arguments = args.HasValue ? JsonElementToDictionary(args.Value) : new Dictionary<string, object?>(),
                SourceFeature = FirstNonEmpty(TryGetString(item, "source_feature"), TryGetString(item, "sourceFeature"), TryGetString(item, "name")),
                Note = FirstNonEmpty(TryGetString(item, "note"), TryGetString(item, "description")),
            });
            index++;
        }

        return result;
    }

    public static string MakeStandardId(string text)
    {
        var chars = text.ToLowerInvariant().Select(ch =>
            char.IsLetterOrDigit(ch) || ch is >= '\u4e00' and <= '\u9fff' ? ch : '-').ToArray();
        var id = string.Join("-", new string(chars).Split('-', StringSplitOptions.RemoveEmptyEntries));
        return string.IsNullOrWhiteSpace(id) ? "standard" : id[..Math.Min(120, id.Length)];
    }

    public static Dictionary<string, object?> JsonElementToDictionary(JsonElement element)
    {
        var result = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
        foreach (var prop in element.EnumerateObject())
            result[prop.Name] = JsonElementToObject(prop.Value);
        return result;
    }

    static object? JsonElementToObject(JsonElement element)
    {
        return element.ValueKind switch
        {
            JsonValueKind.String => element.GetString(),
            JsonValueKind.Number => element.TryGetInt64(out var i) ? i : element.GetDouble(),
            JsonValueKind.True => true,
            JsonValueKind.False => false,
            JsonValueKind.Array => element.EnumerateArray().Select(JsonElementToObject).ToList(),
            JsonValueKind.Object => JsonElementToDictionary(element),
            _ => null,
        };
    }

    static string TryGetString(JsonElement element, string name)
    {
        return element.ValueKind == JsonValueKind.Object &&
               element.TryGetProperty(name, out var value) &&
               value.ValueKind != JsonValueKind.Null
            ? value.ToString()
            : "";
    }

    static int? TryGetInt(JsonElement element, string name)
    {
        if (element.ValueKind != JsonValueKind.Object ||
            !element.TryGetProperty(name, out var value) ||
            value.ValueKind != JsonValueKind.Number)
            return null;

        return value.TryGetInt32(out var intValue) ? intValue : null;
    }

    static JsonElement? TryGetObject(JsonElement element, string name)
    {
        return element.ValueKind == JsonValueKind.Object &&
               element.TryGetProperty(name, out var value) &&
               value.ValueKind == JsonValueKind.Object
            ? value
            : null;
    }

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

sealed class LessonBuilder
{
    readonly TeachingLanguageDesigner _designer;

    public LessonBuilder(TeachingLanguageDesigner designer)
    {
        _designer = designer;
    }

    public List<LessonStep> Build(StandardAnswer standard)
    {
        var designedScript = _designer.Design(standard);
        var designedByOrder = designedScript.Steps.ToDictionary(step => step.Order);
        return standard.Steps.Select((step, index) =>
        {
            designedByOrder.TryGetValue(index + 1, out var designed);
            return new LessonStep
            {
                StepId = $"lesson-{index + 1:000}",
                Order = index + 1,
                Title = designed?.Title ?? TeachingLanguageDesigner.LabelFor(step.Skill, step.Name),
                VoiceText = designed?.VoiceText ?? TeachingLanguageDesigner.FallbackVoiceText(step),
                ExpectedSkill = step.Skill,
                ExpectedArguments = step.Arguments,
                SourceFeature = step.SourceFeature,
                TeachingIntent = designed?.TeachingIntent ?? "",
                CommonMistake = designed?.CommonMistake ?? "",
                CheckPolicy = new Dictionary<string, object?>
                {
                    ["match"] = "skill_and_core_arguments",
                    ["method_required"] = true,
                    ["geometry_is_secondary"] = true,
                },
            };
        }).ToList();
    }
}

sealed class TeachingLanguageDesigner
{
    public TeachingScript Design(StandardAnswer standard)
    {
        var context = PartContext(standard);
        var steps = standard.Steps.Select((step, index) => DesignStep(standard, step, index + 1, context)).ToList();
        return new TeachingScript
        {
            StandardId = standard.StandardId,
            Title = standard.Title,
            Role = "design-teaching-language-agent",
            Summary = $"面向学生的 SolidWorks 建模语音教学脚本。对象：{standard.Title}，步骤数：{steps.Count}。",
            Steps = steps,
        };
    }

    TeachingScriptStep DesignStep(StandardAnswer standard, SkillStep step, int order, string context)
    {
        var title = LabelFor(step.Skill, step.Name);
        var intent = IntentFor(step, context);
        var command = CommandHintFor(step);
        var mistake = MistakeFor(step);
        var parameter = ParameterPhrase(step);
        var voice = VoiceTextFor(order, title, step, intent, command, parameter, mistake);
        return new TeachingScriptStep
        {
            Order = order,
            Skill = step.Skill,
            SourceFeature = step.SourceFeature,
            Title = title,
            VoiceText = voice,
            TeachingIntent = intent.Trim(),
            SolidWorksCommandHint = command.Trim(),
            CommonMistake = mistake.Trim(),
            KeyParameters = CoreParameters(step),
        };
    }

    static string VoiceTextFor(int order, string title, SkillStep step, string intent, string command, string parameter, string mistake)
    {
        if (string.Equals(step.Skill, "draw_circle_diameter_mm", StringComparison.OrdinalIgnoreCase) &&
            TryGetNumber(step.Arguments, "diameter_mm", out var diameterMm))
        {
            var center = CircleCenterPhrase(step);
            return $"第 {order} 步，{title}。这一步先别急着随手画，直接画一个直径 {FormatMm(diameterMm)} 毫米的圆{center}。在草图工具栏选择圆，画完立刻用智能尺寸把直径标成 {FormatMm(diameterMm)} 毫米。{mistake}";
        }

        return $"第 {order} 步，{title}。{intent}{command}{parameter}{mistake}";
    }

    static string PartContext(StandardAnswer standard)
    {
        if (standard.StandardId == "part-gear-gear-1-high-speed-shaft" || standard.Title.Contains("齿轮1+高速轴", StringComparison.OrdinalIgnoreCase))
            return "这是齿轮1与高速轴一体件，教学重点是先建立轴类回转主体，再建立齿轮轮齿、孔槽和修饰特征，学生要理解每个特征为什么选择这种建模方法。";
        return $"当前零件是 {standard.Title}，教学重点是按标准建模流程逐步完成特征树。";
    }

    static string IntentFor(SkillStep step, string context)
    {
        return step.Skill switch
        {
            "create_new_part" => $"先新建一个零件文件，作为后续所有特征的建模环境。{context}",
            "create_front_plane_sketch" => "在前视基准面进入草图，因为这一步需要建立正视方向的截面或轮廓。",
            "create_top_plane_sketch" => "在上视基准面进入草图，用来建立水平参考方向上的轮廓。",
            "create_right_plane_sketch" => "在右视基准面进入草图，用来建立侧向参考轮廓。",
            "create_face_sketch_by_signature" => "在已有实体面上新建草图，后续特征会依赖这个面的位置和方向。",
            "draw_circle_diameter_mm" => "绘制圆形轮廓，这是轴段、齿轮坯或孔特征的基础几何。",
            "draw_line_mm" => "绘制直线轮廓，用来限定截面边界或辅助构造。",
            "draw_center_line_mm" => "绘制中心线，作为旋转、对称或定位的参考。",
            "draw_arc_center_start_end_mm" => "绘制圆弧，形成齿形、过渡轮廓或局部曲线。",
            "draw_spline_points_mm" => "绘制样条曲线，用来表达非直线的齿形或过渡边界。",
            "exit_sketch" => "退出当前草图，让草图可以被后续特征命令调用。",
            "extrude_boss_mm" => "使用凸台拉伸生成实体材料，这是增加实体体积的步骤。",
            "extrude_cut_mm" => "使用切除拉伸去除材料，这是开孔、开槽或削减实体的步骤。",
            "create_revolve_boss_mm" => "使用旋转凸台生成轴对称实体，这比多次拉伸更符合轴类零件建模思路。",
            "create_revolve_cut_mm" => "使用旋转切除去除轴对称材料，适合环槽或回转凹陷。",
            "create_reference_axis" => "创建基准轴，给圆周阵列、旋转或装配定位提供稳定参考。",
            "create_helix_mm" => "创建螺旋线，作为扫描切除或齿形路径的参考曲线。",
            "swept_cut_profile_path" => "使用扫描切除沿路径去除材料，适合齿形或螺旋路径类特征。",
            "swept_cut_circular_profile_mm" => "使用扫描切除，用圆形截面沿路径去除材料。",
            "swept_boss_circular_profile_mm" => "使用扫描凸台，用圆形截面沿路径增加材料。",
            "circular_pattern_feature" => "使用圆周阵列复制种子特征，形成绕轴重复分布的结构。",
            "linear_pattern_feature_mm" => "使用线性阵列复制种子特征，形成等间距重复结构。",
            "mirror_feature_about_plane" => "使用镜像保证左右或前后对称，避免重复手工建模。",
            "fillet_edges_radius_mm" => "添加圆角，减少尖锐边并符合零件加工和装配要求。",
            "chamfer_edges_angle_distance_mm" => "添加倒角，便于装配导入并去除尖角。",
            _ => "按照标准特征树继续完成本步骤，重点保持建模方法和参数一致。",
        };
    }

    static string CommandHintFor(SkillStep step)
    {
        return step.Skill switch
        {
            "create_new_part" => "在 SolidWorks 中点击新建，选择零件。",
            "create_front_plane_sketch" => "在特征树中选择前视基准面，然后点击草图。",
            "create_top_plane_sketch" => "在特征树中选择上视基准面，然后点击草图。",
            "create_right_plane_sketch" => "在特征树中选择右视基准面，然后点击草图。",
            "create_face_sketch_by_signature" => "选择模型上对应的实体面，然后点击草图。",
            "draw_circle_diameter_mm" => "在草图工具栏选择圆，并用智能尺寸标注直径。",
            "draw_line_mm" => "在草图工具栏选择直线，并按标准轮廓绘制。",
            "draw_center_line_mm" => "在草图工具栏选择中心线。",
            "draw_arc_center_start_end_mm" => "在草图工具栏选择圆弧命令。",
            "draw_spline_points_mm" => "在草图工具栏选择样条曲线。",
            "exit_sketch" => "点击退出草图。",
            "extrude_boss_mm" => "点击特征选项卡中的凸台/基体拉伸。",
            "extrude_cut_mm" => "点击特征选项卡中的拉伸切除。",
            "create_revolve_boss_mm" => "点击特征选项卡中的旋转凸台/基体。",
            "create_revolve_cut_mm" => "点击特征选项卡中的旋转切除。",
            "create_reference_axis" => "点击参考几何体，选择基准轴。",
            "create_helix_mm" => "点击曲线，选择螺旋线/涡状线。",
            "swept_cut_profile_path" or "swept_cut_circular_profile_mm" => "点击特征选项卡中的扫描切除。",
            "swept_boss_circular_profile_mm" => "点击特征选项卡中的扫描凸台/基体。",
            "circular_pattern_feature" => "点击特征选项卡中的圆周阵列。",
            "linear_pattern_feature_mm" => "点击特征选项卡中的线性阵列。",
            "mirror_feature_about_plane" => "点击特征选项卡中的镜像。",
            "fillet_edges_radius_mm" => "点击特征选项卡中的圆角。",
            "chamfer_edges_angle_distance_mm" => "点击特征选项卡中的倒角。",
            _ => "请在 SolidWorks 中选择与本步骤匹配的特征命令。",
        };
    }

    static string MistakeFor(SkillStep step)
    {
        return step.Skill switch
        {
            "extrude_boss_mm" => "注意这是增加材料，不能误用切除拉伸。",
            "extrude_cut_mm" => "注意这是去除材料，不能用抽壳、删除面或凸台拉伸替代。",
            "create_revolve_boss_mm" => "注意回转体优先使用旋转凸台，不要用多个拉伸堆出来。",
            "create_revolve_cut_mm" => "注意环槽类结构应使用旋转切除，不要用普通拉伸切除替代。",
            "swept_cut_profile_path" or "swept_cut_circular_profile_mm" => "注意扫描切除必须同时选对轮廓和路径，不能用放样或普通切除替代。",
            "circular_pattern_feature" => "注意阵列轴和数量必须正确，不能手工复制多个特征。",
            "mirror_feature_about_plane" => "注意镜像基准面要选对，否则对称方向会错。",
            "fillet_edges_radius_mm" => "注意圆角半径要和标准一致，不要漏选边线。",
            "chamfer_edges_angle_distance_mm" => "注意倒角距离和角度要和标准一致。",
            _ => "完成后系统会读取特征树，检查你使用的方法是否符合标准答案。",
        };
    }

    static string ParameterPhrase(SkillStep step)
    {
        var parts = CoreParameters(step);
        return parts.Count == 0 ? "" : "关键参数是：" + string.Join("，", parts.Select(pair => $"{ParameterLabel(pair.Key)} 为 {FormatParameterValue(pair.Key, pair.Value)}")) + "。";
    }

    static string CircleCenterPhrase(SkillStep step)
    {
        if (TryGetNumber(step.Arguments, "cx_mm", out var cx) && TryGetNumber(step.Arguments, "cy_mm", out var cy))
            return $"，圆心放在 X 等于 {FormatMm(cx)} 毫米、Y 等于 {FormatMm(cy)} 毫米的位置";
        if (step.Arguments.TryGetValue("center_model_mm", out var value) && value is List<object?> list && list.Count >= 2 &&
            TryGetNumber(list[0], out var x) && TryGetNumber(list[1], out var y))
            return $"，圆心参考坐标 X 等于 {FormatMm(x)} 毫米、Y 等于 {FormatMm(y)} 毫米";
        return "";
    }

    static string ParameterLabel(string key)
    {
        return key switch
        {
            "diameter_mm" => "直径",
            "depth_mm" => "拉伸深度",
            "height_mm" => "高度",
            "pitch_mm" => "螺距",
            "revolution" => "圈数",
            "radius_mm" => "半径",
            "distance_mm" => "距离",
            "angle_deg" => "角度",
            "count" => "数量",
            "count1" => "数量",
            "spacing1_mm" => "间距",
            "axis_name" => "阵列轴",
            "end_condition" => "终止条件",
            "end_condition_code" => "终止条件代码",
            "through_all" => "完全贯穿",
            _ => key,
        };
    }

    static string FormatParameterValue(string key, object? value)
    {
        if (TryGetNumber(value, out var number))
        {
            if (key.EndsWith("_mm", StringComparison.OrdinalIgnoreCase))
                return $"{FormatMm(number)} 毫米";
            if (key.EndsWith("_deg", StringComparison.OrdinalIgnoreCase))
                return $"{FormatMm(number)} 度";
            return FormatMm(number);
        }

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

    static string FormatMm(double value)
    {
        return Math.Abs(value - Math.Round(value)) < 0.0005
            ? Math.Round(value).ToString("0")
            : value.ToString("0.###");
    }

    static bool TryGetNumber(Dictionary<string, object?> values, string key, out double number)
    {
        number = 0;
        return values.TryGetValue(key, out var value) && TryGetNumber(value, out number);
    }

    static bool TryGetNumber(object? value, out double number)
    {
        number = 0;
        try
        {
            switch (value)
            {
                case double d:
                    number = d;
                    return true;
                case float f:
                    number = f;
                    return true;
                case int i:
                    number = i;
                    return true;
                case long l:
                    number = l;
                    return true;
                case decimal m:
                    number = (double)m;
                    return true;
                case string s:
                    return double.TryParse(s, out number);
                default:
                    return false;
            }
        }
        catch
        {
            return false;
        }
    }

    static Dictionary<string, object?> CoreParameters(SkillStep step)
    {
        var keys = step.Skill switch
        {
            "extrude_boss_mm" or "extrude_cut_mm" => new[] { "depth_mm", "end_condition", "end_condition_code", "through_all" },
            "draw_circle_diameter_mm" => new[] { "diameter_mm" },
            "create_revolve_boss_mm" or "create_revolve_cut_mm" => new[] { "angle_deg" },
            "create_helix_mm" => new[] { "height_mm", "pitch_mm", "revolution" },
            "swept_cut_circular_profile_mm" or "swept_boss_circular_profile_mm" => new[] { "diameter_mm" },
            "circular_pattern_feature" => new[] { "count", "angle_deg", "axis_name" },
            "linear_pattern_feature_mm" => new[] { "count1", "spacing1_mm" },
            "fillet_edges_radius_mm" => new[] { "radius_mm" },
            "chamfer_edges_angle_distance_mm" => new[] { "distance_mm", "angle_deg" },
            _ => Array.Empty<string>(),
        };
        return keys.Where(step.Arguments.ContainsKey).ToDictionary(key => key, key => step.Arguments[key]);
    }

    public static string LabelFor(string skill, string fallback)
    {
        return skill switch
        {
            "create_new_part" => "新建零件",
            "create_front_plane_sketch" => "前视基准面草图",
            "create_top_plane_sketch" => "上视基准面草图",
            "create_right_plane_sketch" => "右视基准面草图",
            "create_face_sketch_by_signature" => "实体面草图",
            "draw_circle_diameter_mm" => "绘制圆并标注直径",
            "draw_line_mm" => "绘制直线",
            "draw_center_line_mm" => "绘制中心线",
            "draw_arc_center_start_end_mm" => "绘制圆弧",
            "draw_spline_points_mm" => "绘制样条曲线",
            "exit_sketch" => "退出草图",
            "extrude_boss_mm" => "凸台拉伸",
            "extrude_cut_mm" => "切除拉伸",
            "create_revolve_boss_mm" => "旋转凸台",
            "create_revolve_cut_mm" => "旋转切除",
            "create_reference_axis" => "创建基准轴",
            "create_helix_mm" => "创建螺旋线",
            "swept_cut_profile_path" => "扫描切除",
            "swept_cut_circular_profile_mm" => "扫描切除",
            "swept_boss_circular_profile_mm" => "扫描凸台",
            "circular_pattern_feature" => "圆周阵列",
            "linear_pattern_feature_mm" => "线性阵列",
            "mirror_feature_about_plane" => "镜像特征",
            "fillet_edges_radius_mm" => "圆角",
            "chamfer_edges_angle_distance_mm" => "倒角",
            _ => string.IsNullOrWhiteSpace(fallback) ? skill : fallback,
        };
    }

    public static string FallbackVoiceText(SkillStep step)
    {
        return $"请在 SolidWorks 中完成：{LabelFor(step.Skill, step.Name)}。系统会检查特征树中的方法和关键参数。";
    }
}

sealed class TeachingScript
{
    public string StandardId { get; set; } = "";
    public string Title { get; set; } = "";
    public string Role { get; set; } = "";
    public string Summary { get; set; } = "";
    public List<TeachingScriptStep> Steps { get; set; } = new();
}

sealed class TeachingScriptStep
{
    public int Order { get; set; }
    public string Skill { get; set; } = "";
    public string SourceFeature { get; set; } = "";
    public string Title { get; set; } = "";
    public string VoiceText { get; set; } = "";
    public string TeachingIntent { get; set; } = "";
    public string SolidWorksCommandHint { get; set; } = "";
    public string CommonMistake { get; set; } = "";
    public Dictionary<string, object?> KeyParameters { get; set; } = new();
}

sealed class TeachingIntroductionBuilder
{
    readonly AppPaths _paths;

    public TeachingIntroductionBuilder(AppPaths paths)
    {
        _paths = paths;
    }

    public TeachingIntroduction Build(StandardAnswer standard)
    {
        var partName = standard.Title;
        var standardPartPath = ResolveStandardPartPath(partName);
        var displayPlanPath = ResolveDisplayPlanPath(partName, standard.SourcePath);
        var planFacts = ExtractPlanFacts(displayPlanPath);
        var frameworkFacts = ExtractFrameworkFacts(partName);
        var role = DescribeRole(partName, frameworkFacts);
        var features = DescribeFeatures(partName, planFacts);
        var modelingFocus = DescribeModelingFocus(standard, planFacts);
        var sourceNotes = new List<string>();
        if (!string.IsNullOrWhiteSpace(frameworkFacts.CategoryName))
            sourceNotes.Add($"知识库三层框架：零件分类={frameworkFacts.CategoryName}");
        if (frameworkFacts.BomNames.Count > 0)
            sourceNotes.Add($"BOM 关联零件：{string.Join("、", frameworkFacts.BomNames.Take(5))}");
        if (!string.IsNullOrWhiteSpace(displayPlanPath))
            sourceNotes.Add($"导入讲解依据：{Path.GetFileName(displayPlanPath)}");
        if (!string.IsNullOrWhiteSpace(standardPartPath))
            sourceNotes.Add($"展示模型：{Path.GetFileName(standardPartPath)}");

        var highlightSummary = planFacts.HighlightTargets.Count > 0
            ? $"知识库识别出 {planFacts.HighlightTargets.Count} 个可讲解的功能面，我会按顺序边高亮边讲。"
            : "当前知识库还没有稳定的功能面语义，我会先按零件整体讲解。";
        var voice = $"先认识标准零件：{partName}。{highlightSummary}";
        return new TeachingIntroduction
        {
            StandardId = standard.StandardId,
            Title = partName,
            StandardPartPath = standardPartPath,
            VoiceText = voice,
            Overview = $"{partName} 是二级齿轮减速器课程中的关键传动零件，本课不是直接背步骤，而是先理解零件在装配体中的功能，再学习为什么要采用这些建模特征。",
            ReducerPosition = role,
            FunctionalRole = BuildFunctionalRole(partName, frameworkFacts),
            KeyFeatures = features,
            ModelingFocus = modelingFocus,
            SourceNotes = sourceNotes,
            KnowledgeSnippets = BuildKnowledgeSnippets(frameworkFacts, planFacts),
            HighlightTargets = planFacts.HighlightTargets,
            KnowledgeCoverage = BuildKnowledgeCoverage(frameworkFacts, planFacts, standardPartPath),
        };
    }

    string ResolveStandardPartPath(string partName)
    {
        if (string.IsNullOrWhiteSpace(partName) || !Directory.Exists(_paths.ReducerModelDir))
            return "";

        var exact = Path.Combine(_paths.ReducerModelDir, partName + ".SLDPRT");
        if (File.Exists(exact))
            return exact;

        return Directory.EnumerateFiles(_paths.ReducerModelDir, "*.sldprt", SearchOption.TopDirectoryOnly)
            .Concat(Directory.EnumerateFiles(_paths.ReducerModelDir, "*.SLDPRT", SearchOption.TopDirectoryOnly))
            .FirstOrDefault(path => Path.GetFileNameWithoutExtension(path).Contains(partName, StringComparison.OrdinalIgnoreCase)) ?? "";
    }

    string ResolveDisplayPlanPath(string partName, string fallback)
    {
        if (!string.IsNullOrWhiteSpace(partName) && Directory.Exists(_paths.ReducerModelDir))
        {
            var exact = Path.Combine(_paths.ReducerModelDir, partName + "_modeling_plan.json");
            if (File.Exists(exact))
                return exact;
        }

        return fallback;
    }

    FrameworkFacts ExtractFrameworkFacts(string partName)
    {
        var facts = new FrameworkFacts();
        if (!File.Exists(_paths.ReducerFrameworkPath))
            return facts;

        try
        {
            using var doc = JsonDocument.Parse(File.ReadAllText(_paths.ReducerFrameworkPath));
            if (doc.RootElement.TryGetProperty("function_implementation_library", out var implementation))
            {
                facts.FunctionModule = ReadString(implementation, "function_module");
                facts.Implementation = ReadString(implementation, "implementation");
            }

            if (!doc.RootElement.TryGetProperty("part_library", out var partLibrary) || partLibrary.ValueKind != JsonValueKind.Array)
                return facts;

            foreach (var category in partLibrary.EnumerateArray())
            {
                var categoryText = category.GetRawText();
                if (!categoryText.Contains(partName, StringComparison.OrdinalIgnoreCase) &&
                    !categoryText.Contains("高速齿轮轴", StringComparison.OrdinalIgnoreCase))
                    continue;

                facts.CategoryName = ReadString(category, "category_name");
                if (category.TryGetProperty("bom_parts", out var bomParts) && bomParts.ValueKind == JsonValueKind.Array)
                {
                    foreach (var part in bomParts.EnumerateArray())
                    {
                        var name = ReadString(part, "part_name");
                        if (!string.IsNullOrWhiteSpace(name))
                            facts.BomNames.Add(name);
                    }
                }

                if (category.TryGetProperty("design_knowledge_candidates", out var candidates) && candidates.ValueKind == JsonValueKind.Array)
                {
                    foreach (var candidate in candidates.EnumerateArray().Take(8))
                    {
                        var title = ReadString(candidate, "title");
                        if (!string.IsNullOrWhiteSpace(title))
                            facts.DesignCandidateTitles.Add(title);
                    }
                }
                break;
            }
        }
        catch
        {
        }

        return facts;
    }

    static PlanFacts ExtractPlanFacts(string sourcePath)
    {
        var facts = new PlanFacts();
        if (string.IsNullOrWhiteSpace(sourcePath) || !File.Exists(sourcePath))
            return facts;

        try
        {
            using var doc = JsonDocument.Parse(File.ReadAllText(sourcePath));
            facts.PartPath = ReadString(doc.RootElement, "partPath");
            if (doc.RootElement.TryGetProperty("sourceAssembly", out var sourceAssembly))
                facts.SourceAssembly = ReadString(sourceAssembly, "name");
            if (!doc.RootElement.TryGetProperty("modelingPlan", out var modelingPlan) ||
                !modelingPlan.TryGetProperty("steps", out var steps) ||
                steps.ValueKind != JsonValueKind.Array)
                return facts;

            foreach (var step in steps.EnumerateArray())
            {
                var skill = ReadString(step, "skillName");
                if (string.IsNullOrWhiteSpace(skill))
                    continue;
                facts.SkillCounts[skill] = facts.SkillCounts.TryGetValue(skill, out var count) ? count + 1 : 1;

                if (!step.TryGetProperty("arguments", out var args) || args.ValueKind != JsonValueKind.Object)
                    continue;
                if (args.TryGetProperty("diameter_mm", out var diameter) && diameter.TryGetDouble(out var diameterValue))
                    facts.DiametersMm.Add(Math.Round(diameterValue, 3));
                if (args.TryGetProperty("depth_mm", out var depth) && depth.TryGetDouble(out var depthValue))
                    facts.DepthMm.Add(Math.Round(depthValue, 3));
                if (args.TryGetProperty("count", out var patternCount) && patternCount.TryGetInt32(out var countValue))
                    facts.PatternCounts.Add(countValue);
            }

            if (doc.RootElement.TryGetProperty("functionalFaces", out var faces) && faces.ValueKind == JsonValueKind.Array)
            {
                foreach (var face in faces.EnumerateArray())
                {
                    var role = ReadString(face, "face_role");
                    if (string.IsNullOrWhiteSpace(role))
                        continue;
                    var surfaceType = ReadString(face, "surface_type");
                    var sourceFeature = ReadString(face, "solidworks_source_feature");
                    var target = new HighlightTarget
                    {
                        Id = $"face-{facts.HighlightTargets.Count + 1:000}",
                        Kind = "functional_face",
                        Role = role,
                        Label = TranslateFaceRole(role),
                        SurfaceType = TranslateSurfaceType(surfaceType),
                        RawSurfaceType = surfaceType,
                        SourceFeature = sourceFeature,
                        HighlightStatus = "knowledge_ready_api_pending",
                        Description = DescribeFaceRole(role, surfaceType, sourceFeature),
                        RadiusMm = ReadDouble(face, "radius_mm"),
                        DiameterMm = ReadDouble(face, "diameter_mm"),
                        AreaMm2 = ReadDouble(face, "area_mm2"),
                        CenterMm = ReadDoubleArray(face, "center_mm"),
                        Axis = ReadDoubleArray(face, "axis"),
                        Normal = ReadDoubleArray(face, "normal"),
                        BBoxMm = ReadDoubleArray(face, "bbox_mm"),
                    };
                    facts.HighlightTargets.Add(target);
                }
            }
        }
        catch
        {
        }

        return facts;
    }

    static string TranslateFaceRole(string role)
    {
        return role switch
        {
            "bearing_mount_face" => "轴承安装圆柱面",
            "oil_ring_mount_face" => "挡油环安装圆柱面",
            "oil_ring_axial_stop_face" => "挡油环轴向止挡面",
            "distance_mate_face" => "距离配合基准面",
            "cylindrical_mate_face" => "圆柱配合面",
            "keyway_bottom_contact_face" => "键槽底部接触面",
            "keyway_side_contact_face" => "键槽侧向接触面",
            "keyway_end_arc_face" => "键槽端部圆弧面",
            _ => role,
        };
    }

    static string TranslateSurfaceType(string surfaceType)
    {
        return surfaceType switch
        {
            "cylinder" => "圆柱面",
            "plane" => "平面",
            "cone" => "圆锥面",
            "sphere" => "球面",
            _ => string.IsNullOrWhiteSpace(surfaceType) ? "未知面类型" : surfaceType,
        };
    }

    static string DescribeFaceRole(string role, string surfaceType, string sourceFeature)
    {
        var label = TranslateFaceRole(role);
        var surface = TranslateSurfaceType(surfaceType);
        var source = string.IsNullOrWhiteSpace(sourceFeature) ? "" : $"，来源特征是 {sourceFeature}";
        return $"{label}，面类型为{surface}{source}。";
    }

    static string DescribeRole(string partName, FrameworkFacts facts)
    {
        if (partName.Contains("齿轮1+高速轴", StringComparison.OrdinalIgnoreCase))
            return "它位于减速器的高速级，是把输入端高速旋转传入齿轮传动链的齿轮轴类零件；在装配中它与轴承、端盖以及后续啮合齿轮共同确定高速级的传动位置。";
        if (!string.IsNullOrWhiteSpace(facts.FunctionModule) || !string.IsNullOrWhiteSpace(facts.Implementation))
            return $"它属于{facts.FunctionModule}中的{facts.Implementation}相关零件。";
        return "它是二级齿轮减速器装配体中的一个非标准零件，位置和形状都服务于整机传动与装配关系。";
    }

    static string BuildFunctionalRole(string partName, FrameworkFacts facts)
    {
        if (partName.Contains("齿轮1+高速轴", StringComparison.OrdinalIgnoreCase))
            return "这个零件同时承担“轴”和“小齿轮”的作用：轴段用于支承和定位，齿轮段用于参与高速级啮合传动。建模时要把回转轴体、齿轮轮廓、齿槽/螺旋线等结构作为一个连续零件来理解。";
        return "这个零件的功能需要结合减速器装配关系理解：外形不是孤立几何，而是由传动、定位、支承、密封或连接需求共同决定。";
    }

    static string DescribeFeatures(string partName, PlanFacts facts)
    {
        var parts = new List<string>();
        if (facts.SkillCounts.ContainsKey("extrude_boss_mm"))
            parts.Add("包含凸台拉伸形成的基础实体");
        if (facts.SkillCounts.ContainsKey("create_helix_mm"))
            parts.Add("包含螺旋线作为齿形或扫描路径");
        if (facts.SkillCounts.ContainsKey("swept_cut_profile_path") || facts.SkillCounts.ContainsKey("swept_cut_circular_profile_mm"))
            parts.Add("包含扫描切除形成的齿槽或沟槽");
        if (facts.SkillCounts.ContainsKey("circular_pattern_feature"))
            parts.Add("包含圆周阵列形成的重复齿形结构");
        if (facts.SkillCounts.ContainsKey("fillet_edges_radius_mm"))
            parts.Add("包含圆角用于过渡和加工合理性");

        var diameterText = facts.DiametersMm.Count > 0
            ? $"提取到的典型直径包括 {string.Join("、", facts.DiametersMm.Distinct().Take(5).Select(v => $"{v:0.###}mm"))}"
            : "";
        var featureText = parts.Count == 0 ? "主要由草图、实体特征和修饰特征组合完成" : string.Join("，", parts);
        return string.IsNullOrWhiteSpace(diameterText) ? $"它的基本特征是：{featureText}。" : $"它的基本特征是：{featureText}；{diameterText}。";
    }

    static string DescribeModelingFocus(StandardAnswer standard, PlanFacts facts)
    {
        var stepCount = standard.Steps.Count > 0 ? standard.Steps.Count : facts.SkillCounts.Values.Sum();
        var focus = new List<string> { "先建立基准草图和基础实体" };
        if (facts.SkillCounts.ContainsKey("create_helix_mm"))
            focus.Add("再用螺旋线控制齿形路径");
        if (facts.SkillCounts.ContainsKey("swept_cut_profile_path") || facts.SkillCounts.ContainsKey("swept_cut_circular_profile_mm"))
            focus.Add("用扫描切除表达齿槽加工思想");
        if (facts.SkillCounts.ContainsKey("circular_pattern_feature"))
            focus.Add("最后用圆周阵列保证齿形均布");
        focus.Add("修饰边角并保持特征树顺序清晰");
        return $"本标准答案共有 {stepCount} 个建模步骤。建模重点是：{string.Join("，", focus)}。";
    }

    static List<string> BuildKnowledgeSnippets(FrameworkFacts frameworkFacts, PlanFacts planFacts)
    {
        var snippets = new List<string>();
        if (!string.IsNullOrWhiteSpace(frameworkFacts.FunctionModule))
            snippets.Add($"功能模块：{frameworkFacts.FunctionModule}");
        if (!string.IsNullOrWhiteSpace(frameworkFacts.Implementation))
            snippets.Add($"实现对象：{frameworkFacts.Implementation}");
        if (!string.IsNullOrWhiteSpace(frameworkFacts.CategoryName))
            snippets.Add($"知识库零件分类：{frameworkFacts.CategoryName}");
        if (!string.IsNullOrWhiteSpace(planFacts.SourceAssembly))
            snippets.Add($"来源装配体：{planFacts.SourceAssembly}");
        if (frameworkFacts.DesignCandidateTitles.Count > 0)
            snippets.Add($"关联设计知识：{string.Join("、", frameworkFacts.DesignCandidateTitles.Take(3))}");
        return snippets;
    }

    static KnowledgeCoverage BuildKnowledgeCoverage(FrameworkFacts frameworkFacts, PlanFacts planFacts, string standardPartPath)
    {
        var missing = new List<string>();
        if (string.IsNullOrWhiteSpace(standardPartPath))
            missing.Add("没有找到可直接打开的标准零件文件");
        if (string.IsNullOrWhiteSpace(frameworkFacts.CategoryName))
            missing.Add("没有找到该零件在三层框架中的分类");
        if (planFacts.HighlightTargets.Count == 0)
            missing.Add("没有功能面语义，无法做面级讲解/高亮");

        return new KnowledgeCoverage
        {
            HasStandardPart = !string.IsNullOrWhiteSpace(standardPartPath),
            HasFrameworkCategory = !string.IsNullOrWhiteSpace(frameworkFacts.CategoryName),
            HasFunctionalFaces = planFacts.HighlightTargets.Count > 0,
            FunctionalFaceCount = planFacts.HighlightTargets.Count,
            HighlightSupport = planFacts.HighlightTargets.Count > 0 ? "知识库有功能面语义；SolidWorks 面级高亮还需要按面签名验证。" : "当前只能做零件整体/特征级讲解。",
            MissingItems = missing,
        };
    }

    static string ReadString(JsonElement element, string name)
    {
        return element.ValueKind == JsonValueKind.Object &&
               element.TryGetProperty(name, out var value) &&
               value.ValueKind != JsonValueKind.Null
            ? value.ToString()
            : "";
    }

    static double? ReadDouble(JsonElement element, string name)
    {
        if (element.ValueKind != JsonValueKind.Object ||
            !element.TryGetProperty(name, out var value) ||
            value.ValueKind != JsonValueKind.Number)
            return null;

        return value.TryGetDouble(out var number) ? number : null;
    }

    static List<double> ReadDoubleArray(JsonElement element, string name)
    {
        if (element.ValueKind != JsonValueKind.Object ||
            !element.TryGetProperty(name, out var value) ||
            value.ValueKind != JsonValueKind.Array)
            return new List<double>();

        var result = new List<double>();
        foreach (var item in value.EnumerateArray())
        {
            if (item.ValueKind == JsonValueKind.Number && item.TryGetDouble(out var number))
                result.Add(number);
        }

        return result;
    }

    sealed class FrameworkFacts
    {
        public string FunctionModule { get; set; } = "";
        public string Implementation { get; set; } = "";
        public string CategoryName { get; set; } = "";
        public List<string> BomNames { get; } = new();
        public List<string> DesignCandidateTitles { get; } = new();
    }

    sealed class PlanFacts
    {
        public string PartPath { get; set; } = "";
        public string SourceAssembly { get; set; } = "";
        public Dictionary<string, int> SkillCounts { get; } = new(StringComparer.OrdinalIgnoreCase);
        public List<double> DiametersMm { get; } = new();
        public List<double> DepthMm { get; } = new();
        public List<int> PatternCounts { get; } = new();
        public List<HighlightTarget> HighlightTargets { get; } = new();
    }
}

sealed class TeachingIntroduction
{
    public string StandardId { get; set; } = "";
    public string Title { get; set; } = "";
    public string StandardPartPath { get; set; } = "";
    public string VoiceText { get; set; } = "";
    public string Overview { get; set; } = "";
    public string ReducerPosition { get; set; } = "";
    public string FunctionalRole { get; set; } = "";
    public string KeyFeatures { get; set; } = "";
    public string ModelingFocus { get; set; } = "";
    public List<string> SourceNotes { get; set; } = new();
    public List<string> KnowledgeSnippets { get; set; } = new();
    public List<HighlightTarget> HighlightTargets { get; set; } = new();
    public KnowledgeCoverage KnowledgeCoverage { get; set; } = new();
}

sealed class HighlightTarget
{
    public string Id { get; set; } = "";
    public string Kind { get; set; } = "";
    public string Role { get; set; } = "";
    public string Label { get; set; } = "";
    public string SurfaceType { get; set; } = "";
    public string RawSurfaceType { get; set; } = "";
    public string SourceFeature { get; set; } = "";
    public string Description { get; set; } = "";
    public string HighlightStatus { get; set; } = "";
    public double? RadiusMm { get; set; }
    public double? DiameterMm { get; set; }
    public double? AreaMm2 { get; set; }
    public List<double> CenterMm { get; set; } = new();
    public List<double> Axis { get; set; } = new();
    public List<double> Normal { get; set; } = new();
    public List<double> BBoxMm { get; set; } = new();
}

sealed class KnowledgeCoverage
{
    public bool HasStandardPart { get; set; }
    public bool HasFrameworkCategory { get; set; }
    public bool HasFunctionalFaces { get; set; }
    public int FunctionalFaceCount { get; set; }
    public string HighlightSupport { get; set; } = "";
    public List<string> MissingItems { get; set; } = new();
}

sealed record HighlightResult(
    bool Ok,
    string Level,
    string MatchedName,
    string Message,
    double Score,
    string VoiceText);

sealed class FeatureGrader
{
    readonly Dictionary<string, string[]> _coreArguments = new(StringComparer.OrdinalIgnoreCase)
    {
        ["extrude_boss_mm"] = new[] { "depth_mm", "end_condition_code", "through_all" },
        ["extrude_cut_mm"] = new[] { "depth_mm", "end_condition_code", "through_all" },
        ["shell_remove_face_at_point_mm"] = new[] { "thickness_mm" },
        ["draw_circle_diameter_mm"] = new[] { "diameter_mm", "center_model_mm", "cx_mm", "cy_mm" },
        ["fillet_edges_radius_mm"] = new[] { "radius_mm" },
        ["chamfer_edges_angle_distance_mm"] = new[] { "distance_mm", "angle_deg" },
        ["linear_pattern_feature_mm"] = new[] { "count1", "spacing1_mm" },
        ["circular_pattern_feature"] = new[] { "count", "angle_deg" },
    };

    public StepCompareResult CompareStep(LessonStep expected, SkillStep? observed)
    {
        if (observed == null)
        {
            return new StepCompareResult
            {
                Ok = false,
                Score = 0,
                ExpectedStep = expected,
                FeedbackText = $"没有检测到本步骤的操作。请执行：{expected.Title}。",
                Issues = new List<CompareIssue>
                {
                    new()
                    {
                        Code = "missing_operation",
                        Message = $"Expected {expected.ExpectedSkill}, but no user feature was observed.",
                        Expected = new Dictionary<string, object?> { ["skill"] = expected.ExpectedSkill },
                    },
                },
            };
        }

        var issues = new List<CompareIssue>();
        if (!string.Equals(expected.ExpectedSkill, observed.Skill, StringComparison.OrdinalIgnoreCase))
        {
            issues.Add(new CompareIssue
            {
                Code = "wrong_feature_method",
                Message = $"方法错误：标准答案要求 {expected.ExpectedSkill}，学生实际使用 {observed.Skill}。",
                Expected = new Dictionary<string, object?> { ["skill"] = expected.ExpectedSkill, ["title"] = expected.Title },
                Actual = new Dictionary<string, object?> { ["skill"] = observed.Skill, ["name"] = observed.Name, ["source_feature"] = observed.SourceFeature },
            });
        }

        issues.AddRange(CompareCoreArguments(expected.ExpectedSkill, expected.ExpectedArguments, observed.Arguments));
        var ok = issues.Count == 0;
        return new StepCompareResult
        {
            Ok = ok,
            Score = ok ? 100.0 : Math.Max(0.0, 60.0 - 20.0 * issues.Count),
            ExpectedStep = expected,
            ObservedStep = observed,
            Issues = issues,
            FeedbackText = ok
                ? $"正确。你使用了本步骤要求的 {expected.Title}，可以进入下一步。"
                : $"{issues[0].Message} 本步骤要求：{expected.VoiceText}",
        };
    }

    public GradeReport GradeSequence(List<LessonStep> expected, List<SkillStep> observed)
    {
        var issues = new List<CompareIssue>();
        var passed = 0;
        for (var i = 0; i < expected.Count; i++)
        {
            var result = CompareStep(expected[i], i < observed.Count ? observed[i] : null);
            if (result.Ok)
                passed++;
            issues.AddRange(result.Issues);
        }

        var score = expected.Count == 0 ? 100.0 : Math.Round((double)passed / expected.Count * 100.0, 2);
        return new GradeReport
        {
            Ok = score >= 90.0 && issues.All(issue => issue.Severity != "error"),
            Score = score,
            PassedSteps = passed,
            TotalSteps = expected.Count,
            Issues = issues,
            Summary = $"Passed {passed}/{expected.Count} feature-method steps.",
        };
    }

    IEnumerable<CompareIssue> CompareCoreArguments(string skill, Dictionary<string, object?> expected, Dictionary<string, object?> actual)
    {
        if (!_coreArguments.TryGetValue(skill, out var keys))
            yield break;

        foreach (var key in keys)
        {
            if (!expected.ContainsKey(key))
                continue;
            if (!actual.ContainsKey(key))
            {
                yield return new CompareIssue
                {
                    Code = "missing_parameter",
                    Message = $"参数缺失：{key}。",
                    Expected = new Dictionary<string, object?> { [key] = expected[key] },
                };
                continue;
            }

            if (!ValuesClose(expected[key], actual[key]))
            {
                yield return new CompareIssue
                {
                    Code = "wrong_parameter",
                    Message = $"参数错误：{key} 标准值为 {expected[key]}，实际值为 {actual[key]}。",
                    Expected = new Dictionary<string, object?> { [key] = expected[key] },
                    Actual = new Dictionary<string, object?> { [key] = actual[key] },
                };
            }
        }
    }

    static bool ValuesClose(object? expected, object? actual, double tolerance = 0.05)
    {
        if (TryDouble(expected, out var expectedDouble) && TryDouble(actual, out var actualDouble))
            return Math.Abs(expectedDouble - actualDouble) <= tolerance;

        if (expected is List<object?> expectedList && actual is List<object?> actualList && expectedList.Count == actualList.Count)
            return expectedList.Zip(actualList).All(pair => ValuesClose(pair.First, pair.Second, tolerance));

        return Convert.ToString(expected) == Convert.ToString(actual);
    }

    static bool TryDouble(object? value, out double result)
    {
        result = 0;
        return value switch
        {
            double d => Set(out result, d),
            float f => Set(out result, f),
            int i => Set(out result, i),
            long l => Set(out result, l),
            decimal m => Set(out result, (double)m),
            string s => double.TryParse(s, out result),
            _ => false,
        };
    }

    static bool Set(out double target, double value)
    {
        target = value;
        return true;
    }
}

sealed class SessionStore
{
    readonly ConcurrentDictionary<string, LearningSession> _items = new();

    public LearningSession Start(string mode, StandardAnswer standard, LessonBuilder lessonBuilder)
    {
        var session = new LearningSession
        {
            SessionId = "sess-" + Guid.NewGuid().ToString("N")[..10],
            Mode = mode,
            StandardId = standard.StandardId,
            Title = standard.Title,
            CreatedAt = DateTime.Now,
            LessonSteps = lessonBuilder.Build(standard),
        };
        _items[session.SessionId] = session;
        return session;
    }

    public bool TryGet(string sessionId, out LearningSession session) => _items.TryGetValue(sessionId, out session!);

    public StepCompareResult SubmitTeachingObservation(LearningSession session, List<SkillStep> observed, FeatureGrader grader)
    {
        session.ObservedSteps.AddRange(observed);
        if (session.CurrentStepIndex >= session.LessonSteps.Count)
        {
            session.Status = "completed";
            var completed = new StepCompareResult { Ok = true, Score = 100, FeedbackText = "教学步骤已经完成。" };
            session.Reports.Add(completed);
            return completed;
        }

        var expected = session.LessonSteps[session.CurrentStepIndex];
        var actual = observed.LastOrDefault();
        var result = grader.CompareStep(expected, actual);
        session.Reports.Add(result);
        if (result.Ok)
        {
            session.CurrentStepIndex++;
            if (session.CurrentStepIndex >= session.LessonSteps.Count)
                session.Status = "completed";
        }
        return result;
    }

    public StepCompareResult PollTeachingObservation(LearningSession session, List<SkillStep> observed, FeatureGrader grader)
    {
        if (session.CurrentStepIndex >= session.LessonSteps.Count)
        {
            session.Status = "completed";
            return new StepCompareResult { Ok = true, Score = 100, FeedbackText = "教学步骤已经完成。" };
        }

        var expected = session.LessonSteps[session.CurrentStepIndex];
        var actual = observed.LastOrDefault();
        var result = grader.CompareStep(expected, actual);
        if (!result.Ok)
            return result;

        session.ObservedSteps.AddRange(observed);
        session.Reports.Add(result);
        session.CurrentStepIndex++;
        if (session.CurrentStepIndex >= session.LessonSteps.Count)
            session.Status = "completed";
        return result;
    }

    public void AcceptCurrentStepFromLiveState(LearningSession session, StepCompareResult result)
    {
        session.Reports.Add(result);
        session.CurrentStepIndex++;
        if (session.CurrentStepIndex >= session.LessonSteps.Count)
            session.Status = "completed";
    }

    public StepCompareResult? CheckActiveDocumentForCurrentStep(LearningSession session, SolidWorksDocumentStatus documentStatus)
    {
        if (session.CurrentStepIndex >= session.LessonSteps.Count)
            return null;

        var expected = session.LessonSteps[session.CurrentStepIndex];
        var expectsPartModeling = session.StandardId.StartsWith("part-", StringComparison.OrdinalIgnoreCase);
        if (!expectsPartModeling)
            return null;

        if (!documentStatus.Connected)
            return WrongDocumentResult(expected, "没有连接到 SolidWorks。请先启动 SolidWorks，然后按照语音提示继续。", documentStatus);

        if (!documentStatus.HasDocument)
            return WrongDocumentResult(expected, "还没有检测到打开的 SolidWorks 文档。当前步骤需要新建零件，请点击新建并选择零件。", documentStatus);

        if (documentStatus.TypeName == "part")
        {
            var sketchResult = CheckActiveSketchForCurrentStep(expected, documentStatus);
            if (sketchResult != null)
                return sketchResult;
            var liveCircleResult = CheckActiveSketchCircleForCurrentStep(expected, documentStatus);
            if (liveCircleResult != null)
                return liveCircleResult;
            var exitSketchResult = CheckExitSketchForCurrentStep(expected, documentStatus);
            if (exitSketchResult != null)
                return exitSketchResult;
            return null;
        }

        var message = documentStatus.TypeName switch
        {
            "assembly" => "你点错了。当前打开的是装配体，但本步骤要求新建零件。请关闭或切回正确窗口，点击新建，然后选择零件，不要选择装配体。",
            "drawing" => "你点错了。当前打开的是工程图，但本步骤要求新建零件。请点击新建，然后选择零件。",
            _ => $"当前活动文档类型不是零件，检测到的是 {documentStatus.TypeName}。本步骤要求在零件环境中操作。",
        };
        return WrongDocumentResult(expected, message, documentStatus);
    }

    static StepCompareResult? CheckActiveSketchForCurrentStep(LessonStep expected, SolidWorksDocumentStatus documentStatus)
    {
        var expectedReference = expected.ExpectedSkill switch
        {
            "create_front_plane_sketch" => "front",
            "create_top_plane_sketch" => "top",
            "create_right_plane_sketch" => "right",
            _ => "",
        };

        if (string.IsNullOrWhiteSpace(expectedReference))
            return null;

        if (!documentStatus.ActiveSketch)
            return WrongDocumentResult(expected, $"当前还没有进入草图编辑。请在 SolidWorks 中选择 {PlaneDisplayName(expectedReference)}，然后点击草图。", documentStatus);

        if (SketchReferenceMatches(documentStatus.ActiveSketchReferenceName, expectedReference))
        {
            return new StepCompareResult
            {
                Ok = true,
                Score = 100,
                ExpectedStep = expected,
                FeedbackText = $"正确。你已经在 {PlaneDisplayName(expectedReference)} 进入草图，可以继续下一步。",
            };
        }

        return WrongDocumentResult(
            expected,
            $"你进入了草图，但草图基准面不对。当前检测到的是 {DisplayOrUnknown(documentStatus.ActiveSketchReferenceName)}，本步骤要求选择 {PlaneDisplayName(expectedReference)}。",
            documentStatus);
    }

    static StepCompareResult? CheckActiveSketchCircleForCurrentStep(LessonStep expected, SolidWorksDocumentStatus documentStatus)
    {
        if (!string.Equals(expected.ExpectedSkill, "draw_circle_diameter_mm", StringComparison.OrdinalIgnoreCase))
            return null;

        if (!documentStatus.ActiveSketch)
            return WrongLiveSketchResult(expected, "当前已经不在草图编辑状态。请重新进入当前草图，系统会实时检测圆和直径尺寸。", documentStatus, "not_editing_sketch");

        var circles = documentStatus.ActiveSketchCircles.Where(circle => circle.IsFullCircle).ToList();
        if (circles.Count == 0)
            return WrongLiveSketchResult(expected, "还没有检测到完整圆。请在当前草图中使用圆命令绘制圆，不要用圆弧或样条替代。", documentStatus, "missing_circle");

        if (!TryGetExpectedDouble(expected.ExpectedArguments, "diameter_mm", out var expectedDiameterMm))
        {
            var firstCircle = circles[0];
            return CorrectLiveSketchResult(expected, firstCircle, "正确。已经检测到完整圆，可以继续下一步。");
        }

        var toleranceMm = Math.Max(0.05, expectedDiameterMm * 0.005);
        var matchingCircle = circles
            .OrderBy(circle => Math.Abs(circle.DiameterMm - expectedDiameterMm))
            .FirstOrDefault(circle => Math.Abs(circle.DiameterMm - expectedDiameterMm) <= toleranceMm);
        if (matchingCircle == null)
        {
            var detected = string.Join("、", circles.Select(circle => $"{circle.DiameterMm:0.###}mm"));
            return WrongLiveSketchResult(
                expected,
                $"检测到圆，但直径不对。当前圆直径为 {detected}，本步骤要求直径 {expectedDiameterMm:0.###}mm。请用智能尺寸把圆标注并修改到标准值。",
                documentStatus,
                "wrong_circle_diameter");
        }

        var hasMatchingDimension = documentStatus.ActiveSketchDimensions.Any(dimension => Math.Abs(dimension.ValueMm - expectedDiameterMm) <= toleranceMm);
        var message = hasMatchingDimension
            ? $"正确。已经检测到直径 {expectedDiameterMm:0.###}mm 的圆和尺寸标注，可以退出草图。"
            : $"正确。已经检测到直径 {expectedDiameterMm:0.###}mm 的圆，可以退出草图。";
        return CorrectLiveSketchResult(expected, matchingCircle, message);
    }

    static StepCompareResult? CheckExitSketchForCurrentStep(LessonStep expected, SolidWorksDocumentStatus documentStatus)
    {
        if (!string.Equals(expected.ExpectedSkill, "exit_sketch", StringComparison.OrdinalIgnoreCase))
            return null;

        if (documentStatus.ActiveSketch)
            return WrongLiveSketchResult(expected, "圆已经完成。现在请点击左上角“退出草图”，让草图进入后续特征建模流程。", documentStatus, "still_editing_sketch");

        return new StepCompareResult
        {
            Ok = true,
            Score = 100,
            ExpectedStep = expected,
            ObservedStep = new SkillStep
            {
                Skill = "exit_sketch",
                Name = expected.Title,
                Description = "已退出草图编辑",
            },
            FeedbackText = "正确。已经退出草图，可以继续下一步特征操作。",
        };
    }

    static StepCompareResult CorrectLiveSketchResult(LessonStep expected, SketchCircleInfo circle, string message)
    {
        return new StepCompareResult
        {
            Ok = true,
            Score = 100,
            ExpectedStep = expected,
            ObservedStep = new SkillStep
            {
                Skill = "draw_circle_diameter_mm",
                Name = expected.Title,
                Description = "活动草图实时检测到圆",
                Arguments = new Dictionary<string, object?>
                {
                    ["diameter_mm"] = circle.DiameterMm,
                    ["cx_mm"] = circle.CenterXLocalMm,
                    ["cy_mm"] = circle.CenterYLocalMm,
                },
            },
            FeedbackText = message,
        };
    }

    static StepCompareResult WrongLiveSketchResult(LessonStep expected, string message, SolidWorksDocumentStatus documentStatus, string code)
    {
        return new StepCompareResult
        {
            Ok = false,
            Score = 0,
            ExpectedStep = expected,
            FeedbackText = message,
            Issues = new List<CompareIssue>
            {
                new()
                {
                    Code = code,
                    Message = message,
                    Expected = new Dictionary<string, object?>
                    {
                        ["skill"] = expected.ExpectedSkill,
                        ["arguments"] = expected.ExpectedArguments,
                    },
                    Actual = new Dictionary<string, object?>
                    {
                        ["active_sketch"] = documentStatus.ActiveSketch,
                        ["circles"] = documentStatus.ActiveSketchCircles,
                        ["dimensions"] = documentStatus.ActiveSketchDimensions,
                    },
                },
            },
        };
    }

    static bool TryGetExpectedDouble(Dictionary<string, object?> args, string key, out double value)
    {
        value = 0;
        if (!args.TryGetValue(key, out var raw) || raw == null)
            return false;

        try
        {
            if (raw is JsonElement element)
            {
                if (element.ValueKind == JsonValueKind.Number && element.TryGetDouble(out value))
                    return true;
                if (element.ValueKind == JsonValueKind.String && double.TryParse(element.GetString(), out value))
                    return true;
                return false;
            }

            value = Convert.ToDouble(raw);
            return true;
        }
        catch
        {
            return raw is string text && double.TryParse(text, out value);
        }
    }

    static bool SketchReferenceMatches(string referenceName, string expectedReference)
    {
        var text = referenceName.ToLowerInvariant();
        return expectedReference switch
        {
            "front" => text.Contains("front") || referenceName.Contains("前视"),
            "top" => text.Contains("top") || referenceName.Contains("上视"),
            "right" => text.Contains("right") || referenceName.Contains("右视"),
            _ => false,
        };
    }

    static string PlaneDisplayName(string reference)
    {
        return reference switch
        {
            "front" => "前视基准面",
            "top" => "上视基准面",
            "right" => "右视基准面",
            _ => "指定基准面",
        };
    }

    static string DisplayOrUnknown(string value) => string.IsNullOrWhiteSpace(value) ? "未识别的基准面" : value;

    static StepCompareResult WrongDocumentResult(LessonStep expected, string message, SolidWorksDocumentStatus documentStatus)
    {
        return new StepCompareResult
        {
            Ok = false,
            Score = 0,
            ExpectedStep = expected,
            FeedbackText = message,
            Issues = new List<CompareIssue>
            {
                new()
                {
                    Code = "wrong_solidworks_document_type",
                    Message = message,
                    Expected = new Dictionary<string, object?>
                    {
                        ["document_type"] = "part",
                        ["skill"] = expected.ExpectedSkill,
                    },
                    Actual = new Dictionary<string, object?>
                    {
                        ["connected"] = documentStatus.Connected,
                        ["has_document"] = documentStatus.HasDocument,
                        ["document_type"] = documentStatus.TypeName,
                        ["title"] = documentStatus.Title,
                    },
                },
            },
        };
    }

    public GradeReport SubmitTest(LearningSession session, List<SkillStep> observed, FeatureGrader grader)
    {
        session.ObservedSteps = observed;
        var report = grader.GradeSequence(session.LessonSteps, observed);
        session.FinalReport = report;
        session.Status = "completed";
        return report;
    }
}

sealed class SolidWorksExtractor
{
    readonly AppPaths _paths;

    public SolidWorksExtractor(AppPaths paths)
    {
        _paths = paths;
    }

    public Task<List<SkillStep>> AuditActivePartAsync()
    {
        var outputDir = Path.Combine(_paths.RuntimeDir, "audits", "active-part");
        Directory.CreateDirectory(outputDir);
        return RunPartAuditAsync(new[] { "--active", "--output-dir", outputDir }, outputDir);
    }

    public Task<List<SkillStep>> AuditPartAsync(string partPath)
    {
        if (!File.Exists(partPath))
            throw new FileNotFoundException(partPath);

        var outputDir = Path.Combine(_paths.RuntimeDir, "audits", Path.GetFileNameWithoutExtension(partPath));
        Directory.CreateDirectory(outputDir);
        return RunPartAuditAsync(new[] { partPath, "--output-dir", outputDir }, outputDir);
    }

    async Task<List<SkillStep>> RunPartAuditAsync(string[] args, string outputDir)
    {
        var startInfo = new ProcessStartInfo
        {
            FileName = "dotnet",
            WorkingDirectory = _paths.Agent4Root,
            UseShellExecute = false,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
        };
        startInfo.ArgumentList.Add("run");
        startInfo.ArgumentList.Add("--project");
        startInfo.ArgumentList.Add(_paths.PartFeatureAuditProject);
        startInfo.ArgumentList.Add("--");
        foreach (var arg in args)
            startInfo.ArgumentList.Add(arg);

        using var process = Process.Start(startInfo) ?? throw new InvalidOperationException("Failed to start PartFeatureAudit.");
        var stdout = await process.StandardOutput.ReadToEndAsync();
        var stderr = await process.StandardError.ReadToEndAsync();
        await process.WaitForExitAsync();

        if (process.ExitCode != 0)
            throw new InvalidOperationException($"PartFeatureAudit failed. exit={process.ExitCode}\n{stdout}\n{stderr}");

        return ReadLatestSteps(outputDir);
    }

    static List<SkillStep> ReadLatestSteps(string outputDir)
    {
        var candidates = Directory.EnumerateFiles(outputDir, "*_modeling_plan.json")
            .Concat(Directory.EnumerateFiles(outputDir, "*_skill_flow.json"))
            .Select(path => new FileInfo(path))
            .OrderByDescending(file => file.LastWriteTimeUtc)
            .ToList();

        if (candidates.Count == 0)
            throw new FileNotFoundException($"No audit JSON generated under {outputDir}");

        using var doc = JsonDocument.Parse(File.ReadAllText(candidates[0].FullName));
        return SkillFlow.NormalizeSteps(doc.RootElement);
    }
}

sealed class StartSessionRequest
{
    public string StandardId { get; set; } = "";
}

sealed class ObservationRequest
{
    public List<Dictionary<string, object?>> Steps { get; set; } = new();
}

sealed class AuditPartRequest
{
    public string PartPath { get; set; } = "";
}

sealed class SubmitTestRequest
{
    public List<Dictionary<string, object?>> Steps { get; set; } = new();
    public string PartPath { get; set; } = "";
}

sealed class SkillStep
{
    public string Id { get; set; } = "";
    public int Order { get; set; }
    public string Skill { get; set; } = "";
    public string Name { get; set; } = "";
    public string Description { get; set; } = "";
    public Dictionary<string, object?> Arguments { get; set; } = new(StringComparer.OrdinalIgnoreCase);
    public string SourceFeature { get; set; } = "";
    public string Note { get; set; } = "";
}

sealed class StandardAnswer
{
    public string StandardId { get; set; } = "";
    public string Title { get; set; } = "";
    public string Kind { get; set; } = "unknown";
    public string SourcePath { get; set; } = "";
    public string SourceType { get; set; } = "skillflow";
    public List<string> Tags { get; set; } = new();
    public string Summary { get; set; } = "";
    public List<SkillStep> Steps { get; set; } = new();
}

sealed class LessonStep
{
    public string StepId { get; set; } = "";
    public int Order { get; set; }
    public string Title { get; set; } = "";
    public string VoiceText { get; set; } = "";
    public string ExpectedSkill { get; set; } = "";
    public Dictionary<string, object?> ExpectedArguments { get; set; } = new(StringComparer.OrdinalIgnoreCase);
    public string SourceFeature { get; set; } = "";
    public string TeachingIntent { get; set; } = "";
    public string CommonMistake { get; set; } = "";
    public Dictionary<string, object?> CheckPolicy { get; set; } = new();
}

sealed class CompareIssue
{
    public string Severity { get; set; } = "error";
    public string Code { get; set; } = "";
    public string Message { get; set; } = "";
    public Dictionary<string, object?> Expected { get; set; } = new();
    public Dictionary<string, object?> Actual { get; set; } = new();
}

sealed class StepCompareResult
{
    public bool Ok { get; set; }
    public double Score { get; set; }
    public LessonStep? ExpectedStep { get; set; }
    public SkillStep? ObservedStep { get; set; }
    public List<CompareIssue> Issues { get; set; } = new();
    public string FeedbackText { get; set; } = "";
}

sealed class GradeReport
{
    public bool Ok { get; set; }
    public double Score { get; set; }
    public double MaxScore { get; set; } = 100.0;
    public int PassedSteps { get; set; }
    public int TotalSteps { get; set; }
    public List<CompareIssue> Issues { get; set; } = new();
    public string Summary { get; set; } = "";
}

sealed class LearningSession
{
    public string SessionId { get; set; } = "";
    public string Mode { get; set; } = "";
    public string StandardId { get; set; } = "";
    public string Title { get; set; } = "";
    public int CurrentStepIndex { get; set; }
    public DateTime CreatedAt { get; set; }
    public List<LessonStep> LessonSteps { get; set; } = new();
    public List<SkillStep> ObservedSteps { get; set; } = new();
    public List<StepCompareResult> Reports { get; set; } = new();
    public GradeReport? FinalReport { get; set; }
    public string Status { get; set; } = "active";
}

