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

static class DwgDraftApi
{
    public static IServiceCollection AddDwgDraft(this IServiceCollection services)
    {
        services.AddSingleton<DwgDraftService>();
        return services;
    }

    public static WebApplication MapDwgDraftApi(this WebApplication app)
    {
        var group = app.MapGroup("/api/dwg-draft");

        group.MapGet("/installation", (DwgDraftService dwg) => Results.Ok(dwg.DiscoverInstallation()));
        group.MapGet("/bridge", (DwgDraftService dwg) => Results.Ok(dwg.ReadBridgeStatus()));

        group.MapPost("/extract", (DwgDraftExtractRequest request, DwgDraftService dwg) =>
        {
            try
            {
                return Results.Ok(dwg.Extract(request));
            }
            catch (Exception ex)
            {
                return Results.Problem($"AutoCAD DWG 提取失败：{ex.Message}", statusCode: 500);
            }
        });

        group.MapPost("/open", (DwgDraftExtractRequest request, DwgDraftService dwg) =>
        {
            try
            {
                return Results.Ok(dwg.Extract(request));
            }
            catch (Exception ex)
            {
                return Results.Problem($"AutoCAD DWG 打开/提取失败：{ex.Message}", statusCode: 500);
            }
        });

        group.MapGet("/latest-extraction", (DwgDraftService dwg) =>
        {
            try
            {
                return Results.Ok(dwg.ReadLatestExtraction());
            }
            catch (Exception ex)
            {
                return Results.Problem($"读取 DWG 提取结果失败：{ex.Message}", statusCode: 500);
            }
        });

        group.MapGet("/latest-extraction/category/{category}", (string category, DwgDraftService dwg) =>
        {
            try
            {
                var result = dwg.ReadCategoryExtraction(category);
                return result.Found ? Results.Ok(result) : Results.NotFound(result);
            }
            catch (Exception ex)
            {
                return Results.Problem($"读取 DWG 分类提取结果失败：{ex.Message}", statusCode: 500);
            }
        });

        return app;
    }
}

sealed class DwgDraftService
{
    const string DefaultProgId = "AutoCAD.Application.23.1";
    static readonly JsonSerializerOptions JsonOptions = new()
    {
        WriteIndented = true,
        PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
        DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower
    };

    static readonly string[] ProgIdCandidates =
    [
        "AutoCAD.Application.23.1",
        "AutoCAD.Application.23",
        "AutoCAD.Application"
    ];

    readonly AppPaths _paths;
    readonly object _lock = new();

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

    public DwgDraftInstallation DiscoverInstallation()
    {
        var progIds = Registry.ClassesRoot.GetSubKeyNames()
            .Where(name => name.StartsWith("AutoCAD.Application", StringComparison.OrdinalIgnoreCase))
            .OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
            .Select(ReadProgIdInfo)
            .ToList();

        var configuredExe = Environment.GetEnvironmentVariable("AUTOCAD_EXE");
        var exeCandidates = string.IsNullOrWhiteSpace(configuredExe)
            ? Array.Empty<string>()
            : new[] { configuredExe };

        return new DwgDraftInstallation
        {
            Found = progIds.Count > 0 || exeCandidates.Any(File.Exists),
            DefaultProgId = DefaultProgId,
            ProgIds = progIds,
            AcadExeCandidates = exeCandidates.Where(File.Exists).ToList(),
            Notes =
            [
                "DWG 通过 AutoCAD COM 读取，后续二维图查错以 AutoCAD 路径为主。",
                "AutoCAD COM 调用可能在软件忙碌时返回 RPC_E_CALL_REJECTED，后端已对属性读取和集合遍历做重试。"
            ]
        };
    }

    public DwgDraftBridgeStatus ReadBridgeStatus()
    {
        var bridgeDir = ResolveBridgeDir();
        var heartbeatPath = Path.Combine(bridgeDir, "heartbeat.json");
        var alive = IsBridgeAlive(heartbeatPath);
        JsonElement? heartbeat = null;
        if (File.Exists(heartbeatPath))
        {
            using var doc = JsonDocument.Parse(File.ReadAllText(heartbeatPath));
            heartbeat = doc.RootElement.Clone();
        }

        return new DwgDraftBridgeStatus
        {
            Ok = true,
            Alive = alive,
            BridgeDir = bridgeDir,
            HeartbeatPath = heartbeatPath,
            LastWriteTimeUtc = File.Exists(heartbeatPath) ? File.GetLastWriteTimeUtc(heartbeatPath) : null,
            Heartbeat = heartbeat,
            Message = alive ? "AutoCAD DWG bridge is online." : "AutoCAD DWG bridge is not online."
        };
    }

    public DwgDraftExtractionResult Extract(DwgDraftExtractRequest request)
    {
        lock (_lock)
        {
            return RunExternalExtractor(request);
        }
    }

