using System.Globalization;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;

Console.OutputEncoding = Encoding.UTF8;

try
{
    var options = CliOptions.Parse(args);
    if (string.IsNullOrWhiteSpace(options.BrepReportPath) || !File.Exists(options.BrepReportPath))
        throw new FileNotFoundException("Missing B-rep report. Pass --brep-report <section_brep_report.json>.");
    if (string.IsNullOrWhiteSpace(options.DimensionsPath) || !File.Exists(options.DimensionsPath))
        throw new FileNotFoundException("Missing dimensions file. Pass --dimensions <dimension anchors json>.");

    Directory.CreateDirectory(options.OutputDir);
    var report = DimensionFaceMatcher.Run(options);
    var outputPath = options.OutputPath ?? Path.Combine(options.OutputDir, "dimension_face_matches.json");
    File.WriteAllText(outputPath, JsonSerializer.Serialize(report, JsonDefaults.Options), new UTF8Encoding(false));

    Console.WriteLine($"Dimension-face matches written: {outputPath}");
    Console.WriteLine($"Dimensions: {report.Dimensions.Count}; unique: {report.Dimensions.Count(d => d.IsUnique)}");
}
catch (Exception ex)
{
    Console.Error.WriteLine(ex.ToString());
    Environment.ExitCode = 1;
}

static class DimensionFaceMatcher
{
    public static MatchReport Run(CliOptions options)
    {
        using var brepDoc = JsonDocument.Parse(File.ReadAllText(options.BrepReportPath));
        using var dimensionDoc = JsonDocument.Parse(File.ReadAllText(options.DimensionsPath));

        var faces = LoadFaces(brepDoc.RootElement);
        if (faces.Count == 0)
            throw new InvalidOperationException("No faces found. Expected section_brep_report.json with assemblyFaces.");

        var view = ReadViewSpec(dimensionDoc.RootElement, options);
        var projectedFaces = ProjectFaces(faces, view);
        var modelBounds = Bounds2D.Union(projectedFaces.Select(face => face.ProjectedBBox));
        projectedFaces = projectedFaces
            .Select(face => face with
            {
                NormalizedCenter = modelBounds.Normalize(face.ProjectedCenter),
                NormalizedBBox = modelBounds.Normalize(face.ProjectedBBox)
            })
            .ToList();

        var dimensions = LoadDimensions(dimensionDoc.RootElement);
        if (dimensions.Count == 0)
            throw new InvalidOperationException("No dimensions found. Expected a JSON array or an object with dimensions.");

        var drawingBounds = ReadDrawingBounds(dimensionDoc.RootElement)
            ?? Bounds2D.Union(dimensions.SelectMany(d => d.AllBoxes()));
        dimensions = dimensions.Select(d => d.Normalize(drawingBounds)).ToList();

        var results = dimensions
            .Select(dimension => MatchDimension(dimension, projectedFaces, options.Top))
            .ToList();

        return new MatchReport
        {
            Schema = "dimension_face_match_report_v1",
            BrepReportPath = Path.GetFullPath(options.BrepReportPath),
            DimensionsPath = Path.GetFullPath(options.DimensionsPath),
            View = view,
            ModelProjectedBounds = modelBounds,
            DrawingBounds = drawingBounds,
            FaceCount = faces.Count,
            Dimensions = results
        };
    }

    static DimensionMatchResult MatchDimension(DrawingDimension dimension, IReadOnlyList<ProjectedFace> faces, int top)
    {
        var anchors = dimension.Anchors.Count > 0 ? dimension.Anchors : [dimension.FallbackAnchor()];
        var anchorA = anchors[0];
        var anchorB = anchors.Count > 1 ? anchors[1] : anchors[0];

        var anchorCandidatesA = faces
            .Select(face => ScoreAnchorFace(dimension, anchorA, face))
            .OrderByDescending(score => score.Score)
            .Take(30)
            .ToList();
        var anchorCandidatesB = faces
            .Select(face => ScoreAnchorFace(dimension, anchorB, face))
            .OrderByDescending(score => score.Score)
            .Take(30)
            .ToList();

        var pairScores = new List<FacePairCandidate>();
        foreach (var a in anchorCandidatesA)
        {
            foreach (var b in anchorCandidatesB)
            {
                if (a.Face.FaceRef == b.Face.FaceRef)
                    continue;
                pairScores.Add(ScoreFacePair(dimension, a, b, swapped: false));
            }
        }

        if (anchors.Count > 1)
        {
            foreach (var a in anchorCandidatesA)
            {
                foreach (var b in anchorCandidatesB)
                {
                    if (a.Face.FaceRef == b.Face.FaceRef)
                        continue;
                    pairScores.Add(ScoreFacePair(dimension, b, a, swapped: true));
                }
            }
        }

        var distinctPairScores = pairScores
            .GroupBy(score => UnorderedPairKey(score.FaceA.FaceRef, score.FaceB.FaceRef), StringComparer.OrdinalIgnoreCase)
            .Select(group => group.OrderByDescending(score => score.Score).First())
            .OrderByDescending(score => score.Score)
            .ToList();

        var best = distinctPairScores.Take(Math.Max(top, 1)).ToList();
        var topScore = best.FirstOrDefault()?.Score ?? 0.0;
        var secondScore = distinctPairScores
            .Skip(1)
            .FirstOrDefault()?.Score ?? 0.0;

        return new DimensionMatchResult
        {
            DimensionId = dimension.Id,
            Text = dimension.Text,
            Kind = dimension.Kind,
            MeasurementMm = dimension.MeasurementMm,
            BBox = dimension.BBox,
            AnchorCount = anchors.Count,
            TopCandidates = best,
            IsUnique = topScore >= 0.72 && topScore - secondScore >= 0.12,
            TopScore = Math.Round(topScore, 4),
            SecondScore = Math.Round(secondScore, 4),
            ScoreMargin = Math.Round(topScore - secondScore, 4)
        };
    }

