using System.ComponentModel;
using Kompas.Mcp.Core;
using Kompas.Mcp.Core.Modeling;
using Kompas.Mcp.Core.Query;
using Kompas.Mcp.Core.Validation;
using ModelContextProtocol.Server;

namespace Kompas.Mcp.Host.Tools;

/// <summary>Инструменты формообразующих операций.</summary>
[McpServerToolType]
public sealed class FeatureTools(
    KompasSession session,
    PartModeler modeler,
    HoleService holes,
    PrimitiveService primitives,
    ModelInspectionService inspection,
    AutoValidation autoValidation)
{
    [McpServerTool(Name = "primitive")]
    [Description("Построить элементарное тело БЕЗ эскиза, по размерам: kind=block (length×width×height " +
                 "по осям X/Y/Z, точка x,y,z — угол) | cylinder (diameter, height) | sphere (diameter) | " +
                 "cone (diameter основания, height); у тел вращения x,y,z — центр основания. " +
                 "result задаёт, что делать с уже построенным: new (отдельное тело) | union (приплюсовать) | " +
                 "subtract (вычесть — карман или паз без эскиза и выреза) | intersect. Для призматических " +
                 "форм это короче связки sketch_create+extrude; сложный контур (текст, кривые) по-прежнему " +
                 "делается эскизом.")]
    public async Task<string> Primitive(
        [Description("Тип тела: block | cylinder | sphere | cone")] string kind,
        [Description("X точки привязки, мм")] double x = 0,
        [Description("Y точки привязки, мм")] double y = 0,
        [Description("Z точки привязки, мм")] double z = 0,
        [Description("Длина по X, мм — для block")] double? length = null,
        [Description("Ширина по Y, мм — для block")] double? width = null,
        [Description("Высота по Z, мм — для block | cylinder | cone")] double? height = null,
        [Description("Диаметр, мм — для cylinder | sphere | cone (основания)")] double? diameter = null,
        [Description("Что сделать с существующим телом: new | union | subtract | intersect")] string result = "new",
        [Description("Прямое направление высоты (иначе вниз от точки привязки)")] bool forward = true,
        [Description("Имя операции в дереве построения, например «Плашка 82×10.5»")] string? name = null)
    {
        await session.ConnectAsync();
        var parsedKind = Primitives.ParseKind(kind);
        var parsedResult = Primitives.ParseResult(result);

        await primitives.PrimitiveAsync(parsedKind, x, y, z, length, width, height, diameter, parsedResult, forward, name);

        var what = parsedKind.ToString().ToLowerInvariant();
        var how = parsedResult switch
        {
            PrimitiveResult.Union => "объединён с телом",
            PrimitiveResult.Subtract => "вычтен из тела",
            PrimitiveResult.Intersect => "оставлено пересечение",
            _ => "создан отдельным телом",
        };
        return await autoValidation.AnnotateAsync($"Примитив {what} в ({x}, {y}, {z}) — {how}.");
    }

    [McpServerTool(Name = "extrude")]
    [Description("Выдавить эскиз: mode=boss — добавить материал, mode=cut — вырезать. depth — глубина в мм. " +
                 "Для сквозного выреза передайте throughAll=true (depth не нужен; для boss сквозной режим " +
                 "неприменим). forward — прямое направление; если операция ушла «не туда», повторите с " +
                 "forward=false. thinThickness>0 превращает контур в стенку такой толщины (тонкостенное тело); " +
                 "thinSide=outward наращивает её наружу — так из контура получается ободок/эквидистанта вокруг " +
                 "него. Возвращает id операции.")]
    public async Task<string> Extrude(
        [Description("Id эскиза (должен быть закрыт или закроется автоматически)")] int sketchId,
        [Description("Режим: boss (добавить материал) | cut (вырезать)")] string mode,
        [Description("Глубина, мм (не нужна при throughAll)")] double depth = 0,
        [Description("Вырезать насквозь через всю деталь (только для cut)")] bool throughAll = false,
        [Description("Прямое направление")] bool forward = true,
        [Description("Толщина тонкой стенки, мм; 0 — сплошное сечение по контуру")] double thinThickness = 0,
        [Description("Сторона стенки от контура: outward | inward | both")] string thinSide = "outward",
        [Description("Имя операции в дереве построения, например «Рельеф букв OldMan»")] string? name = null)
    {
        await session.ConnectAsync();
        var cut = ParseCutMode(mode);
        var side = ThinSides.Parse(thinSide);
        var id = await modeler.ExtrudeAsync(sketchId, depth, cut: cut, forward: forward, throughAll: throughAll,
            thinThickness: thinThickness, thinSide: side, name: name);
        var what = cut ? "Вырез" : "Выдавливание";
        var how = throughAll ? "насквозь" : $"на {depth} мм";
        var wall = thinThickness > 0 ? $", стенка {thinThickness} мм ({side.ToString().ToLowerInvariant()})" : "";
        return await autoValidation.AnnotateAsync($"{what} выдавливанием {how}{wall} создан(о), id={id}.");
    }

    [McpServerTool(Name = "revolve")]
    [Description("Вращать эскиз вокруг его осевой линии: mode=boss — добавить материал, mode=cut — вырезать. " +
                 "Эскиз должен содержать профиль И осевую линию (примитив type=axis в sketch_create/sketch_add). " +
                 "angle — угол в градусах (по умолчанию 360). Возвращает id операции.")]
    public async Task<string> Revolve(
        [Description("Id эскиза с профилем и осевой линией")] int sketchId,
        [Description("Режим: boss (добавить материал) | cut (вырезать)")] string mode,
        [Description("Угол вращения, градусы (0..360]")] double angle = 360,
        [Description("Прямое направление")] bool forward = true,
        [Description("Имя операции в дереве построения, например «Тело вращения корпуса»")] string? name = null)
    {
        await session.ConnectAsync();
        var cut = ParseCutMode(mode);
        var id = await modeler.RevolveAsync(sketchId, angle, cut: cut, forward: forward, name: name);
        var what = cut ? "Вырез вращением" : "Вращение";
        return await autoValidation.AnnotateAsync($"{what} на {angle}° создан(о), id={id}.");
    }

    [McpServerTool(Name = "fillet_edge")]
    [Description("Скруглить рёбра детали радиусом radius (мм) — ОДНОЙ операцией. Рёбра задаются любым из " +
                 "способов: edgeIndices — список индексов из list_edges (предпочтительно, когда список рёбер " +
                 "обозрим); points — список точек {x,y,z} на рёбрах (для тел с сотнями рёбер, где list_edges " +
                 "неподъёмен: все углы скругляются одной операцией, а не N вызовами); x,y,z — одна точка. " +
                 "Если ребра нет в части точек, ошибка перечислит ВСЕ такие точки сразу. Возвращает id операции.")]
    public async Task<string> FilletEdge(
        [Description("Радиус скругления, мм")] double radius,
        [Description("Индексы рёбер из list_edges")] int[]? edgeIndices = null,
        [Description("Точки на рёбрах: [{x,y,z}, …] — одна операция на все")] Point3dInput[]? points = null,
        [Description("X точки на ребре, мм")] double? x = null,
        [Description("Y точки на ребре, мм")] double? y = null,
        [Description("Z точки на ребре, мм")] double? z = null,
        [Description("Имя операции в дереве построения, например «Скругления углов плашки»")] string? name = null)
    {
        await session.ConnectAsync();
        if (edgeIndices is { Length: > 0 })
        {
            var id = await modeler.FilletEdgesAsync(edgeIndices, radius, name);
            var count = edgeIndices.Distinct().Count();
            return await autoValidation.AnnotateAsync($"Скругление R{radius} создано на {count} рёбрах, id={id}.");
        }

        var parsed = Point3dInputs.Parse(points);
        if (parsed.Count > 0)
        {
            var id = await modeler.FilletEdgesByPointsAsync(parsed, radius, name);
            return await autoValidation.AnnotateAsync(
                $"Скругление R{radius} создано на {parsed.Count} рёбрах (по точкам), id={id}.");
        }

        var (px, py, pz) = RequirePoint(x, y, z, "ребре", "edgeIndices/points");
        var byPoint = await modeler.FilletEdgeAsync(px, py, pz, radius, name);
        return await autoValidation.AnnotateAsync(
            $"Скругление R{radius} создано на ребре в точке ({px}, {py}, {pz}), id={byPoint}.");
    }

    [McpServerTool(Name = "chamfer_edge")]
    [Description("Снять фаску с рёбер детали катетом distance (мм) — ОДНОЙ операцией. Рёбра задаются любым " +
                 "из способов: edgeIndices — список индексов из list_edges; points — список точек {x,y,z} " +
                 "на рёбрах (одна операция на все, путь для тел с сотнями рёбер); x,y,z — одна точка. " +
                 "Возвращает id операции.")]
    public async Task<string> ChamferEdge(
        [Description("Катет фаски, мм")] double distance,
        [Description("Индексы рёбер из list_edges")] int[]? edgeIndices = null,
        [Description("Точки на рёбрах: [{x,y,z}, …] — одна операция на все")] Point3dInput[]? points = null,
        [Description("X точки на ребре, мм")] double? x = null,
        [Description("Y точки на ребре, мм")] double? y = null,
        [Description("Z точки на ребре, мм")] double? z = null,
        [Description("Имя операции в дереве построения, например «Фаска против слоновьей ноги»")] string? name = null)
    {
        await session.ConnectAsync();
        if (edgeIndices is { Length: > 0 })
        {
            var id = await modeler.ChamferEdgesAsync(edgeIndices, distance, name);
            var count = edgeIndices.Distinct().Count();
            return await autoValidation.AnnotateAsync($"Фаска {distance}×{distance} создана на {count} рёбрах, id={id}.");
        }

        var parsed = Point3dInputs.Parse(points);
        if (parsed.Count > 0)
        {
            var id = await modeler.ChamferEdgesByPointsAsync(parsed, distance, name);
            return await autoValidation.AnnotateAsync(
                $"Фаска {distance}×{distance} создана на {parsed.Count} рёбрах (по точкам), id={id}.");
        }

        var (px, py, pz) = RequirePoint(x, y, z, "ребре", "edgeIndices/points");
        var byPoint = await modeler.ChamferEdgeAsync(px, py, pz, distance, name);
        return await autoValidation.AnnotateAsync(
            $"Фаска {distance}×{distance} создана на ребре в точке ({px}, {py}, {pz}), id={byPoint}.");
    }

    [McpServerTool(Name = "shell")]
    [Description("Превратить тело активной детали в оболочку (придать стенкам толщину): удалить (открыть) грани с индексами faceIndices из list_faces и задать толщину стенки thickness (мм). outward=false — толщина внутрь (габарит сохраняется), true — наружу. Возвращает id операции.")]
    public async Task<string> Shell(
        [Description("Индексы удаляемых (открываемых) граней из list_faces")] int[] faceIndices,
        [Description("Толщина стенки, мм")] double thickness,
        [Description("Толщина наружу (true) или внутрь (false)")] bool outward = false,
        [Description("Имя операции в дереве построения, например «Оболочка корпуса 1.6»")] string? name = null)
    {
        await session.ConnectAsync();
        var id = await modeler.ShellAsync(faceIndices, thickness, outward, name);
        var removed = faceIndices?.Distinct().Count() ?? 0;
        return await autoValidation.AnnotateAsync($"Оболочка толщиной {thickness} мм создана (удалено граней: {removed}), id={id}.");
    }

    [McpServerTool(Name = "rib")]
    [Description("Построить ребро жёсткости от разомкнутого контура эскиза до тела. sketchId — эскиз с разомкнутым контуром (отрезок/ломаная) на плоскости, пересекающей тело. thickness — толщина стенки ребра (мм). side — направление достройки полотна до тела (ksRibSideEnum): left|right|up|down (если ребро не строится — перебрать значения). symmetric=true — толщина симметрична плоскости эскиза, false — в одну сторону. angle — уклон стенок (градусы). Возвращает id операции.")]
    public async Task<string> Rib(int sketchId, double thickness, string side = "up", bool symmetric = true, double angle = 0,
        [Description("Имя операции в дереве построения, например «Ребро жёсткости левое»")] string? name = null)
    {
        await session.ConnectAsync();
        var id = await modeler.RibAsync(sketchId, thickness, side, symmetric, angle, name);
        return await autoValidation.AnnotateAsync($"Ребро жёсткости толщиной {thickness} мм создано (side={side}), id={id}.");
    }

    [McpServerTool(Name = "sweep")]
    [Description("Кинематическая операция: переместить замкнутый профиль (эскиз profileSketchId) вдоль траектории (эскиз pathSketchId), образуя сплошное тело. Профиль и траектория должны быть совместимы: на разных плоскостях (обычно перпендикулярных), начало траектории — на плоскости профиля; иначе операция вернёт ошибку построения. Возвращает id операции.")]
    public async Task<string> Sweep(int profileSketchId, int pathSketchId,
        [Description("Имя операции в дереве построения, например «Труба по траектории»")] string? name = null)
    {
        await session.ConnectAsync();
        var id = await modeler.SweepAsync(profileSketchId, pathSketchId, name);
        return await autoValidation.AnnotateAsync(
            $"Кинематическая операция создана (профиль {profileSketchId} по траектории {pathSketchId}), id={id}.");
    }

    [McpServerTool(Name = "loft")]
    [Description("Операция по сечениям (loft): построить сплошное тело по нескольким закрытым эскизам-сечениям (sketchIds, минимум 2, в порядке списка). Сечения обычно на параллельных плоскостях — задавайте их через sketch_create с plane + offset. closed=true — замкнуть сечения в кольцо. Возвращает id операции.")]
    public async Task<string> Loft(
        [Description("Id эскизов-сечений (минимум 2, в порядке построения)")] int[] sketchIds,
        [Description("Замкнуть сечения в кольцо")] bool closed = false,
        [Description("Имя операции в дереве построения, например «Переход по сечениям»")] string? name = null)
    {
        await session.ConnectAsync();
        var id = await modeler.LoftAsync(sketchIds, closed, name);
        var sections = sketchIds?.Distinct().Count() ?? 0;
        return await autoValidation.AnnotateAsync($"Операция по сечениям создана ({sections} сечений), id={id}.");
    }

    [McpServerTool(Name = "pattern")]
    [Description("Массив операций: размножить ранее созданные операции featureIds (их id из extrude/revolve/" +
                 "fillet_edge/hole/…). kind=linear — вдоль координатной оси axis с шагом step в мм; " +
                 "kind=circular — вокруг оси axis с угловым шагом step в градусах (например 90 для 4 шт). " +
                 "count — количество экземпляров ВСЕГО, включая исходный (>=2). reverse — обратное направление " +
                 "вращения (только circular). geometric=true — быстрое геометрическое копирование (может не " +
                 "строиться на скруглениях/фасках кривых поверхностей). Возвращает id операции.")]
    public async Task<string> Pattern(
        [Description("Тип массива: linear | circular")] string kind,
        [Description("Id операций-источников")] int[] featureIds,
        [Description("Координатная ось (направления или вращения): X|Y|Z")] string axis,
        [Description("Число экземпляров всего, включая исходный (>=2)")] int count,
        [Description("Шаг между соседними: мм для linear, градусы для circular")] double step,
        [Description("Обратное направление вращения (только circular)")] bool reverse = false,
        [Description("Геометрическое копирование (быстрее)")] bool geometric = false,
        [Description("Имя операции в дереве построения, например «Массив отверстий крепления»")] string? name = null)
    {
        await session.ConnectAsync();
        var circular = ParsePatternKind(kind);
        var parsedAxis = CoordinateAxes.Parse(axis);

        if (circular)
        {
            var id = await modeler.CircularPatternAsync(featureIds, parsedAxis, count, step, reverse, geometric, name);
            return await autoValidation.AnnotateAsync($"Круговой массив создан ({count} экз. вокруг {axis}, шаг {step}°), id={id}.");
        }

        var linearId = await modeler.LinearPatternAsync(featureIds, parsedAxis, count, step, geometric, name);
        return await autoValidation.AnnotateAsync($"Линейный массив создан ({count} экз. вдоль {axis}, шаг {step} мм), id={linearId}.");
    }

    [McpServerTool(Name = "mirror")]
    [Description("Зеркальное отражение относительно координатной плоскости plane (XOY|XOZ|YOZ). " +
                 "Без featureIds отражается ВСЁ тело детали (оригинал + зеркало — удобно строить половину и " +
                 "достраивать симметрию). С featureIds отражаются только указанные операции (их оригиналы " +
                 "сохраняются). Возвращает id операции.")]
    public async Task<string> Mirror(
        [Description("Плоскость симметрии: XOY|XOZ|YOZ")] string plane,
        [Description("Id отражаемых операций; пусто — отразить всё тело")] int[]? featureIds = null,
        [Description("Имя операции в дереве построения, например «Зеркало кронштейна»")] string? name = null)
    {
        await session.ConnectAsync();
        var basePlane = BasePlanes.Parse(plane);

        if (featureIds is { Length: > 0 })
        {
            var id = await modeler.MirrorOperationAsync(featureIds, basePlane, name);
            return await autoValidation.AnnotateAsync($"Зеркальная копия операций создана (плоскость {plane}), id={id}.");
        }

        var bodyId = await modeler.MirrorBodyAsync(basePlane, name);
        return await autoValidation.AnnotateAsync($"Тело зеркально отражено (плоскость {plane}), id={bodyId}.");
    }

    [McpServerTool(Name = "hole")]
    [Description("Просверлить параметрическое отверстие (API7) — в отличие от extrude cut, без эскиза. " +
                 "Грань и центр отверстия задаются ЛИБО faceIndex из list_faces (сверлится в центре грани), " +
                 "ЛИБО точкой x,y,z на грани в мировых координатах модели (точка = центр отверстия). " +
                 "type: simple (простое цилиндрическое) | counterbore (с ЦЕКОВКОЙ — цилиндрическое уширение под " +
                 "винт с цилиндрической головкой, нужны spotfaceDiameter > diameter и spotfaceDepth) | " +
                 "countersink (с ЗЕНКОВКОЙ — конической, под потайную головку, нужны sinkDiameter > diameter и " +
                 "sinkAngle в (0;180), типично 90) | conic (коническое, нужен conicAngle в (0;180); diameter — " +
                 "диаметр у дна). throughAll=true — сквозное (depth не нужен), иначе глухое на глубину depth " +
                 "(для counterbore depth должен быть больше spotfaceDepth). Направление в тело подбирается " +
                 "автоматически по факту убыли материала.")]
    public async Task<string> Hole(
        [Description("Диаметр отверстия, мм (для conic — у дна)")] double diameter,
        [Description("Тип: simple | counterbore | countersink | conic")] string type = "simple",
        [Description("Индекс грани из list_faces — отверстие в центре грани")] int? faceIndex = null,
        [Description("X точки-центра на грани, мм")] double? x = null,
        [Description("Y точки-центра на грани, мм")] double? y = null,
        [Description("Z точки-центра на грани, мм")] double? z = null,
        [Description("Глубина, мм (если throughAll=false)")] double depth = 0,
        [Description("Сквозное отверстие через всё тело")] bool throughAll = false,
        [Description("Диаметр цековки, мм (> diameter) — для counterbore")] double? spotfaceDiameter = null,
        [Description("Глубина цековки, мм — для counterbore")] double? spotfaceDepth = null,
        [Description("Диаметр зенковки, мм (> diameter) — для countersink")] double? sinkDiameter = null,
        [Description("Угол конуса зенковки, градусы (0..180, типично 90) — для countersink")] double? sinkAngle = null,
        [Description("Угол конуса, градусы (0..180) — для conic")] double? conicAngle = null,
        [Description("Имя операции в дереве построения, например «Отверстие под M4»")] string? name = null)
    {
        await session.ConnectAsync();
        var kind = (type ?? "simple").Trim().ToLowerInvariant();
        var (px, py, pz) = await ResolveFacePointAsync(faceIndex, x, y, z);
        var where = faceIndex is { } fi ? $"в центре грани [{fi}]" : $"в точке ({px}, {py}, {pz})";
        var how = throughAll ? "сквозное" : $"глухое на {depth} мм";

        switch (kind)
        {
            case "simple":
                await holes.HoleAsync(px, py, pz, diameter, depth, throughAll, name);
                return await autoValidation.AnnotateAsync($"Отверстие Ø{diameter} ({how}) создано {where}.");

            case "counterbore":
                var spotD = Required(spotfaceDiameter, "spotfaceDiameter", kind);
                var spotH = Required(spotfaceDepth, "spotfaceDepth", kind);
                await holes.CounterboreHoleAsync(px, py, pz, diameter, depth, throughAll, spotD, spotH, name);
                return await autoValidation.AnnotateAsync(
                    $"Отверстие с цековкой Ø{diameter} (цековка Ø{spotD}×{spotH}, {how}) создано {where}.");

            case "countersink":
                var sinkD = Required(sinkDiameter, "sinkDiameter", kind);
                var sinkA = Required(sinkAngle, "sinkAngle", kind);
                await holes.CountersinkHoleAsync(px, py, pz, diameter, depth, throughAll, sinkD, sinkA, name);
                return await autoValidation.AnnotateAsync(
                    $"Отверстие с зенковкой Ø{diameter} (зенковка Ø{sinkD}, угол {sinkA}°, {how}) создано {where}.");

            case "conic":
                var conicA = Required(conicAngle, "conicAngle", kind);
                await holes.ConicHoleAsync(px, py, pz, diameter, depth, throughAll, conicA, name);
                return await autoValidation.AnnotateAsync(
                    $"Коническое отверстие Ø{diameter} (угол {conicA}°, {how}) создано {where}.");

            default:
                throw new ArgumentException(
                    $"Неизвестный тип отверстия '{type}'. Допустимо: simple | counterbore | countersink | conic.");
        }
    }

    [McpServerTool(Name = "draft")]
    [Description("Уклон граней: наклонить грани faceIndices (из list_faces) на угол angle (градусы, строго 0 < angle < 90) относительно нейтральной (опорной) координатной плоскости neutralPlane (XOY|XOZ|YOZ) — сечение тела в этой плоскости остаётся неизменным, грани уклоняются вокруг линии пересечения с ней. outward=true — расширение (материал добавляется), false — сужение. Грани должны граничить/пересекаться с нейтральной плоскостью. Возвращает id операции.")]
    public async Task<string> Draft(
        [Description("Индексы уклоняемых граней из list_faces")] int[] faceIndices,
        [Description("Нейтральная (опорная) плоскость: XOY|XOZ|YOZ")] string neutralPlane,
        [Description("Угол уклона, градусы (0 < angle < 90)")] double angle,
        [Description("Расширение (true) или сужение (false)")] bool outward = false,
        [Description("Имя операции в дереве построения, например «Уклон стенок под съём»")] string? name = null)
    {
        await session.ConnectAsync();
        var id = await modeler.DraftAsync(faceIndices, BasePlanes.Parse(neutralPlane), angle, outward, name);
        var count = faceIndices?.Distinct().Count() ?? 0;
        return await autoValidation.AnnotateAsync(
            $"Уклон {angle}° создан на {count} гранях (нейтральная плоскость {neutralPlane}), id={id}.");
    }

    [McpServerTool(Name = "rebuild")]
    [Description("Перестроить активный документ. Нужен перед чтением МЦХ после правок переменных/геометрии.")]
    public async Task<string> Rebuild()
    {
        await session.ConnectAsync();
        await modeler.RebuildAsync();
        return await autoValidation.AnnotateAsync("Документ перестроен.");
    }

    /// <summary>Разобрать режим boss|cut для выдавливания/вращения.</summary>
    private static bool ParseCutMode(string? mode) => (mode ?? "").Trim().ToLowerInvariant() switch
    {
        "boss" or "add" => false,
        "cut" => true,
        _ => throw new ArgumentException($"Неизвестный режим '{mode}'. Допустимо: boss (добавить материал) | cut (вырезать)."),
    };

    /// <summary>Разобрать тип массива: true — круговой, false — линейный.</summary>
    private static bool ParsePatternKind(string? kind) => (kind ?? "").Trim().ToLowerInvariant() switch
    {
        "linear" => false,
        "circular" => true,
        _ => throw new ArgumentException($"Неизвестный тип массива '{kind}'. Допустимо: linear | circular."),
    };

    /// <summary>Точка выбора объекта: либо все три координаты, либо понятная ошибка с альтернативой.</summary>
    private static (double X, double Y, double Z) RequirePoint(double? x, double? y, double? z, string on, string indexParam)
    {
        if (x is { } px && y is { } py && z is { } pz) return (px, py, pz);
        throw new ArgumentException(
            $"Не задан объект: передайте {indexParam} (надёжнее) либо все три координаты точки на {on}: x, y, z.");
    }

    /// <summary>Грань по индексу превращаем в точку на ней (центр параметрической области):
    /// операции API7 выбирают грань только по мировой точке, а индекс из list_faces надёжнее.</summary>
    private async Task<(double X, double Y, double Z)> ResolveFacePointAsync(int? faceIndex, double? x, double? y, double? z)
    {
        if (faceIndex is { } fi) return await inspection.FaceCenterPointAsync(fi);
        return RequirePoint(x, y, z, "грани", "faceIndex");
    }

    private static double Required(double? value, string name, string kind)
        => value ?? throw new ArgumentException($"type={kind} требует параметр {name}.");
}