    DwgDraftExtractionResult RunExternalExtractor(DwgDraftExtractRequest request)
    {
        TraceExternalExtractor("start");
        ApplyDrawingPathBase64(request);
        if (string.IsNullOrWhiteSpace(request.DrawingPath))
            throw new ArgumentException("drawing_path 不能为空。", nameof(request));

        var drawingPath = Path.GetFullPath(request.DrawingPath.Trim().Trim('"'));
        TraceExternalExtractor($"drawing_path={drawingPath}");
        if (!File.Exists(drawingPath))
            throw new FileNotFoundException($"DWG 文件不存在：{drawingPath}");

        var progId = string.IsNullOrWhiteSpace(request.ProgId) ? ResolveProgId() : request.ProgId.Trim();
        TraceExternalExtractor($"prog_id={progId}");
        var outputDir = ResolveOutputDir(request.OutputDir);
        Directory.CreateDirectory(outputDir);
        TraceExternalExtractor($"output_dir={outputDir}");

        var extractorDll = Path.Combine(
            _paths.Agent4Root,
            "tools",
            "drawing-diagnostics",
            "AutoCadDwgExtractor",
            "bin",
            "Debug",
            "net10.0",
            "AutoCadDwgExtractor.dll");
        TraceExternalExtractor($"extractor_dll={extractorDll}; exists={File.Exists(extractorDll)}");
        if (!File.Exists(extractorDll))
            throw new FileNotFoundException($"AutoCAD DWG 提取器尚未编译：{extractorDll}");

        if (request.UseBridge &&
            TryRunBridgeExtractor(request, drawingPath, progId, outputDir, out var bridgeResult))
        {
            return bridgeResult;
        }

        var startInfo = new ProcessStartInfo
        {
            FileName = "dotnet",
            WorkingDirectory = _paths.Agent4Root,
            UseShellExecute = false,
            RedirectStandardOutput = true,
            RedirectStandardError = true
        };
        startInfo.ArgumentList.Add(extractorDll);
        startInfo.ArgumentList.Add("--drawing-path");
        startInfo.ArgumentList.Add(drawingPath);
        startInfo.ArgumentList.Add("--output-dir");
        startInfo.ArgumentList.Add(outputDir);
        startInfo.ArgumentList.Add("--prog-id");
        startInfo.ArgumentList.Add(progId);
        startInfo.ArgumentList.Add("--visible");
        startInfo.ArgumentList.Add(request.Visible ? "true" : "false");
        startInfo.ArgumentList.Add("--close-after-extract");
        startInfo.ArgumentList.Add(request.CloseAfterExtract ? "true" : "false");
        startInfo.ArgumentList.Add("--max-entities");
        startInfo.ArgumentList.Add((request.MaxEntities <= 0 ? 100_000 : request.MaxEntities).ToString(CultureInfo.InvariantCulture));
        if (!string.IsNullOrWhiteSpace(request.MainViewSeedMode))
        {
            startInfo.ArgumentList.Add("--main-view-seed-mode");
            startInfo.ArgumentList.Add(request.MainViewSeedMode.Trim());
        }
        if (!string.IsNullOrWhiteSpace(request.ReuseExtractionDir))
        {
            startInfo.ArgumentList.Add("--reuse-extraction-dir");
            startInfo.ArgumentList.Add(Path.GetFullPath(request.ReuseExtractionDir.Trim().Trim('"')));
        }

        TraceExternalExtractor("process_start");
        using var process = Process.Start(startInfo) ?? throw new InvalidOperationException("Failed to start AutoCadDwgExtractor.");
        TraceExternalExtractor($"process_started pid={process.Id}");
        var stdout = process.StandardOutput.ReadToEnd();
        var stderr = process.StandardError.ReadToEnd();
        process.WaitForExit();
        TraceExternalExtractor($"process_exit exit_code={process.ExitCode}; stdout_len={stdout.Length}; stderr_len={stderr.Length}");

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

        var manifestPath = Path.Combine(outputDir, "manifest.json");
        TraceExternalExtractor($"manifest={manifestPath}; exists={File.Exists(manifestPath)}");
        if (!File.Exists(manifestPath))
            throw new FileNotFoundException($"AutoCadDwgExtractor did not write manifest: {manifestPath}\n{stdout}\n{stderr}");

        using var doc = JsonDocument.Parse(File.ReadAllText(manifestPath));
        var root = doc.RootElement;
        var entityCount = root.TryGetProperty("entity_count", out var countElement) && countElement.ValueKind == JsonValueKind.Number
            ? countElement.GetInt32()
            : 0;
        var typeCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
        if (root.TryGetProperty("type_counts", out var typeCountsElement) && typeCountsElement.ValueKind == JsonValueKind.Object)
        {
            foreach (var item in typeCountsElement.EnumerateObject())
            {
                if (item.Value.ValueKind == JsonValueKind.Number)
                    typeCounts[item.Name] = item.Value.GetInt32();
            }
        }

        return new DwgDraftExtractionResult
        {
            Ok = true,
            Source = "AutoCAD COM external extractor",
            ProgId = progId,
            Started = root.TryGetProperty("started_auto_cad", out var startedElement) && startedElement.ValueKind is JsonValueKind.True,
            DrawingPath = drawingPath,
            ExportDir = outputDir,
            ManifestPath = manifestPath,
            EntityCount = entityCount,
            TypeCounts = typeCounts,
            CategoryFiles = ReadCategoryFiles(root, outputDir),
            Message = $"已通过 AutoCAD COM 外部提取器提取 DWG：{Path.GetFileName(drawingPath)}"
        };
    }