    static string UnorderedPairKey(string a, string b) =>
        string.Compare(a, b, StringComparison.OrdinalIgnoreCase) <= 0 ? $"{a}||{b}" : $"{b}||{a}";

    static AnchorFaceScore ScoreAnchorFace(DrawingDimension dimension, DimensionAnchor anchor, ProjectedFace face)
    {
        var positionScore = ScoreDistance(anchor.NormalizedMidpoint, face.NormalizedCenter, 0.18);
        var bboxScore = anchor.NormalizedBBox is { } anchorBox
            ? ScoreBoxSimilarity(anchorBox, face.NormalizedBBox)
            : 0.5;
        var ownerScore = ScoreOwner(dimension.OwnerComponents, face);
        var typeScore = ScoreFaceKindForDimension(dimension.Kind, face.FaceKind);

        var score = 0.50 * positionScore +
                    0.18 * bboxScore +
                    0.20 * ownerScore +
                    0.12 * typeScore;

        return new AnchorFaceScore
        {
            Face = face,
            Score = Clamp01(score),
            PositionScore = positionScore,
            BBoxScore = bboxScore,
            OwnerScore = ownerScore,
            TypeScore = typeScore
        };
    }

    static FacePairCandidate ScoreFacePair(DrawingDimension dimension, AnchorFaceScore a, AnchorFaceScore b, bool swapped)
    {
        var value = ScoreValue(dimension, a.Face, b.Face);
        var relation = ScoreRelation(dimension.Kind, a.Face, b.Face);
        var anchorScore = (a.Score + b.Score) / 2.0;
        var roleScore = ScoreRole(dimension, a.Face, b.Face);

        var score = 0.45 * value.Score +
                    0.35 * anchorScore +
                    0.12 * relation +
                    0.08 * roleScore;

        return new FacePairCandidate
        {
            Score = Math.Round(Clamp01(score), 4),
            FaceA = FaceSummary.From(a.Face),
            FaceB = FaceSummary.From(b.Face),
            ExpectedMeasurementMm = value.ExpectedMeasurementMm,
            MeasurementErrorMm = value.ErrorMm,
            Assignment = swapped ? "anchor_b_to_face_a__anchor_a_to_face_b" : "anchor_a_to_face_a__anchor_b_to_face_b",
            Components = new CandidateScoreBreakdown
            {
                ValueScore = Math.Round(value.Score, 4),
                AnchorScore = Math.Round(anchorScore, 4),
                RelationScore = Math.Round(relation, 4),
                RoleScore = Math.Round(roleScore, 4),
                AnchorAScore = Math.Round(a.Score, 4),
                AnchorBScore = Math.Round(b.Score, 4),
                AnchorAPositionScore = Math.Round(a.PositionScore, 4),
                AnchorBPositionScore = Math.Round(b.PositionScore, 4)
            },
            Evidence = BuildEvidence(dimension, a.Face, b.Face, value)
        };
    }

    static ValueScore ScoreValue(DrawingDimension dimension, ProjectedFace a, ProjectedFace b)
    {
        if (dimension.MeasurementMm is not { } measured || measured <= 0)
            return new ValueScore(0.5, null, null);

        var expected = ExpectedMeasurement(dimension.Kind, a, b);
        if (expected is not { } value || value <= 0)
            return new ValueScore(0.35, null, null);

        var error = Math.Abs(measured - value);
        var tolerance = Math.Max(0.5, measured * 0.025);
        var score = 1.0 / (1.0 + error / tolerance);
        return new ValueScore(Clamp01(score), Math.Round(value, 4), Math.Round(error, 4));
    }

    static double? ExpectedMeasurement(string kind, ProjectedFace a, ProjectedFace b)
    {
        if (kind is "diameter" or "cylindrical_fit")
        {
            var diameters = new[] { a.DiameterMm, b.DiameterMm }
                .Where(v => v is > 0)
                .Select(v => v!.Value)
                .ToList();
            if (diameters.Count > 0)
                return diameters.Average();
        }

        if (kind is "plane_distance" or "linear_distance")
        {
            if (a.FaceKind == "plane" && b.FaceKind == "plane" && a.Normal.Length >= 3 && b.Normal.Length >= 3)
            {
                var dot = Math.Abs(Vec.Dot(Vec.Normalize(a.Normal), Vec.Normalize(b.Normal)));
                if (dot >= 0.92)
                    return Math.Abs(Vec.Dot(Vec.Sub(b.CenterMm, a.CenterMm), Vec.Normalize(a.Normal)));
            }
        }

        if (kind is "axis_distance")
        {
            if (a.FaceKind == "cylinder" && b.FaceKind == "cylinder" && a.Axis.Length >= 3 && b.Axis.Length >= 3 && a.AxisPointMm.Length >= 3 && b.AxisPointMm.Length >= 3)
                return AxisDistanceMm(a.AxisPointMm, a.Axis, b.AxisPointMm);
        }

        return Distance2D(a.ProjectedCenter, b.ProjectedCenter);
    }

