static class PartModelProcessing
{
    public const string DirectPartBrepSource = "part_brep_direct";

    public static List<ComponentHighlightImageRequest> BuildComponentHighlightImageRequests(SectionBrepReport report) => [];

    public static List<FeatureHighlightImageRequest> BuildFeatureHighlightImageRequests(SectionBrepReport report)
    {
        var result = new List<FeatureHighlightImageRequest>();
        var component = report.AssemblyComponents
            .FirstOrDefault(c => c.ReviewScope.Equals("part_design_required", StringComparison.OrdinalIgnoreCase))
            ?? report.AssemblyComponents.FirstOrDefault();
        if (component == null)
            return result;

        var facesByRef = report.AssemblyFaces
            .GroupBy(FaceRef, StringComparer.OrdinalIgnoreCase)
            .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);

        foreach (var feature in report.FeatureGraph.Features
            .Where(f => f.FaceRefs.Count > 0)
            .Where(f => SameComponent(f, component))
            .OrderBy(f => FeatureHighlightPriority(f.Type))
            .ThenBy(f => f.Id, StringComparer.OrdinalIgnoreCase))
        {
            var targetFaces = feature.FaceRefs
                .Select(faceRef => facesByRef.TryGetValue(faceRef, out var face) ? face : null)
                .OfType<AssemblyFaceEvidence>()
                .ToList();
            if (targetFaces.Count == 0)
                continue;

            var viewChoice = ChooseBestObliqueFeatureView(feature, targetFaces, report.AssemblyFaces);
            result.Add(new FeatureHighlightImageRequest
            {
                PlanId = $"feature_highlight_{SanitizeId(feature.Id)}",
                ComponentId = component.Id,
                InstanceName = component.InstanceName,
                DisplayName = component.DisplayName,
                ComponentPath = component.Path,
                FeatureId = feature.Id,
                FeatureType = feature.Type,
                FaceRefs = feature.FaceRefs.Distinct(StringComparer.OrdinalIgnoreCase).ToList(),
                Views = [viewChoice.ViewName],
                PreferredDirectionMm = Vec.Round(viewChoice.Direction),
                BlockingFaceRefs = viewChoice.BlockingFaceRefs,
                SelectionBasis = "feature_graph_face_refs_grouped_before_image_export"
            });
        }