    bool TryRunBridgeExtractor(
        DwgDraftExtractRequest request,
        string drawingPath,
        string progId,
        string outputDir,
        out DwgDraftExtractionResult result)
    {
        result = new DwgDraftExtractionResult();
        var bridgeDir = ResolveBridgeDir();
        var heartbeatPath = Path.Combine(bridgeDir, "heartbeat.json");
        if (!IsBridgeAlive(heartbeatPath))
        {
            TraceExternalExtractor($"bridge_not_available heartbeat={heartbeatPath}");
            return false;
        }

        var jobsDir = Path.Combine(bridgeDir, "jobs");
        var resultsDir = Path.Combine(bridgeDir, "results");
        Directory.CreateDirectory(jobsDir);
        Directory.CreateDirectory(resultsDir);

        var jobId = $"{DateTime.UtcNow:yyyyMMddHHmmssfff}-{Guid.NewGuid():N}";
        var jobPath = Path.Combine(jobsDir, $"{jobId}.request.json");
        var tempJobPath = jobPath + ".tmp";
        var resultPath = Path.Combine(resultsDir, $"{jobId}.result.json");
        var job = new DwgBridgeJob
        {
            JobId = jobId,
            DrawingPath = drawingPath,
            OutputDir = outputDir,
            ProgId = progId,
            Visible = request.Visible,
            CloseAfterExtract = request.CloseAfterExtract,
            MaxEntities = request.MaxEntities <= 0 ? 100_000 : request.MaxEntities,
            MainViewSeedMode = request.MainViewSeedMode,
            ReuseExtractionDir = string.IsNullOrWhiteSpace(request.ReuseExtractionDir) ? "" : Path.GetFullPath(request.ReuseExtractionDir.Trim().Trim('"'))
        };

        File.WriteAllText(tempJobPath, JsonSerializer.Serialize(job, JsonOptions));
        File.Move(tempJobPath, jobPath, overwrite: true);
        TraceExternalExtractor($"bridge_job_submitted job_id={jobId}; bridge_dir={bridgeDir}");

        var timeout = TimeSpan.FromSeconds(request.BridgeTimeoutSeconds <= 0 ? 600 : request.BridgeTimeoutSeconds);
        var deadline = DateTime.UtcNow.Add(timeout);
        while (DateTime.UtcNow < deadline)
        {
            if (File.Exists(resultPath))
            {
                var bridgeResult = JsonSerializer.Deserialize<DwgBridgeJobResult>(File.ReadAllText(resultPath), JsonOptions)
                    ?? throw new InvalidOperationException($"Invalid bridge result: {resultPath}");
                TraceExternalExtractor($"bridge_job_result job_id={jobId}; ok={bridgeResult.Ok}; exit={bridgeResult.ExitCode}");
                if (!bridgeResult.Ok)
                {
                    throw new InvalidOperationException(
                        $"AutoCAD bridge extraction failed. job={jobId}; exit={bridgeResult.ExitCode}\n{bridgeResult.Error}\n{bridgeResult.Stdout}\n{bridgeResult.Stderr}");
                }

                var manifestPath = string.IsNullOrWhiteSpace(bridgeResult.ManifestPath)
                    ? Path.Combine(outputDir, "manifest.json")
                    : bridgeResult.ManifestPath;
                if (!File.Exists(manifestPath))
                    throw new FileNotFoundException($"AutoCAD bridge did not write manifest: {manifestPath}");

                result = ReadExternalManifest(
                    manifestPath,
                    outputDir,
                    drawingPath,
                    progId,
                    "AutoCAD COM bridge extractor",
                    $"Read DWG through AutoCAD bridge: {Path.GetFileName(drawingPath)}");
                return true;
            }

            Thread.Sleep(500);
        }

        throw new TimeoutException($"AutoCAD bridge extraction timed out. job={jobId}; timeout={timeout.TotalSeconds:0}s");
    }

    bool IsBridgeAlive(string heartbeatPath)
    {
        if (!File.Exists(heartbeatPath))
            return false;
        return DateTime.UtcNow - File.GetLastWriteTimeUtc(heartbeatPath) <= TimeSpan.FromSeconds(30);
    }

    string ResolveBridgeDir()
    {
        var configured = Environment.GetEnvironmentVariable("AGENT4_DWG_BRIDGE_DIR");
        if (!string.IsNullOrWhiteSpace(configured))
            return Path.GetFullPath(configured.Trim().Trim('"'));
        return Path.Combine(_paths.RuntimeDir, "dwg-draft", "bridge");
    }