    static double ScoreRelation(string kind, ProjectedFace a, ProjectedFace b)
    {
        if (kind is "diameter" or "cylindrical_fit")
        {
            if (a.FaceKind != "cylinder" || b.FaceKind != "cylinder")
                return 0.15;
            var axisScore = 0.5;
            if (a.Axis.Length >= 3 && b.Axis.Length >= 3)
                axisScore = Math.Abs(Vec.Dot(Vec.Normalize(a.Axis), Vec.Normalize(b.Axis)));
            var radiusScore = 0.5;
            if (a.RadiusMm is > 0 && b.RadiusMm is > 0)
            {
                var tolerance = Math.Max(0.5, Math.Max(a.RadiusMm.Value, b.RadiusMm.Value) * 0.03);
                radiusScore = 1.0 / (1.0 + Math.Abs(a.RadiusMm.Value - b.RadiusMm.Value) / tolerance);
            }
            return Clamp01(0.55 * axisScore + 0.45 * radiusScore);
        }

        if (kind is "plane_distance" or "linear_distance")
        {
            if (a.FaceKind != "plane" || b.FaceKind != "plane")
                return 0.3;
            if (a.Normal.Length < 3 || b.Normal.Length < 3)
                return 0.55;
            return Clamp01(Math.Abs(Vec.Dot(Vec.Normalize(a.Normal), Vec.Normalize(b.Normal))));
        }

        return a.FaceKind == b.FaceKind ? 0.65 : 0.45;
    }

    static double ScoreRole(DrawingDimension dimension, ProjectedFace a, ProjectedFace b)
    {
        var text = $"{dimension.Text} {a.FunctionalRole} {b.FunctionalRole} {a.SurfaceProcessRole} {b.SurfaceProcessRole}".ToLowerInvariant();
        if (dimension.Kind is "cylindrical_fit" or "diameter")
        {
            var fitText = text.Contains('φ') || text.Contains("%%c") || text.Contains("h7") || text.Contains("h6") || text.Contains("fit") || text.Contains("bore") || text.Contains("bearing") || text.Contains("cylindrical");
            return fitText ? 0.9 : 0.55;
        }

        if (dimension.Kind is "plane_distance" or "linear_distance")
        {
            var planeText = text.Contains("plane") || text.Contains("planar") || text.Contains("contact") || text.Contains("mating") || text.Contains("distance");
            return planeText ? 0.8 : 0.55;
        }

        return 0.55;
    }

    static List<string> BuildEvidence(DrawingDimension dimension, ProjectedFace a, ProjectedFace b, ValueScore value)
    {
        var evidence = new List<string>();
        if (dimension.MeasurementMm != null && value.ExpectedMeasurementMm != null)
            evidence.Add($"measurement {dimension.MeasurementMm:0.####}mm vs expected {value.ExpectedMeasurementMm:0.####}mm");
        evidence.Add($"face kinds: {a.FaceKind} <-> {b.FaceKind}");
        evidence.Add($"components: {a.ComponentDisplayNameOrName} <-> {b.ComponentDisplayNameOrName}");
        if (!string.IsNullOrWhiteSpace(a.FunctionalRole) || !string.IsNullOrWhiteSpace(b.FunctionalRole))
            evidence.Add($"functional roles: {a.FunctionalRole} <-> {b.FunctionalRole}");
        return evidence;
    }

    static double ScoreFaceKindForDimension(string kind, string faceKind) =>
        kind switch
        {
            "diameter" or "cylindrical_fit" => faceKind == "cylinder" ? 1.0 : 0.15,
            "plane_distance" => faceKind == "plane" ? 1.0 : 0.25,
            "linear_distance" => faceKind == "plane" ? 0.75 : faceKind == "cylinder" ? 0.45 : 0.35,
            "axis_distance" => faceKind == "cylinder" ? 1.0 : 0.2,
            _ => 0.5
        };

    static double ScoreOwner(IReadOnlyList<string> owners, ProjectedFace face)
    {
        if (owners.Count == 0)
            return 1.0;

        foreach (var owner in owners)
        {
            if (string.IsNullOrWhiteSpace(owner))
                continue;
            if (ContainsNormalized(face.ComponentName, owner) ||
                ContainsNormalized(face.ComponentDisplayName, owner) ||
                ContainsNormalized(face.ComponentPath, owner) ||
                ContainsNormalized(face.ComponentFileName, owner))
                return 1.0;
        }

        return 0.2;
    }

    static bool ContainsNormalized(string left, string right) =>
        NormalizeText(left).Contains(NormalizeText(right), StringComparison.OrdinalIgnoreCase) ||
        NormalizeText(right).Contains(NormalizeText(left), StringComparison.OrdinalIgnoreCase);

    static string NormalizeText(string value) =>
        new(value.Where(ch => !char.IsWhiteSpace(ch) && ch is not '-' and not '_' and not '.').ToArray());

    static double ScoreBoxSimilarity(Bounds2D a, Bounds2D b)
    {
        var center = ScoreDistance(a.Center, b.Center, 0.20);
        var aw = Math.Max(a.Width, 1e-9);
        var ah = Math.Max(a.Height, 1e-9);
        var bw = Math.Max(b.Width, 1e-9);
        var bh = Math.Max(b.Height, 1e-9);
        var size = 1.0 / (1.0 + Math.Abs(Math.Log(aw / bw)) + Math.Abs(Math.Log(ah / bh)));
        return Clamp01(0.65 * center + 0.35 * size);
    }

    static double ScoreDistance(Point2D a, Point2D b, double tolerance)
    {
        var d = Distance2D(a, b);
        return Clamp01(1.0 / (1.0 + d / Math.Max(tolerance, 1e-9)));
    }