        return result;
    }

    public static List<ComponentContextImageRequest> BuildComponentContextImageRequests(SectionBrepReport report) => [];

    public static AutoSectionImagePlan BuildAxisSectionPlan(SectionBrepReport report)
    {
        var plan = new AutoSectionImagePlan
        {
            MaxRequests = 1
        };

        var axisFace = report.AssemblyFaces
            .Where(face => face.FaceKind.Equals("cylinder", StringComparison.OrdinalIgnoreCase))
            .Where(face => face.Axis.Length >= 3 && face.AxisPointMm.Length >= 3)
            .OrderByDescending(face => face.AreaMm2)
            .ThenByDescending(face => face.RadiusMm)
            .FirstOrDefault();
        if (axisFace == null)
            return plan;

        var containedAxis = Vec.Normalize(axisFace.Axis);
        var coaxialFaces = report.AssemblyFaces
            .Where(face => face.FaceKind.Equals("cylinder", StringComparison.OrdinalIgnoreCase))
            .Where(face => face.Axis.Length >= 3 && face.AxisPointMm.Length >= 3)
            .Where(face => Math.Abs(Vec.Dot(Vec.Normalize(face.Axis), containedAxis)) >= 0.95)
            .ToList();
        if (!HasInternalRotationalStructure(coaxialFaces))
            return plan;

        var target = string.IsNullOrWhiteSpace(axisFace.ComponentDisplayName)
            ? axisFace.ComponentName
            : axisFace.ComponentDisplayName;
        var planeCandidates = ChooseSectionPlaneKeys(containedAxis);
        for (var i = 0; i < Math.Min(plan.MaxRequests, planeCandidates.Count); i++)
        {
            var candidate = planeCandidates[i];
            plan.Requests.Add(new AutoSectionImageRequest
            {
                Request = $"part_axis_center_section_{i + 1}",
                Rule = "For SLDPRT rotational parts with internal/coaxial structure, create a section plane through the main cylindrical axis.",
                Target = target,
                Reason = "Part input has no assembly components to hide or highlight; section evidence is generated directly from the opened part body.",
                PlaneKey = candidate.PlaneKey,
                OffsetMm = 0,
                OriginMm = Vec.Round(axisFace.AxisPointMm),
                PreferredNormalMm = Vec.Round(candidate.Normal),
                PreferredContainedAxisMm = Vec.Round(containedAxis),
                SourceRefs = [$"{axisFace.ComponentName}:face#{axisFace.FaceIndex}"]
            });
        }

        return plan;
    }

    public static List<string> BuildSectionViewRequests(SectionBrepReport report)
    {
        return report.SectionImagePlan.Requests
            .Where(request => !string.IsNullOrWhiteSpace(request.PlaneKey))
            .Take(2)
            .Select(request =>
                $"{request.PlaneKey}:{request.Request}|target={SanitizeMetadata(request.Target)};reason={SanitizeMetadata(request.Reason)}")
            .ToList();
    }

    static bool HasInternalRotationalStructure(IReadOnlyList<AssemblyFaceEvidence> coaxialFaces)
    {
        if (coaxialFaces.Count < 2)
            return false;

        var radiusLayers = coaxialFaces
            .Where(face => face.RadiusMm > 0)
            .Select(face => Math.Round(face.RadiusMm, 1))
            .Distinct()
            .Count();
        if (radiusLayers >= 2)
            return true;

        return coaxialFaces.Any(face =>
            face.FunctionalRole.Contains("hole", StringComparison.OrdinalIgnoreCase) ||
            face.FunctionalRole.Contains("bore", StringComparison.OrdinalIgnoreCase) ||
            face.SurfaceProcessRole.Contains("hole", StringComparison.OrdinalIgnoreCase) ||
            face.SurfaceProcessRole.Contains("bore", StringComparison.OrdinalIgnoreCase));
    }

    static List<(string PlaneKey, double[] Normal)> ChooseSectionPlaneKeys(double[] containedAxis)
    {
        var candidates = new List<(string PlaneKey, double[] Normal)>
        {
            ("front", [0.0, 0.0, 1.0]),
            ("top", [0.0, 1.0, 0.0]),
            ("right", [1.0, 0.0, 0.0])
        };

        return candidates
            .OrderBy(candidate => Math.Abs(Vec.Dot(containedAxis, candidate.Normal)))
            .ThenBy(candidate => candidate.PlaneKey, StringComparer.OrdinalIgnoreCase)
            .Take(2)
            .ToList();
    }

    static string SanitizeMetadata(string value) =>
        (value ?? "").Replace(';', ',').Replace('|', '/').Replace('\r', ' ').Replace('\n', ' ').Trim();

    static bool SameComponent(MechanicalFeature feature, AssemblyComponentSummary component)
    {
        if (!string.IsNullOrWhiteSpace(feature.ComponentPath) &&
            !string.IsNullOrWhiteSpace(component.Path) &&
            feature.ComponentPath.Equals(component.Path, StringComparison.OrdinalIgnoreCase))
            return true;

        return feature.ComponentName.Equals(component.InstanceName, StringComparison.OrdinalIgnoreCase);
    }

    static int FeatureHighlightPriority(string featureType)
    {
        if (featureType.Contains("unknown", StringComparison.OrdinalIgnoreCase) ||
            featureType.Contains("complex", StringComparison.OrdinalIgnoreCase))
            return 0;
        if (featureType.Contains("hole", StringComparison.OrdinalIgnoreCase))
            return 1;
        if (featureType.Contains("cylindrical", StringComparison.OrdinalIgnoreCase))
            return 2;
        if (featureType.Contains("planar", StringComparison.OrdinalIgnoreCase))
            return 3;
        return 4;
    }

    static (string ViewName, double[] Direction, List<string> BlockingFaceRefs) ChooseBestObliqueFeatureView(
        MechanicalFeature feature,
        IReadOnlyList<AssemblyFaceEvidence> targetFaces,
        IReadOnlyList<AssemblyFaceEvidence> allFaces)
    {
        var targetBox = UnionBoxes(targetFaces.Select(f => f.BBoxMm));
        if (targetBox.Length < 6)
            return ("isometric", [1.0, 1.0, 1.0], []);

        var targetRefs = feature.FaceRefs.ToHashSet(StringComparer.OrdinalIgnoreCase);
        var others = allFaces
            .Where(face => !targetRefs.Contains(FaceRef(face)))
            .Where(face => face.BBoxMm.Length >= 6)
            .ToList();

        var isPlanarTarget = IsPlanarFeatureTarget(feature, targetFaces);
        var preferredViewSide = DeterminePreferredViewSide(feature, targetFaces, allFaces, targetBox);
        var candidates = BuildObliqueCandidateDirections(false);
        candidates = EnforcePreferredViewSide(candidates, preferredViewSide, isPlanarTarget);

        var projectedTargetAreas = candidates
            .Select(direction => ProjectBox(targetBox, direction).Area)
            .ToList();
        var maxTargetArea = Math.Max(1.0, projectedTargetAreas.Max());

        var bestScore = double.MaxValue;
        var bestDirection = candidates[0];
        var bestBlocking = new List<string>();

        for (var i = 0; i < candidates.Count; i++)
        {
            var direction = candidates[i];
            var targetProjection = ProjectBox(targetBox, direction);
            var blockers = new List<(string Ref, double Penalty)>();
            var score = (maxTargetArea - targetProjection.Area) / maxTargetArea * (isPlanarTarget ? 0.08 : 0.35);
            var absDirection = direction.Select(Math.Abs).ToArray();
            var maxComponent = Math.Max(absDirection[0], Math.Max(absDirection[1], absDirection[2]));
            var minComponent = Math.Min(absDirection[0], Math.Min(absDirection[1], absDirection[2]));
            score += (maxComponent - minComponent) / Math.Max(maxComponent, 0.001) * (isPlanarTarget ? 0.04 : 0.25);
            score += PreferredViewSidePenalty(direction, preferredViewSide, isPlanarTarget);
            score += StableNonPlanarAzimuthPenalty(feature, targetFaces, direction, preferredViewSide, isPlanarTarget);

            foreach (var other in others)
            {
                var otherProjection = ProjectBox(other.BBoxMm, direction);
                var overlap = OverlapArea(targetProjection, otherProjection);
                if (overlap <= 0.001)
                    continue;

                var inFrontGap = otherProjection.MaxDepth - targetProjection.MinDepth;
                if (inFrontGap <= 0)
                    continue;

                var penalty = overlap / Math.Max(targetProjection.Area, 1.0) * (1.0 + Math.Min(inFrontGap, 100.0) / 200.0);
                score += penalty;
                blockers.Add((FaceRef(other), penalty));
            }

            if (score < bestScore)
            {
                bestScore = score;
                bestDirection = direction;
                bestBlocking = blockers
                    .OrderByDescending(item => item.Penalty)
                    .Take(8)
                    .Select(item => item.Ref)
                    .ToList();
            }
        }

        return (ObliqueDirectionName(bestDirection), bestDirection, bestBlocking);
    }

    static bool IsPlanarFeatureTarget(MechanicalFeature feature, IReadOnlyList<AssemblyFaceEvidence> targetFaces)
    {
        if (feature.Type.Contains("planar", StringComparison.OrdinalIgnoreCase))
            return true;

        return targetFaces.Count > 0 &&
            targetFaces.All(face => face.FaceKind.Equals("plane", StringComparison.OrdinalIgnoreCase));
    }

    static double[] DeterminePreferredViewSide(
        MechanicalFeature feature,
        IReadOnlyList<AssemblyFaceEvidence> targetFaces,
        IReadOnlyList<AssemblyFaceEvidence> allFaces,
        double[] targetBox)
    {
        var planeNormal = WeightedAverageNormal(targetFaces
            .Where(face => face.FaceKind.Equals("plane", StringComparison.OrdinalIgnoreCase))
            .Where(face => face.Normal.Length >= 3));
        if (planeNormal.Length >= 3)
            return planeNormal;

        var componentFaces = allFaces
            .Where(face => face.ComponentName.Equals(feature.ComponentName, StringComparison.OrdinalIgnoreCase))
            .Where(face => face.BBoxMm.Length >= 6)
            .ToList();
        if (componentFaces.Count == 0)
            componentFaces = allFaces.Where(face => face.BBoxMm.Length >= 6).ToList();

        var modelBox = UnionBoxes(componentFaces.Select(face => face.BBoxMm));
        var targetCenter = BoxCenter(targetBox);
        var modelCenter = BoxCenter(modelBox);
        if (targetCenter.Length < 3 || modelCenter.Length < 3 || modelBox.Length < 6)
            return [];

        var verticalOffset = targetCenter[1] - modelCenter[1];
        if (Math.Abs(verticalOffset) >= 0.001)
            return verticalOffset > 0
                ? [0.0, 1.0, 0.0]
                : [0.0, -1.0, 0.0];

        return [];
    }

    static double[] WeightedAverageNormal(IEnumerable<AssemblyFaceEvidence> faces)
    {
        var sum = new[] { 0.0, 0.0, 0.0 };
        var totalWeight = 0.0;
        foreach (var face in faces)
        {
            var normal = Vec.Normalize(face.Normal);
            if (normal.Length < 3)
                continue;

            var weight = Math.Max(face.AreaMm2, 1.0);
            sum = Vec.Add(sum, Vec.Mul(normal, weight));
            totalWeight += weight;
        }

        if (totalWeight <= 0.0 || Vec.Norm(sum) < 1e-6)
            return [];

        return Vec.Normalize(sum);
    }

    static List<double[]> EnforcePreferredViewSide(List<double[]> candidates, double[] preferredViewSide, bool preferSteeperPlanarOblique)
    {
        if (preferredViewSide.Length < 3)
            return candidates;

        var preferred = Vec.Normalize(preferredViewSide);
        var minimumAlignment = preferSteeperPlanarOblique ? 0.30 : 0.7;
        var maximumAlignment = preferSteeperPlanarOblique ? 0.78 : 1.0;
        var filtered = candidates
            .Where(direction =>
            {
                var alignment = Vec.Dot(Vec.Normalize(direction), preferred);
                return alignment >= minimumAlignment && alignment <= maximumAlignment;
            })
            .ToList();

        if (filtered.Count == 0 && preferSteeperPlanarOblique)
        {
            filtered = candidates
                .Where(direction => Vec.Dot(Vec.Normalize(direction), preferred) >= minimumAlignment)
                .ToList();
        }

        return filtered.Count > 0 ? filtered : candidates;
    }

    static double PreferredViewSidePenalty(double[] direction, double[] preferredViewSide, bool preferSteeperPlanarOblique)
    {
        if (preferredViewSide.Length < 3)
            return 0.0;

        var alignment = Vec.Dot(Vec.Normalize(direction), Vec.Normalize(preferredViewSide));
        if (preferSteeperPlanarOblique)
        {
            const double targetPlanarAlignment = 0.45;
            return Math.Abs(alignment - targetPlanarAlignment) * 0.55;
        }

        return (1.0 - alignment) * 0.55;
    }

    static double StableNonPlanarAzimuthPenalty(
        MechanicalFeature feature,
        IReadOnlyList<AssemblyFaceEvidence> targetFaces,
        double[] direction,
        double[] preferredViewSide,
        bool isPlanarTarget)
    {
        if (isPlanarTarget || preferredViewSide.Length < 3)
            return 0.0;

        var hasCurvedOrUnknownTarget = targetFaces.Any(face =>
            face.FaceKind.Equals("cylinder", StringComparison.OrdinalIgnoreCase) ||
            face.FaceKind.Equals("cone", StringComparison.OrdinalIgnoreCase) ||
            face.FaceKind.Equals("other", StringComparison.OrdinalIgnoreCase));
        if (!hasCurvedOrUnknownTarget)
            return 0.0;

        var preferred = Vec.Normalize(preferredViewSide);
        if (preferred.Length < 3 || Math.Abs(preferred[1]) < 0.5)
            return 0.0;

        // When curved/unknown faces only tell us "upper/lower", X/Z is otherwise decided
        // by small bbox-score differences. Keep a stable oblique quadrant for direct part
        // images so adjacent main-axis cylindrical features are viewed consistently.
        var stableOblique = Vec.Normalize([1.0, preferred[1] >= 0 ? 2.0 : -2.0, -1.0]);
        var alignment = Vec.Dot(Vec.Normalize(direction), stableOblique);
        var weight = feature.Type.Contains("main_axis_cylindrical", StringComparison.OrdinalIgnoreCase)
            ? 0.35
            : 0.18;

        return (1.0 - alignment) * weight;
    }

    static double[] ProjectOntoPlane(double[] vector, double[] planeNormal)
    {
        var normal = Vec.Normalize(planeNormal);
        var v = Vec.Normalize(vector);
        if (normal.Length < 3 || v.Length < 3)
            return [];

        return Vec.Normalize(Vec.Sub(v, Vec.Mul(normal, Vec.Dot(v, normal))));
    }

    static string ObliqueDirectionName(double[] direction)
    {
        static string Sign(double value, string axis) => value >= 0 ? $"{axis}p" : $"{axis}n";
        var ax = Math.Abs(direction.ElementAtOrDefault(0));
        var ay = Math.Abs(direction.ElementAtOrDefault(1));
        var az = Math.Abs(direction.ElementAtOrDefault(2));
        var min = Math.Max(0.001, Math.Min(ax, Math.Min(ay, az)));
        var wx = Math.Max(1, (int)Math.Round(ax / min));
        var wy = Math.Max(1, (int)Math.Round(ay / min));
        var wz = Math.Max(1, (int)Math.Round(az / min));
        var signName = $"oblique_{Sign(direction.ElementAtOrDefault(0), "x")}_{Sign(direction.ElementAtOrDefault(1), "y")}_{Sign(direction.ElementAtOrDefault(2), "z")}";
        return wx == 1 && wy == 1 && wz == 1
            ? signName
            : $"{signName}_x{wx}_y{wy}_z{wz}";
    }

    static List<double[]> BuildObliqueCandidateDirections(bool includeSteepPlanarObliques)
    {
        var signs = new[] { -1.0, 1.0 };
        var result = new List<double[]>();

        foreach (var sx in signs)
        foreach (var sy in signs)
        foreach (var sz in signs)
        {
            result.Add(Vec.Normalize([sx, sy, sz]));
        }

        return result;
    }

    static double[] UnionBoxes(IEnumerable<double[]> boxes)
    {
        var valid = boxes.Where(box => box.Length >= 6).ToList();
        if (valid.Count == 0)
            return [];

        return
        [
            valid.Min(box => box[0]),
            valid.Min(box => box[1]),
            valid.Min(box => box[2]),
            valid.Max(box => box[3]),
            valid.Max(box => box[4]),
            valid.Max(box => box[5])
        ];
    }

    static double[] BoxCenter(double[] box) =>
        box.Length >= 6
            ? [(box[0] + box[3]) / 2.0, (box[1] + box[4]) / 2.0, (box[2] + box[5]) / 2.0]
            : [];

    static ProjectedBox ProjectBox(double[] box, double[] viewDirection)
    {
        var d = Vec.Normalize(viewDirection);
        var up = Math.Abs(Vec.Dot(d, [0.0, 1.0, 0.0])) > 0.92 ? new[] { 0.0, 0.0, 1.0 } : new[] { 0.0, 1.0, 0.0 };
        var u = Vec.Normalize(Vec.Cross(up, d));
        var v = Vec.Normalize(Vec.Cross(d, u));

        var corners = BoxCorners(box);
        var uValues = corners.Select(p => Vec.Dot(p, u)).ToList();
        var vValues = corners.Select(p => Vec.Dot(p, v)).ToList();
        var dValues = corners.Select(p => Vec.Dot(p, d)).ToList();

        return new ProjectedBox(
            uValues.Min(),
            uValues.Max(),
            vValues.Min(),
            vValues.Max(),
            dValues.Min(),
            dValues.Max());
    }

    static List<double[]> BoxCorners(double[] box) =>
    [
        [box[0], box[1], box[2]],
        [box[0], box[1], box[5]],
        [box[0], box[4], box[2]],
        [box[0], box[4], box[5]],
        [box[3], box[1], box[2]],
        [box[3], box[1], box[5]],
        [box[3], box[4], box[2]],
        [box[3], box[4], box[5]]
    ];

    static double OverlapArea(ProjectedBox a, ProjectedBox b)
    {
        var width = Math.Max(0.0, Math.Min(a.MaxU, b.MaxU) - Math.Max(a.MinU, b.MinU));
        var height = Math.Max(0.0, Math.Min(a.MaxV, b.MaxV) - Math.Max(a.MinV, b.MinV));
        return width * height;
    }

    static string FaceRef(AssemblyFaceEvidence face) => $"{face.ComponentName}:face#{face.FaceIndex}";

    static string SanitizeId(string value)
    {
        var chars = value.Select(ch => char.IsLetterOrDigit(ch) ? ch : '_').ToArray();
        return new string(chars).Trim('_');
    }
}

sealed record ProjectedBox(double MinU, double MaxU, double MinV, double MaxV, double MinDepth, double MaxDepth)
{
    public double Area => Math.Max(0.0, MaxU - MinU) * Math.Max(0.0, MaxV - MinV);
}