    DwgDraftExtractionResult ReadExternalManifest(
        string manifestPath,
        string outputDir,
        string drawingPath,
        string progId,
        string source,
        string message)
    {
        using var doc = JsonDocument.Parse(File.ReadAllText(manifestPath));
        var root = doc.RootElement;
        var entityCount = root.TryGetProperty("entity_count", out var countElement) && countElement.ValueKind == JsonValueKind.Number
            ? countElement.GetInt32()
            : 0;
        var typeCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
        if (root.TryGetProperty("type_counts", out var typeCountsElement) && typeCountsElement.ValueKind == JsonValueKind.Object)
        {
            foreach (var item in typeCountsElement.EnumerateObject())
            {
                if (item.Value.ValueKind == JsonValueKind.Number)
                    typeCounts[item.Name] = item.Value.GetInt32();
            }
        }

        return new DwgDraftExtractionResult
        {
            Ok = true,
            Source = source,
            ProgId = progId,
            Started = root.TryGetProperty("started_auto_cad", out var startedElement) && startedElement.ValueKind is JsonValueKind.True,
            DrawingPath = drawingPath,
            ExportDir = outputDir,
            ManifestPath = manifestPath,
            EntityCount = entityCount,
            TypeCounts = typeCounts,
            CategoryFiles = ReadCategoryFiles(root, outputDir),
            Message = message
        };
    }