    static List<ProjectedFace> ProjectFaces(IReadOnlyList<ModelFace> faces, ViewSpec view)
    {
        return faces.Select(face =>
        {
            var center = ProjectPoint(face.CenterMm, view);
            var points = Corners(face.BBoxMm).Select(point => ProjectPoint(point, view)).ToList();
            var box = Bounds2D.Union(points);
            return new ProjectedFace(
                face.FaceRef,
                face.ComponentName,
                face.ComponentDisplayName,
                face.ComponentPath,
                face.ComponentFileName,
                face.FaceIndex,
                face.FaceKind,
                face.CenterMm,
                face.BBoxMm,
                face.Normal,
                face.AxisPointMm,
                face.Axis,
                face.RadiusMm,
                face.DiameterMm,
                face.FunctionalRole,
                face.SurfaceProcessRole,
                center,
                box,
                new Point2D(0, 0),
                new Bounds2D(0, 0, 0, 0));
        }).ToList();
    }

    static Point2D ProjectPoint(double[] p, ViewSpec view)
    {
        var basis = ViewBasis.FromDirection(view.Direction);
        var u = Vec.Dot(p, basis.U);
        var v = Vec.Dot(p, basis.V);
        var radians = view.RollDeg * Math.PI / 180.0;
        var cos = Math.Cos(radians);
        var sin = Math.Sin(radians);
        return new Point2D(u * cos - v * sin, u * sin + v * cos);
    }

    static List<ModelFace> LoadFaces(JsonElement root)
    {
        if (!TryGetProperty(root, "assemblyFaces", out var facesEl) || facesEl.ValueKind != JsonValueKind.Array)
            return [];

        var faces = new List<ModelFace>();
        foreach (var faceEl in facesEl.EnumerateArray())
        {
            var componentName = ReadString(faceEl, "componentName");
            var faceIndex = ReadInt(faceEl, "faceIndex") ?? 0;
            var bbox = ReadDoubleArray(faceEl, "bBoxMm", "bboxMm", "bbox_mm");
            var center = ReadDoubleArray(faceEl, "centerMm", "center_mm");
            if (bbox.Length < 6 || center.Length < 3)
                continue;

            var face = new ModelFace
            {
                ComponentName = componentName,
                ComponentDisplayName = ReadString(faceEl, "componentDisplayName"),
                ComponentPath = ReadString(faceEl, "componentPath"),
                ComponentFileName = ReadString(faceEl, "componentFileName"),
                FaceIndex = faceIndex,
                FaceRef = $"{componentName}:face#{faceIndex}",
                FaceKind = ReadString(faceEl, "faceKind", "kind").ToLowerInvariant(),
                AreaMm2 = ReadDouble(faceEl, "areaMm2", "area_mm2") ?? 0,
                CenterMm = center,
                BBoxMm = bbox,
                Normal = ReadDoubleArray(faceEl, "normal"),
                AxisPointMm = ReadDoubleArray(faceEl, "axisPointMm", "axis_point_mm"),
                Axis = ReadDoubleArray(faceEl, "axis"),
                RadiusMm = ReadDouble(faceEl, "radiusMm", "radius_mm"),
                DiameterMm = ReadDouble(faceEl, "diameterMm", "diameter_mm"),
                FunctionalRole = ReadString(faceEl, "functionalRole", "functional_role"),
                SurfaceProcessRole = ReadString(faceEl, "surfaceProcessRole", "surface_process_role")
            };
            faces.Add(face);
        }

        return faces;
    }

    static List<DrawingDimension> LoadDimensions(JsonElement root)
    {
        var array = root.ValueKind == JsonValueKind.Array
            ? root
            : TryGetProperty(root, "dimensions", out var dimensionsEl) && dimensionsEl.ValueKind == JsonValueKind.Array
                ? dimensionsEl
                : default;
        if (array.ValueKind != JsonValueKind.Array)
            return [];

        var result = new List<DrawingDimension>();
        var index = 0;
        foreach (var dimEl in array.EnumerateArray())
        {
            index++;
            var bbox = ReadBounds2D(dimEl, "bbox", "box");
            var measurement = ReadDouble(dimEl, "measurementMm", "measurement_mm", "measurement", "valueMm", "value_mm");
            var text = FirstNonEmpty(
                ReadString(dimEl, "text"),
                ReadString(dimEl, "textOverride", "text_override"),
                ReadString(dimEl, "rawText", "raw_text"));

            var anchors = LoadAnchors(dimEl);
            if (anchors.Count == 0 && bbox != null)
            {
                anchors.Add(new DimensionAnchor
                {
                    Id = "bbox_center",
                    BBox = bbox,
                    Midpoint = bbox.Value.Center
                });
            }

            var owners = ReadStringArray(dimEl, "ownerComponents", "owner_components", "components");
            AddIfNotEmpty(owners, ReadString(dimEl, "ownerComponentA", "owner_component_a", "componentA", "component_a"));
            AddIfNotEmpty(owners, ReadString(dimEl, "ownerComponentB", "owner_component_b", "componentB", "component_b"));

            result.Add(new DrawingDimension
            {
                Id = FirstNonEmpty(ReadString(dimEl, "id"), ReadString(dimEl, "handle"), $"dimension_{index:0000}"),
                Text = text,
                Kind = InferKind(FirstNonEmpty(ReadString(dimEl, "kind"), ReadString(dimEl, "relationType", "relation_type")), text),
                MeasurementMm = measurement,
                BBox = bbox,
                Anchors = anchors,
                OwnerComponents = owners
            });
        }

        return result;
    }

    static List<DimensionAnchor> LoadAnchors(JsonElement dimEl)
    {
        var anchors = new List<DimensionAnchor>();
        if (TryGetProperty(dimEl, "anchors", out var anchorsEl) && anchorsEl.ValueKind == JsonValueKind.Array)
        {
            var index = 0;
            foreach (var anchorEl in anchorsEl.EnumerateArray())
            {
                index++;
                var anchor = ReadAnchor(anchorEl, $"anchor_{index}");
                if (anchor != null)
                    anchors.Add(anchor);
            }
        }

        foreach (var key in new[] { "anchorA", "anchorB", "anchor_a", "anchor_b" })
        {
            if (TryGetProperty(dimEl, key, out var anchorEl) && anchorEl.ValueKind == JsonValueKind.Object)
            {
                var anchor = ReadAnchor(anchorEl, key);
                if (anchor != null)
                    anchors.Add(anchor);
            }
        }

        return anchors;
    }