    void TraceExternalExtractor(string message)
    {
        try
        {
            var dir = Path.Combine(_paths.RuntimeDir, "dwg-draft");
            Directory.CreateDirectory(dir);
            File.AppendAllText(
                Path.Combine(dir, "extractor-dispatch.log"),
                $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {message}{Environment.NewLine}");
        }
        catch
        {
            // Diagnostics must not affect extraction.
        }
    }

    DwgDraftExtractionResult ExtractOnSta(DwgDraftExtractRequest request)
    {
        ApplyDrawingPathBase64(request);
        if (string.IsNullOrWhiteSpace(request.DrawingPath))
            throw new ArgumentException("drawing_path 不能为空。", nameof(request));

        var drawingPath = Path.GetFullPath(request.DrawingPath.Trim().Trim('"'));
        if (!File.Exists(drawingPath))
            throw new FileNotFoundException($"DWG 文件不存在：{drawingPath}");

        var progId = string.IsNullOrWhiteSpace(request.ProgId) ? ResolveProgId() : request.ProgId.Trim();
        var outputDir = ResolveOutputDir(request.OutputDir);
        Directory.CreateDirectory(outputDir);

        var app = ConnectOrCreate(progId, request.Visible, out var started);
        object? doc = null;
        try
        {
            WaitForQuiescent(app, TimeSpan.FromSeconds(20));
            doc = OpenDocument(app, drawingPath);
            WaitForQuiescent(app, TimeSpan.FromSeconds(20));

            var extraction = ExtractDocument(doc, request.MaxEntities <= 0 ? 100_000 : request.MaxEntities);
            var manifest = WriteExtractionFiles(outputDir, drawingPath, progId, started, extraction);

            if (request.CloseAfterExtract)
                TryInvoke(doc, "Close", [false]);

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

    DwgDraftDocumentExtraction ExtractDocument(object doc, int maxEntities)
    {
        var extraction = new DwgDraftDocumentExtraction();
        ExtractSpace(doc, "ModelSpace", extraction, maxEntities);
        ExtractSpace(doc, "PaperSpace", extraction, maxEntities);
        return extraction;
    }

    void ExtractSpace(object doc, string spaceName, DwgDraftDocumentExtraction extraction, int maxEntities)
    {
        var space = GetProperty(doc, spaceName);
        if (space == null)
            return;

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

            for (var i = 0; i < count && extraction.EntitiesCommon.Count < maxEntities; i++)
            {
                var entity = GetCollectionItem(space, i);
                if (entity == null)
                    continue;

                try
                {
                    ExtractEntity(entity, spaceName, i, extraction);
                }
                finally
                {
                    ReleaseCom(entity);
                }
            }
        }
        finally
        {
            ReleaseCom(space);
        }
    }

    void ExtractEntity(object entity, string spaceName, int index, DwgDraftDocumentExtraction extraction)
    {
        var objectName = ReadString(entity, "ObjectName");
        if (string.IsNullOrWhiteSpace(objectName))
            objectName = ReadString(entity, "EntityName");
        if (string.IsNullOrWhiteSpace(objectName))
            objectName = entity.GetType().Name;

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

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

        var details = ExtractDetails(entity, objectName);
        var item = Merge(common, details);
        if (IsDimension(objectName))
        {
            extraction.Dimensions.Add(item);
            var tolerance = ExtractDimensionalTolerance(common, details);
            if (tolerance != null)
                extraction.DimensionalTolerances.Add(tolerance);
        }
        else if (IsGeometry(objectName))
        {
            extraction.Geometry.Add(item);
        }
        else if (IsAnnotation(objectName))
        {
            extraction.Annotations.Add(item);
        }
        else
        {
            extraction.UnknownEntities.Add(item);
        }

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

    static Dictionary<string, object?> ExtractDetails(object entity, string objectName)
    {
        var details = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
        if (objectName.Contains("Line", StringComparison.OrdinalIgnoreCase))
        {
            AddPoint(details, "start", GetProperty(entity, "StartPoint"));
            AddPoint(details, "end", GetProperty(entity, "EndPoint"));
        }
        else if (objectName.Contains("Circle", StringComparison.OrdinalIgnoreCase))
        {
            AddPoint(details, "center", GetProperty(entity, "Center"));
            var radius = ToDouble(GetProperty(entity, "Radius"));
            details["radius"] = radius;
            details["diameter"] = radius == null ? null : radius * 2;
        }
        else if (objectName.Contains("Arc", StringComparison.OrdinalIgnoreCase))
        {
            AddPoint(details, "center", GetProperty(entity, "Center"));
            details["radius"] = ToDouble(GetProperty(entity, "Radius"));
            details["start_angle"] = ToDouble(GetProperty(entity, "StartAngle"));
            details["end_angle"] = ToDouble(GetProperty(entity, "EndAngle"));
        }
        else if (objectName.Contains("Spline", StringComparison.OrdinalIgnoreCase))
        {
            AddPoint(details, "start", GetProperty(entity, "StartPoint"));
            AddPoint(details, "end", GetProperty(entity, "EndPoint"));
            details["degree"] = ToInt(GetProperty(entity, "Degree"));
            details["closed"] = ToBool(GetProperty(entity, "Closed"));
        }
        else if (objectName.Contains("Text", StringComparison.OrdinalIgnoreCase))
        {
            var raw = ReadString(entity, "TextString");
            details["text"] = CleanAcadText(raw);
            details["raw_text"] = raw;
            details["height"] = ToDouble(GetProperty(entity, "Height"));
            details["rotation"] = ToDouble(GetProperty(entity, "Rotation"));
            AddPoint(details, "insertion_point", GetProperty(entity, "InsertionPoint"));
        }
        else if (objectName.Contains("Dimension", StringComparison.OrdinalIgnoreCase))
        {
            var textOverride = ReadString(entity, "TextOverride");
            details["measurement"] = ToDouble(GetProperty(entity, "Measurement"));
            details["text_override"] = textOverride;
            details["text"] = CleanAcadText(textOverride);
            var missingPointFields = new List<string>();
            AddPointFromProperties(details, "text_position", entity, missingPointFields, "TextPosition");
            AddPointFromProperties(details, "xline1_point", entity, missingPointFields, "XLine1Point", "ExtLine1Point", "ExtensionLine1Point");
            AddPointFromProperties(details, "xline2_point", entity, missingPointFields, "XLine2Point", "ExtLine2Point", "ExtensionLine2Point");
            AddPointFromProperties(details, "dim_line_point", entity, missingPointFields, "DimLinePoint", "DimLineLocation");
            AddPointFromProperties(details, "center", entity, missingPointFields, "Center");
            AddPointFromProperties(details, "chord_point", entity, missingPointFields, "ChordPoint");
            AddPointFromProperties(details, "far_chord_point", entity, missingPointFields, "FarChordPoint");
            if (missingPointFields.Count > 0)
                details["dimension_point_missing_fields"] = missingPointFields;
        }
        else if (objectName.Contains("BlockReference", StringComparison.OrdinalIgnoreCase))
        {
            details["name"] = ReadString(entity, "Name");
            details["effective_name"] = ReadString(entity, "EffectiveName");
            details["rotation"] = ToDouble(GetProperty(entity, "Rotation"));
            details["x_scale_factor"] = ToDouble(GetProperty(entity, "XScaleFactor"));
            details["y_scale_factor"] = ToDouble(GetProperty(entity, "YScaleFactor"));
            AddPoint(details, "insertion_point", GetProperty(entity, "InsertionPoint"));
            var attributes = ReadBlockAttributes(entity);
            if (attributes.Count > 0)
            {
                details["attributes"] = attributes;
                details["text"] = string.Join(" ", attributes.Select(a => a.TryGetValue("value", out var value) ? value : null));
            }
        }
        else if (objectName.Contains("Hatch", StringComparison.OrdinalIgnoreCase))
        {
            details["pattern_name"] = ReadString(entity, "PatternName");
            details["pattern_scale"] = ToDouble(GetProperty(entity, "PatternScale"));
        }
        else if (objectName.Contains("RasterImage", StringComparison.OrdinalIgnoreCase))
        {
            details["image_file"] = ReadString(entity, "ImageFile");
        }

        return details;
    }

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

        var raw = TryInvoke(entity, "GetAttributes", []);
        if (raw == null)
            return result;

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

        return result;
    }

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

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

    DwgDraftManifest WriteExtractionFiles(
        string outputDir,
        string drawingPath,
        string progId,
        bool started,
        DwgDraftDocumentExtraction extraction)
    {
        var files = new List<DwgDraftCategoryFile>
        {
            WriteCategory(outputDir, "entities-common", "entities-common.json", extraction.EntitiesCommon),
            WriteCategory(outputDir, "geometry", "geometry.json", extraction.Geometry),
            WriteCategory(outputDir, "annotations", "annotations.json", extraction.Annotations),
            WriteCategory(outputDir, "dimensions", "dimensions.json", extraction.Dimensions),
            WriteCategory(outputDir, "dimensional-tolerances", "dimensional-tolerances.json", extraction.DimensionalTolerances),
            WriteCategory(outputDir, "roughness", "roughness.json", extraction.Roughness),
            WriteCategory(outputDir, "geometric-tolerances", "geometric-tolerances.json", extraction.GeometricTolerances),
            WriteCategory(outputDir, "datums", "datums.json", extraction.Datums),
            WriteCategory(outputDir, "unknown-entities", "unknown-entities.json", extraction.UnknownEntities)
        };

        var manifest = new DwgDraftManifest
        {
            Ok = true,
            Schema = "agent4.dwg-draft.extraction.v1",
            Source = "AutoCAD COM",
            ProgId = progId,
            StartedAutoCad = started,
            DrawingPath = drawingPath,
            ExportedAtUtc = DateTime.UtcNow,
            ModelSpaceCount = extraction.ModelSpaceCount,
            PaperSpaceCount = extraction.PaperSpaceCount,
            EntityCount = extraction.EntitiesCommon.Count,
            TypeCounts = extraction.TypeCounts,
            CategoryFiles = files
        };

        WriteJson(Path.Combine(outputDir, "manifest.json"), manifest);
        return manifest;
    }

    public DwgDraftLatestExtractionResult ReadLatestExtraction()
    {
        var path = ResolveManifestPath("");
        if (!File.Exists(path))
        {
            return new DwgDraftLatestExtractionResult
            {
                Ok = false,
                Found = false,
                ExportDir = Path.GetDirectoryName(path) ?? "",
                ManifestPath = path,
                Message = "DWG extraction manifest was not found. Call POST /api/dwg-draft/extract first."
            };
        }

        using var doc = JsonDocument.Parse(File.ReadAllText(path));
        var root = doc.RootElement.Clone();
        var files = ReadCategoryFiles(root, Path.GetDirectoryName(path) ?? "");
        return new DwgDraftLatestExtractionResult
        {
            Ok = true,
            Found = true,
            ExportDir = Path.GetDirectoryName(path) ?? "",
            ManifestPath = path,
            LastWriteTimeUtc = File.GetLastWriteTimeUtc(path),
            Manifest = root,
            CategoryFiles = files,
            Message = "Read the latest AutoCAD DWG extraction manifest."
        };
    }

    public DwgDraftCategoryExtractionResult ReadCategoryExtraction(string category)
    {
        if (string.IsNullOrWhiteSpace(category))
            throw new ArgumentException("category is required.", nameof(category));

        var manifestPath = ResolveManifestPath("");
        if (!File.Exists(manifestPath))
        {
            return new DwgDraftCategoryExtractionResult
            {
                Ok = false,
                Found = false,
                Category = category,
                ManifestPath = manifestPath,
                Message = "DWG extraction manifest was not found."
            };
        }

        using var manifestDoc = JsonDocument.Parse(File.ReadAllText(manifestPath));
        var exportDir = Path.GetDirectoryName(manifestPath) ?? "";
        var files = ReadCategoryFiles(manifestDoc.RootElement, exportDir);
        var selected = files.FirstOrDefault(file =>
            string.Equals(file.Category, category, StringComparison.OrdinalIgnoreCase) ||
            string.Equals(Path.GetFileNameWithoutExtension(file.File), category, StringComparison.OrdinalIgnoreCase));

        if (selected == null || !File.Exists(selected.FullPath))
        {
            return new DwgDraftCategoryExtractionResult
            {
                Ok = false,
                Found = false,
                Category = category,
                ManifestPath = manifestPath,
                ExportDir = exportDir,
                AvailableCategories = files.Select(file => file.Category).ToList(),
                Message = $"DWG extraction category was not found: {category}"
            };
        }

        using var categoryDoc = JsonDocument.Parse(File.ReadAllText(selected.FullPath));
        return new DwgDraftCategoryExtractionResult
        {
            Ok = true,
            Found = true,
            Category = selected.Category,
            ManifestPath = manifestPath,
            ExportDir = exportDir,
            CategoryPath = selected.FullPath,
            LastWriteTimeUtc = File.GetLastWriteTimeUtc(selected.FullPath),
            ItemCount = selected.ItemCount,
            Data = categoryDoc.RootElement.Clone(),
            AvailableCategories = files.Select(file => file.Category).ToList(),
            Message = $"Read DWG extraction category: {selected.Category}"
        };
    }

    string ResolveOutputDir(string requested)
    {
        if (!string.IsNullOrWhiteSpace(requested))
            return Path.GetFullPath(requested.Trim().Trim('"'));

        var configured = Environment.GetEnvironmentVariable("AGENT4_DWG_EXPORT_DIR");
        if (!string.IsNullOrWhiteSpace(configured))
            return Path.GetFullPath(configured.Trim().Trim('"'));

        return Path.Combine(_paths.RuntimeDir, "dwg-draft", "latest-extraction");
    }

    static void ApplyDrawingPathBase64(DwgDraftExtractRequest request)
    {
        if (string.IsNullOrWhiteSpace(request.DrawingPathBase64))
            return;

        try
        {
            request.DrawingPath = Encoding.UTF8.GetString(Convert.FromBase64String(request.DrawingPathBase64.Trim()));
        }
        catch (Exception ex)
        {
            throw new ArgumentException($"drawing_path_base64 不是有效的 UTF-8 Base64 路径：{ex.Message}", nameof(request));
        }
    }

    string ResolveManifestPath(string requestedDir)
    {
        var dir = ResolveOutputDir(requestedDir);
        return Path.Combine(dir, "manifest.json");
    }

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

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

    static List<DwgDraftCategoryFile> ReadCategoryFiles(JsonElement manifest, string exportDir)
    {
        if (!manifest.TryGetProperty("category_files", out var filesElement) || filesElement.ValueKind != JsonValueKind.Array)
            return [];

        var result = new List<DwgDraftCategoryFile>();
        foreach (var item in filesElement.EnumerateArray())
        {
            var category = item.TryGetProperty("category", out var categoryElement) ? categoryElement.GetString() ?? "" : "";
            var file = item.TryGetProperty("file", out var fileElement) ? fileElement.GetString() ?? "" : "";
            var itemCount = item.TryGetProperty("item_count", out var countElement) && countElement.ValueKind == JsonValueKind.Number
                ? countElement.GetInt32()
                : 0;
            if (string.IsNullOrWhiteSpace(category) || string.IsNullOrWhiteSpace(file))
                continue;

            var fullPath = Path.GetFullPath(Path.Combine(exportDir, file));
            var root = Path.GetFullPath(exportDir);
            if (!fullPath.StartsWith(root, StringComparison.OrdinalIgnoreCase))
                continue;

            result.Add(new DwgDraftCategoryFile
            {
                Category = category,
                File = file,
                FullPath = fullPath,
                ItemCount = itemCount,
                Exists = File.Exists(fullPath)
            });
        }

        return result;
    }

    static string ResolveProgId()
    {
        foreach (var progId in ProgIdCandidates)
        {
            if (Type.GetTypeFromProgID(progId, throwOnError: false) != null)
                return progId;
        }

        return DefaultProgId;
    }

    static object ConnectOrCreate(string progId, bool visible, out bool started)
    {
        var app = TryGetActiveComObject(progId, out _);
        started = false;
        if (app == null)
        {
            var type = Type.GetTypeFromProgID(progId, throwOnError: true)
                ?? throw new InvalidOperationException($"找不到 AutoCAD COM ProgID：{progId}");
            app = Activator.CreateInstance(type)
                ?? throw new InvalidOperationException($"创建 AutoCAD COM 对象失败：{progId}");
            started = true;
        }

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

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

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

            Thread.Sleep(250);
        }
    }

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

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

        return null;
    });

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

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

            return (object?)null;
        });
    }

    static T RunSta<T>(Func<T> action)
    {
        T? result = default;
        Exception? error = null;
        var thread = new Thread(() =>
        {
            try
            {
                result = action();
            }
            catch (Exception ex)
            {
                error = ex;
            }
        });
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
        thread.Join();

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

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

        if (last != null)
            throw last;
        return action();
    }

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

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

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

    static bool IsDimension(string objectName) => objectName.Contains("Dimension", StringComparison.OrdinalIgnoreCase);

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

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

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

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

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

    static string CleanAcadText(string text)
    {
        if (string.IsNullOrWhiteSpace(text))
            return "";

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

    static void AddPoint(Dictionary<string, object?> details, string name, object? value)
    {
        var point = ToPoint(value);
        if (point != null)
            details[name] = point;
    }

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

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

        missingFields.Add(name);
    }

    static List<double>? ToPoint(object? value)
    {
        if (value is Array array)
        {
            var result = new List<double>();
            foreach (var item in array)
            {
                if (ToDouble(item) is { } number)
                    result.Add(number);
            }
            return result.Count > 0 ? result : null;
        }

        return null;
    }

    static string ReadString(object obj, string name)
    {
        return Convert.ToString(GetProperty(obj, name), CultureInfo.InvariantCulture) ?? "";
    }

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

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

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

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

    static DwgDraftProgIdInfo ReadProgIdInfo(string progId)
    {
        var clsid = ReadDefaultValue($@"{progId}\CLSID");
        return new DwgDraftProgIdInfo
        {
            ProgId = progId,
            Description = ReadDefaultValue(progId),
            Clsid = clsid,
            LocalServer32 = string.IsNullOrWhiteSpace(clsid) ? "" : ReadDefaultValue($@"CLSID\{clsid}\LocalServer32")
        };
    }

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

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

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

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

sealed class DwgDraftExtractRequest
{
    public string DrawingPath { get; set; } = "";
    public string DrawingPathBase64 { get; set; } = "";
    public string ProgId { get; set; } = "";
    public bool Visible { get; set; } = true;
    public bool CloseAfterExtract { get; set; }
    public string OutputDir { get; set; } = "";
    public int MaxEntities { get; set; } = 100_000;
    public string MainViewSeedMode { get; set; } = "";
    public string ReuseExtractionDir { get; set; } = "";
    public bool UseBridge { get; set; } = true;
    public int BridgeTimeoutSeconds { get; set; } = 600;
}

sealed class DwgBridgeJob
{
    public string JobId { get; set; } = "";
    public string DrawingPath { get; set; } = "";
    public string OutputDir { get; set; } = "";
    public string ProgId { get; set; } = "";
    public bool Visible { get; set; } = true;
    public bool CloseAfterExtract { get; set; }
    public int MaxEntities { get; set; } = 100_000;
    public string MainViewSeedMode { get; set; } = "";
    public string ReuseExtractionDir { get; set; } = "";
}

sealed class DwgBridgeJobResult
{
    public bool Ok { get; set; }
    public string JobId { get; set; } = "";
    public int ExitCode { get; set; }
    public string Stdout { get; set; } = "";
    public string Stderr { get; set; } = "";
    public string OutputDir { get; set; } = "";
    public string ManifestPath { get; set; } = "";
    public string Error { get; set; } = "";
}