    static DimensionAnchor? ReadAnchor(JsonElement anchorEl, string fallbackId)
    {
        var bbox = ReadBounds2D(anchorEl, "bbox", "box");
        var midpoint = ReadPoint2D(anchorEl, "midpoint", "center", "point");
        var start = ReadPoint2D(anchorEl, "start");
        var end = ReadPoint2D(anchorEl, "end");
        if (midpoint == null && start != null && end != null)
            midpoint = new Point2D((start.Value.X + end.Value.X) / 2.0, (start.Value.Y + end.Value.Y) / 2.0);
        if (bbox == null && start != null && end != null)
            bbox = Bounds2D.Union([start.Value, end.Value]);
        if (midpoint == null && bbox != null)
            midpoint = bbox.Value.Center;
        if (midpoint == null)
            return null;

        return new DimensionAnchor
        {
            Id = FirstNonEmpty(ReadString(anchorEl, "id"), fallbackId),
            BBox = bbox,
            Midpoint = midpoint.Value,
            Start = start,
            End = end
        };
    }

    static ViewSpec ReadViewSpec(JsonElement root, CliOptions options)
    {
        var direction = options.ViewDirection;
        var roll = options.RollDeg;
        if (TryGetProperty(root, "view", out var viewEl) && viewEl.ValueKind == JsonValueKind.Object)
        {
            direction = FirstNonEmpty(ReadString(viewEl, "direction", "viewDirection", "view_direction"), direction);
            roll = ReadDouble(viewEl, "rollDeg", "roll_deg", "roll") ?? roll;
        }

        return new ViewSpec
        {
            Direction = NormalizeDirection(direction),
            RollDeg = NormalizeRoll(roll)
        };
    }

    static Bounds2D? ReadDrawingBounds(JsonElement root)
    {
        if (TryGetProperty(root, "view", out var viewEl) && viewEl.ValueKind == JsonValueKind.Object)
        {
            var viewBox = ReadBounds2D(viewEl, "drawingBBox", "drawing_bbox", "mainViewBBox", "main_view_bbox");
            if (viewBox != null)
                return viewBox;
        }

        return ReadBounds2D(root, "drawingBBox", "drawing_bbox", "mainViewBBox", "main_view_bbox");
    }

    static string InferKind(string explicitKind, string text)
    {
        if (!string.IsNullOrWhiteSpace(explicitKind))
            return explicitKind.Trim().ToLowerInvariant();

        var lower = text.ToLowerInvariant();
        if (lower.Contains("h7") || lower.Contains("h6") || lower.Contains("g6") || lower.Contains("f7") || lower.Contains("fit"))
            return "cylindrical_fit";
        if (text.Contains('Φ') || lower.Contains("%%c") || lower.Contains("diameter"))
            return "diameter";
        if (lower.Contains("axis") || lower.Contains("center"))
            return "axis_distance";
        return "linear_distance";
    }

    static string NormalizeDirection(string value)
    {
        var v = value.Trim().ToUpperInvariant();
        return v switch
        {
            "X" or "+X" or "PX" => "+X",
            "-X" or "NX" => "-X",
            "Y" or "+Y" or "PY" => "+Y",
            "-Y" or "NY" => "-Y",
            "Z" or "+Z" or "PZ" => "+Z",
            "-Z" or "NZ" => "-Z",
            _ => throw new ArgumentException($"Unsupported view direction `{value}`. Use +X/-X/+Y/-Y/+Z/-Z.")
        };
    }

    static double NormalizeRoll(double value)
    {
        var roll = value % 360.0;
        if (roll < 0)
            roll += 360.0;
        return roll;
    }

    static double AxisDistanceMm(double[] pointA, double[] axisA, double[] pointB)
    {
        var axis = Vec.Normalize(axisA);
        var delta = Vec.Sub(pointB, pointA);
        var along = Vec.Mul(axis, Vec.Dot(delta, axis));
        return Vec.Length(Vec.Sub(delta, along));
    }

    static IEnumerable<double[]> Corners(double[] box)
    {
        var xs = new[] { box[0], box[3] };
        var ys = new[] { box[1], box[4] };
        var zs = new[] { box[2], box[5] };
        foreach (var x in xs)
        foreach (var y in ys)
        foreach (var z in zs)
            yield return [x, y, z];
    }

    static double Distance2D(Point2D a, Point2D b)
    {
        var dx = a.X - b.X;
        var dy = a.Y - b.Y;
        return Math.Sqrt(dx * dx + dy * dy);
    }

    static double Clamp01(double value) => Math.Min(1.0, Math.Max(0.0, value));

    static bool TryGetProperty(JsonElement element, string name, out JsonElement value)
    {
        if (element.ValueKind == JsonValueKind.Object)
        {
            foreach (var property in element.EnumerateObject())
            {
                if (property.NameEquals(name) || string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase))
                {
                    value = property.Value;
                    return true;
                }
            }
        }