sealed class DwgDraftBridgeStatus
{
    public bool Ok { get; set; }
    public bool Alive { get; set; }
    public string BridgeDir { get; set; } = "";
    public string HeartbeatPath { get; set; } = "";
    public DateTime? LastWriteTimeUtc { get; set; }
    public JsonElement? Heartbeat { get; set; }
    public string Message { get; set; } = "";
}

sealed class DwgDraftDocumentExtraction
{
    public int ModelSpaceCount { get; set; }
    public int PaperSpaceCount { get; set; }
    public Dictionary<string, int> TypeCounts { get; set; } = new(StringComparer.OrdinalIgnoreCase);
    public List<Dictionary<string, object?>> EntitiesCommon { get; set; } = new();
    public List<Dictionary<string, object?>> Geometry { get; set; } = new();
    public List<Dictionary<string, object?>> Annotations { get; set; } = new();
    public List<Dictionary<string, object?>> Dimensions { get; set; } = new();
    public List<Dictionary<string, object?>> DimensionalTolerances { get; set; } = new();
    public List<Dictionary<string, object?>> Roughness { get; set; } = new();
    public List<Dictionary<string, object?>> GeometricTolerances { get; set; } = new();
    public List<Dictionary<string, object?>> Datums { get; set; } = new();
    public List<Dictionary<string, object?>> UnknownEntities { get; set; } = new();
}

sealed class DwgDraftInstallation
{
    public bool Found { get; set; }
    public string DefaultProgId { get; set; } = "";
    public List<DwgDraftProgIdInfo> ProgIds { get; set; } = new();
    public List<string> AcadExeCandidates { get; set; } = new();
    public List<string> Notes { get; set; } = new();
}

sealed class DwgDraftProgIdInfo
{
    public string ProgId { get; set; } = "";
    public string Description { get; set; } = "";
    public string Clsid { get; set; } = "";
    public string LocalServer32 { get; set; } = "";
}

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

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

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

sealed class DwgDraftLatestExtractionResult
{
    public bool Ok { get; set; }
    public bool Found { get; set; }
    public string ExportDir { get; set; } = "";
    public string ManifestPath { get; set; } = "";
    public DateTime? LastWriteTimeUtc { get; set; }
    public JsonElement? Manifest { get; set; }
    public List<DwgDraftCategoryFile> CategoryFiles { get; set; } = new();
    public string Message { get; set; } = "";
}

sealed class DwgDraftCategoryExtractionResult
{
    public bool Ok { get; set; }
    public bool Found { get; set; }
    public string Category { get; set; } = "";
    public string ExportDir { get; set; } = "";
    public string ManifestPath { get; set; } = "";
    public string CategoryPath { get; set; } = "";
    public DateTime? LastWriteTimeUtc { get; set; }
    public int ItemCount { get; set; }
    public JsonElement? Data { get; set; }
    public List<string> AvailableCategories { get; set; } = new();
    public string Message { get; set; } = "";
}