        value = default;
        return false;
    }

    static string ReadString(JsonElement element, params string[] names)
    {
        foreach (var name in names)
        {
            if (TryGetProperty(element, name, out var value))
            {
                if (value.ValueKind == JsonValueKind.String)
                    return value.GetString() ?? "";
                if (value.ValueKind is JsonValueKind.Number or JsonValueKind.True or JsonValueKind.False)
                    return value.ToString();
            }
        }

        return "";
    }

    static double? ReadDouble(JsonElement element, params string[] names)
    {
        foreach (var name in names)
        {
            if (!TryGetProperty(element, name, out var value))
                continue;
            if (value.ValueKind == JsonValueKind.Number && value.TryGetDouble(out var number))
                return number;
            if (value.ValueKind == JsonValueKind.String && double.TryParse(value.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture, out number))
                return number;
        }

        return null;
    }

    static int? ReadInt(JsonElement element, params string[] names)
    {
        foreach (var name in names)
        {
            if (!TryGetProperty(element, name, out var value))
                continue;
            if (value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out var number))
                return number;
            if (value.ValueKind == JsonValueKind.String && int.TryParse(value.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out number))
                return number;
        }

        return null;
    }

    static double[] ReadDoubleArray(JsonElement element, params string[] names)
    {
        foreach (var name in names)
        {
            if (!TryGetProperty(element, name, out var value))
                continue;
            if (value.ValueKind == JsonValueKind.Array)
                return value.EnumerateArray()
                    .Select(v => v.ValueKind == JsonValueKind.Number && v.TryGetDouble(out var d) ? d : double.NaN)
                    .Where(d => !double.IsNaN(d))
                    .ToArray();
        }

        return [];
    }

    static List<string> ReadStringArray(JsonElement element, params string[] names)
    {
        foreach (var name in names)
        {
            if (!TryGetProperty(element, name, out var value))
                continue;
            if (value.ValueKind == JsonValueKind.Array)
                return value.EnumerateArray()
                    .Select(v => v.ValueKind == JsonValueKind.String ? v.GetString() ?? "" : v.ToString())
                    .Where(v => !string.IsNullOrWhiteSpace(v))
                    .Distinct(StringComparer.OrdinalIgnoreCase)
                    .ToList();
            if (value.ValueKind == JsonValueKind.String)
                return value.GetString()!
                    .Split([';', ',', '|'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
                    .ToList();
        }

        return [];
    }

    static Bounds2D? ReadBounds2D(JsonElement element, params string[] names)
    {
        foreach (var name in names)
        {
            if (!TryGetProperty(element, name, out var value))
                continue;
            if (value.ValueKind == JsonValueKind.Object)
            {
                var minX = ReadDouble(value, "minX", "min_x", "xMin", "xmin");
                var minY = ReadDouble(value, "minY", "min_y", "yMin", "ymin");
                var maxX = ReadDouble(value, "maxX", "max_x", "xMax", "xmax");
                var maxY = ReadDouble(value, "maxY", "max_y", "yMax", "ymax");
                if (minX != null && minY != null && maxX != null && maxY != null)
                    return new Bounds2D(minX.Value, minY.Value, maxX.Value, maxY.Value).Ordered();
            }
            if (value.ValueKind == JsonValueKind.Array)
            {
                var values = value.EnumerateArray()
                    .Where(v => v.ValueKind == JsonValueKind.Number)
                    .Select(v => v.GetDouble())
                    .ToList();
                if (values.Count >= 4)
                    return new Bounds2D(values[0], values[1], values[2], values[3]).Ordered();
            }
        }

        return null;
    }

    static Point2D? ReadPoint2D(JsonElement element, params string[] names)
    {
        foreach (var name in names)
        {
            if (!TryGetProperty(element, name, out var value))
                continue;
            if (value.ValueKind == JsonValueKind.Array)
            {
                var values = value.EnumerateArray()
                    .Where(v => v.ValueKind == JsonValueKind.Number)
                    .Select(v => v.GetDouble())
                    .ToList();
                if (values.Count >= 2)
                    return new Point2D(values[0], values[1]);
            }
            if (value.ValueKind == JsonValueKind.Object)
            {
                var x = ReadDouble(value, "x", "X");
                var y = ReadDouble(value, "y", "Y");
                if (x != null && y != null)
                    return new Point2D(x.Value, y.Value);
            }
        }

        return null;
    }

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

    static void AddIfNotEmpty(List<string> values, string value)
    {
        if (!string.IsNullOrWhiteSpace(value) && !values.Contains(value, StringComparer.OrdinalIgnoreCase))
            values.Add(value);
    }
}

static class JsonDefaults
{
    public static readonly JsonSerializerOptions Options = new()
    {
        WriteIndented = true,
        Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
    };
}

sealed class CliOptions
{
    public string BrepReportPath { get; set; } = "";
    public string DimensionsPath { get; set; } = "";
    public string OutputDir { get; set; } = Path.Combine(Environment.CurrentDirectory, "runtime", "dimension-face-matcher");
    public string? OutputPath { get; set; }
    public string ViewDirection { get; set; } = "+X";
    public double RollDeg { get; set; }
    public int Top { get; set; } = 8;

    public static CliOptions Parse(string[] args)
    {
        var options = new CliOptions();
        for (var i = 0; i < args.Length; i++)
        {
            var arg = args[i];
            string Next()
            {
                if (i + 1 >= args.Length)
                    throw new ArgumentException($"Missing value for {arg}");
                return args[++i];
            }

            switch (arg)
            {
                case "--brep-report":
                    options.BrepReportPath = Next();
                    break;
                case "--dimensions":
                    options.DimensionsPath = Next();
                    break;
                case "--output-dir":
                    options.OutputDir = Next();
                    break;
                case "--output":
                    options.OutputPath = Next();
                    break;
                case "--view-direction":
                    options.ViewDirection = Next();
                    break;
                case "--roll":
                case "--roll-deg":
                    options.RollDeg = double.Parse(Next(), CultureInfo.InvariantCulture);
                    break;
                case "--top":
                    options.Top = int.Parse(Next(), CultureInfo.InvariantCulture);
                    break;
                case "-h":
                case "--help":
                    PrintUsage();
                    Environment.Exit(0);
                    break;
                default:
                    throw new ArgumentException($"Unknown argument: {arg}");
            }
        }

        return options;
    }

    static void PrintUsage()
    {
        Console.WriteLine("""
        Usage:
          dotnet run --project tools\drawing-diagnostics\DimensionFaceMatcher\DimensionFaceMatcher.csproj -- \
            --brep-report <section_brep_report.json> \
            --dimensions <dimension_anchors.json> \
            --view-direction +X \
            --roll 270 \
            --output-dir runtime\dimension-face-matcher

        Dimension input schema:
        {
          "view": {
            "direction": "+X",
            "rollDeg": 270,
            "drawingBBox": { "minX": 0, "minY": 0, "maxX": 300, "maxY": 200 }
          },
          "dimensions": [
            {
              "id": "dim_001",
              "text": "Φ30 H7/h6",
              "measurementMm": 30,
              "kind": "cylindrical_fit",
              "ownerComponents": ["轴", "轴承"],
              "anchors": [
                { "id": "a", "bbox": { "minX": 10, "minY": 20, "maxX": 10, "maxY": 60 } },
                { "id": "b", "bbox": { "minX": 45, "minY": 20, "maxX": 45, "maxY": 60 } }
              ]
            }
          ]
        }
        """);
    }
}

sealed record MatchReport
{
    public string Schema { get; init; } = "";
    public string BrepReportPath { get; init; } = "";
    public string DimensionsPath { get; init; } = "";
    public ViewSpec View { get; init; } = new();
    public Bounds2D ModelProjectedBounds { get; init; }
    public Bounds2D DrawingBounds { get; init; }
    public int FaceCount { get; init; }
    public List<DimensionMatchResult> Dimensions { get; init; } = [];
}

sealed record DimensionMatchResult
{
    public string DimensionId { get; init; } = "";
    public string Text { get; init; } = "";
    public string Kind { get; init; } = "";
    public double? MeasurementMm { get; init; }
    public Bounds2D? BBox { get; init; }
    public int AnchorCount { get; init; }
    public bool IsUnique { get; init; }
    public double TopScore { get; init; }
    public double SecondScore { get; init; }
    public double ScoreMargin { get; init; }
    public List<FacePairCandidate> TopCandidates { get; init; } = [];
}

sealed record FacePairCandidate
{
    public double Score { get; init; }
    public FaceSummary FaceA { get; init; } = new();
    public FaceSummary FaceB { get; init; } = new();
    public double? ExpectedMeasurementMm { get; init; }
    public double? MeasurementErrorMm { get; init; }
    public string Assignment { get; init; } = "";
    public CandidateScoreBreakdown Components { get; init; } = new();
    public List<string> Evidence { get; init; } = [];
}

sealed record FaceSummary
{
    public string FaceRef { get; init; } = "";
    public string ComponentName { get; init; } = "";
    public string ComponentDisplayName { get; init; } = "";
    public int FaceIndex { get; init; }
    public string FaceKind { get; init; } = "";
    public string FunctionalRole { get; init; } = "";
    public string SurfaceProcessRole { get; init; } = "";
    public double[] CenterMm { get; init; } = [];
    public double[] BBoxMm { get; init; } = [];
    public Point2D ProjectedCenter { get; init; }
    public Bounds2D ProjectedBBox { get; init; }
    public double? RadiusMm { get; init; }
    public double? DiameterMm { get; init; }

    public static FaceSummary From(ProjectedFace face) => new()
    {
        FaceRef = face.FaceRef,
        ComponentName = face.ComponentName,
        ComponentDisplayName = face.ComponentDisplayName,
        FaceIndex = face.FaceIndex,
        FaceKind = face.FaceKind,
        FunctionalRole = face.FunctionalRole,
        SurfaceProcessRole = face.SurfaceProcessRole,
        CenterMm = face.CenterMm,
        BBoxMm = face.BBoxMm,
        ProjectedCenter = face.ProjectedCenter,
        ProjectedBBox = face.ProjectedBBox,
        RadiusMm = face.RadiusMm,
        DiameterMm = face.DiameterMm
    };
}

sealed record CandidateScoreBreakdown
{
    public double ValueScore { get; init; }
    public double AnchorScore { get; init; }
    public double RelationScore { get; init; }
    public double RoleScore { get; init; }
    public double AnchorAScore { get; init; }
    public double AnchorBScore { get; init; }
    public double AnchorAPositionScore { get; init; }
    public double AnchorBPositionScore { get; init; }
}

sealed record DrawingDimension
{
    public string Id { get; init; } = "";
    public string Text { get; init; } = "";
    public string Kind { get; init; } = "linear_distance";
    public double? MeasurementMm { get; init; }
    public Bounds2D? BBox { get; init; }
    public List<DimensionAnchor> Anchors { get; init; } = [];
    public List<string> OwnerComponents { get; init; } = [];

    public DrawingDimension Normalize(Bounds2D bounds) => this with
    {
        BBox = BBox == null ? null : bounds.Normalize(BBox.Value),
        Anchors = Anchors.Select(a => a.Normalize(bounds)).ToList()
    };

    public DimensionAnchor FallbackAnchor() => new()
    {
        Id = "dimension_bbox_center",
        BBox = BBox,
        Midpoint = BBox?.Center ?? new Point2D(0, 0),
        NormalizedMidpoint = BBox?.Center ?? new Point2D(0, 0),
        NormalizedBBox = BBox
    };

    public IEnumerable<Bounds2D> AllBoxes()
    {
        if (BBox != null)
            yield return BBox.Value;
        foreach (var anchor in Anchors)
        {
            if (anchor.BBox != null)
                yield return anchor.BBox.Value;
        }
    }
}

sealed record DimensionAnchor
{
    public string Id { get; init; } = "";
    public Bounds2D? BBox { get; init; }
    public Point2D Midpoint { get; init; }
    public Point2D? Start { get; init; }
    public Point2D? End { get; init; }
    public Point2D NormalizedMidpoint { get; init; }
    public Bounds2D? NormalizedBBox { get; init; }

    public DimensionAnchor Normalize(Bounds2D bounds) => this with
    {
        NormalizedMidpoint = bounds.Normalize(Midpoint),
        NormalizedBBox = BBox == null ? null : bounds.Normalize(BBox.Value)
    };
}

sealed record ModelFace
{
    public string FaceRef { get; init; } = "";
    public string ComponentName { get; init; } = "";
    public string ComponentDisplayName { get; init; } = "";
    public string ComponentPath { get; init; } = "";
    public string ComponentFileName { get; init; } = "";
    public int FaceIndex { get; init; }
    public string FaceKind { get; init; } = "";
    public double AreaMm2 { get; init; }
    public double[] CenterMm { get; init; } = [];
    public double[] BBoxMm { get; init; } = [];
    public double[] Normal { get; init; } = [];
    public double[] AxisPointMm { get; init; } = [];
    public double[] Axis { get; init; } = [];
    public double? RadiusMm { get; init; }
    public double? DiameterMm { get; init; }
    public string FunctionalRole { get; init; } = "";
    public string SurfaceProcessRole { get; init; } = "";
}

sealed record ProjectedFace(
    string FaceRef,
    string ComponentName,
    string ComponentDisplayName,
    string ComponentPath,
    string ComponentFileName,
    int FaceIndex,
    string FaceKind,
    double[] CenterMm,
    double[] BBoxMm,
    double[] Normal,
    double[] AxisPointMm,
    double[] Axis,
    double? RadiusMm,
    double? DiameterMm,
    string FunctionalRole,
    string SurfaceProcessRole,
    Point2D ProjectedCenter,
    Bounds2D ProjectedBBox,
    Point2D NormalizedCenter,
    Bounds2D NormalizedBBox)
{
    public string ComponentDisplayNameOrName => string.IsNullOrWhiteSpace(ComponentDisplayName) ? ComponentName : ComponentDisplayName;
}

sealed record AnchorFaceScore
{
    public ProjectedFace Face { get; init; } = null!;
    public double Score { get; init; }
    public double PositionScore { get; init; }
    public double BBoxScore { get; init; }
    public double OwnerScore { get; init; }
    public double TypeScore { get; init; }
}

readonly record struct ValueScore(double Score, double? ExpectedMeasurementMm, double? ErrorMm);

sealed record ViewSpec
{
    public string Direction { get; init; } = "+X";
    public double RollDeg { get; init; }
}

readonly record struct ViewBasis(double[] U, double[] V)
{
    public static ViewBasis FromDirection(string direction) => direction switch
    {
        "+X" => new([0, 1, 0], [0, 0, 1]),
        "-X" => new([0, -1, 0], [0, 0, 1]),
        "+Y" => new([-1, 0, 0], [0, 0, 1]),
        "-Y" => new([1, 0, 0], [0, 0, 1]),
        "+Z" => new([1, 0, 0], [0, 1, 0]),
        "-Z" => new([-1, 0, 0], [0, 1, 0]),
        _ => throw new ArgumentException($"Unsupported direction `{direction}`")
    };
}

readonly record struct Point2D(double X, double Y);

readonly record struct Bounds2D(double MinX, double MinY, double MaxX, double MaxY)
{
    public double Width => Math.Max(MaxX - MinX, 1e-9);
    public double Height => Math.Max(MaxY - MinY, 1e-9);
    public Point2D Center => new((MinX + MaxX) / 2.0, (MinY + MaxY) / 2.0);

    public Bounds2D Ordered() => new(Math.Min(MinX, MaxX), Math.Min(MinY, MaxY), Math.Max(MinX, MaxX), Math.Max(MinY, MaxY));

    public Point2D Normalize(Point2D point) => new((point.X - Center.X) / Width, (point.Y - Center.Y) / Height);

    public Bounds2D Normalize(Bounds2D box)
    {
        var a = Normalize(new Point2D(box.MinX, box.MinY));
        var b = Normalize(new Point2D(box.MaxX, box.MaxY));
        return new Bounds2D(a.X, a.Y, b.X, b.Y).Ordered();
    }

    public static Bounds2D Union(IEnumerable<Point2D> points)
    {
        var list = points.ToList();
        if (list.Count == 0)
            return new Bounds2D(-0.5, -0.5, 0.5, 0.5);
        return new Bounds2D(
            list.Min(p => p.X),
            list.Min(p => p.Y),
            list.Max(p => p.X),
            list.Max(p => p.Y));
    }

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

static class Vec
{
    public static double Dot(double[] a, double[] b) => a.Zip(b).Sum(pair => pair.First * pair.Second);
    public static double[] Sub(double[] a, double[] b) => a.Zip(b).Select(pair => pair.First - pair.Second).ToArray();
    public static double[] Mul(double[] a, double scale) => a.Select(v => v * scale).ToArray();
    public static double Length(double[] a) => Math.Sqrt(a.Sum(v => v * v));
    public static double[] Normalize(double[] a)
    {
        var length = Length(a);
        return length <= 1e-9 ? a.ToArray() : a.Select(v => v / length).ToArray();
    }
}
