using System.Runtime.Versioning;
using Kompas.Mcp.Core.Threading;
using Kompas6Constants;
using KompasAPI7;

namespace Kompas.Mcp.Core.Drawings;

/// <summary>
/// Операции с 2D-чертежами через API7 (<c>IViews</c>). Все вызовы — на STA-потоке
/// <see cref="KompasDispatcher"/>.
/// </summary>
[SupportedOSPlatform("windows")]
public sealed class DrawingService
{
    private readonly KompasSession _session;
    private readonly KompasDispatcher _dispatcher;

    public DrawingService(KompasSession session, KompasDispatcher dispatcher)
    {
        _session = session ?? throw new ArgumentNullException(nameof(session));
        _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
    }

    /// <summary>
    /// Создать на активном чертеже стандартные ассоциативные виды (спереди/сверху/слева) сохранённой
    /// 3D-модели <paramref name="partFilePath"/> (.m3d/.a3d). Главный вид привязывается к точке
    /// (<paramref name="x"/>,<paramref name="y"/>) листа, мм; масштаб — <paramref name="scale"/>.
    /// </summary>
    public Task<DrawingViewsResult> CreateStandardViewsAsync(string partFilePath, double scale,
        double x, double y, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => CreateStandardViewsCore(partFilePath, scale, x, y), ct);

    /// <summary>
    /// Заполнить графы основной надписи активного чертежа: обозначение (графа 2), наименование
    /// изделия (графа 1), материал (графа 3). Передаются только нужные поля (null/пусто —
    /// пропускается). Возвращает число заполненных граф.
    /// </summary>
    public Task<int> FillTitleBlockAsync(string? designation = null, string? name = null,
        string? material = null, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => FillTitleBlockCore(designation, name, material), ct);

    private int FillTitleBlockCore(string? designation, string? name, string? material)
    {
        var fields = StampFields.Collect(designation, name, material);
        if (fields.Count == 0)
            throw new ArgumentException("Укажите хотя бы одно поле основной надписи (designation/name/material).");

        var stamp = RequireActiveStamp();
        // Нетранзакционно: все графы записываются, затем один Update() фиксирует. При Update()==FALSE
        // (на практике не воспроизводится) откат старых значений не делаем — он спекулятивен (Str уже
        // на in-memory объектах графы, повторный Update мог бы тоже упасть); сообщаем об ошибке.
        foreach (var (columnId, text) in fields)
            RequireStampCell(stamp, columnId).Str = text; // Str замещает содержимое (Clear не нужен)
        if (!stamp.Update())
            throw new InvalidOperationException("IStamp.Update вернул FALSE — основная надпись не обновлена.");
        return fields.Count;
    }

    /// <summary>Прочитать текст графы основной надписи (для проверки/диагностики).</summary>
    public Task<string> ReadStampCellAsync(int columnId, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            var stamp = RequireActiveStamp();
            return RequireStampCell(stamp, columnId).Str ?? string.Empty;
        }, ct);

    /// <summary>Получить графу основной надписи по номеру с диагностикой (номер &gt; 0, ячейка != null).</summary>
    private static IText RequireStampCell(IStamp stamp, int columnId)
    {
        if (columnId <= 0)
            throw new ArgumentOutOfRangeException(nameof(columnId), "Номер графы основной надписи должен быть > 0.");
        return stamp.Text[columnId]
            ?? throw new InvalidOperationException($"Графа {columnId} основной надписи недоступна (Text[{columnId}] == null).");
    }

    private IStamp RequireActiveStamp()
    {
        var doc2d = RequireActiveDrawing();
        var sheets = doc2d.LayoutSheets
            ?? throw new InvalidOperationException("Не удалось получить листы оформления чертежа.");
        var sheet = sheets.ItemByNumber[1] as ILayoutSheet
            ?? throw new InvalidOperationException("На чертеже нет листа оформления #1.");
        return sheet.Stamp as IStamp
            ?? throw new InvalidOperationException("У листа оформления нет основной надписи (Stamp).");
    }

    /// <summary>Лист оформления чертежа по номеру (1-based) с диагностикой (номер &gt; 0, лист существует).</summary>
    private ILayoutSheet RequireLayoutSheet(int sheetNumber)
    {
        if (sheetNumber <= 0)
            throw new ArgumentOutOfRangeException(nameof(sheetNumber), "Номер листа должен быть > 0.");
        var doc2d = RequireActiveDrawing();
        var sheets = doc2d.LayoutSheets
            ?? throw new InvalidOperationException("Не удалось получить листы оформления чертежа.");
        return sheets.ItemByNumber[sheetNumber] as ILayoutSheet
            ?? throw new InvalidOperationException($"На чертеже нет листа оформления #{sheetNumber}.");
    }

    /// <summary>
    /// Задать формат (<paramref name="format"/>: A0–A5 или user) и ориентацию листа
    /// <paramref name="sheetNumber"/> активного чертежа. Для стандартного формата размеры авто
    /// (<paramref name="width"/>/<paramref name="height"/> запрещены); для user — заданные width/height (мм),
    /// при этом <paramref name="landscape"/> игнорируется (ориентация из соотношения сторон). Возвращает
    /// итоговый формат, размеры и ориентацию (read-back из COM).
    /// </summary>
    public Task<SheetFormatResult> SetSheetFormatAsync(PaperFormat format, bool landscape, double width,
        double height, int sheetNumber = 1, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => SetSheetFormatCore(format, landscape, width, height, sheetNumber), ct);

    private SheetFormatResult SetSheetFormatCore(PaperFormat format, bool landscape, double width,
        double height, int sheetNumber)
    {
        DrawingValidation.ValidateFormatDimensions(format, width, height);

        var sheet = RequireLayoutSheet(sheetNumber);
        var fmt = sheet.Format
            ?? throw new InvalidOperationException("У листа недоступен формат (Format == null).");
        fmt.Format = PaperFormats.ToKompas(format);
        if (format == PaperFormat.User)
        {
            // Для пользовательского КОМПАС САМ выводит VerticalOrientation из соотношения width/height
            // (W>H → альбомная, H>W → книжная) и игнорирует заданный флаг, W/H не свопает — проверено
            // спайком. Поэтому VerticalOrientation не трогаем; read-back ниже даёт верную ориентацию.
            fmt.FormatWidth = width;
            fmt.FormatHeight = height;
        }
        else
        {
            fmt.VerticalOrientation = !landscape; // стандартный: размеры авто по формату+ориентации
        }
        // FormatMultiplicity оставляем на COM-дефолте (1).
        if (!sheet.Update())
            throw new InvalidOperationException("ILayoutSheet.Update вернул FALSE — формат листа не задан.");

        var fmt2 = sheet.Format
            ?? throw new InvalidOperationException("Формат листа недоступен после Update (Format == null).");
        return new SheetFormatResult
        {
            Format = PaperFormats.FromKompas(fmt2.Format).ToString(),
            Width = fmt2.FormatWidth,
            Height = fmt2.FormatHeight,
            Landscape = !fmt2.VerticalOrientation,
        };
    }

    /// <summary>
    /// Поставить линейный размер на виде активного чертежа между точками (x1,y1)-(x2,y2) в ЛОКАЛЬНОЙ
    /// СК вида (мм); размерная линия проходит через (x3,y3). <paramref name="viewNumber"/> — номер вида
    /// (≤0 = первый несистемный/главный). Значение измеряется автоматически. Возвращает значение и номер вида.
    /// </summary>
    public Task<DrawingDimensionResult> AddLinearDimensionAsync(int viewNumber, double x1, double y1,
        double x2, double y2, double x3, double y3, DimensionOrientation orientation, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
            AddLinearDimensionCore(viewNumber, x1, y1, x2, y2, x3, y3, orientation), ct);

    private DrawingDimensionResult AddLinearDimensionCore(int viewNumber, double x1, double y1,
        double x2, double y2, double x3, double y3, DimensionOrientation orientation)
    {
        DrawingValidation.RequireFiniteCoords(x1, y1, x2, y2, x3, y3);
        DrawingValidation.RequireDistinctPoints(x1, y1, x2, y2);

        var (view, symbols) = RequireSymbols2DContainer(viewNumber);
        var dim = symbols.LineDimensions?.Add()
            ?? throw new InvalidOperationException("LineDimensions.Add вернул null.");
        try
        {
            dim.X1 = x1; dim.Y1 = y1; dim.X2 = x2; dim.Y2 = y2; dim.X3 = x3; dim.Y3 = y3;
            dim.Orientation = DimensionOrientations.ToKompas(orientation);
            ((IDimensionText)dim).AutoNominalValue = true; // авто-измерение значения (по умолчанию, явно)

            if (!dim.Update())
                throw new InvalidOperationException("ILineDimension.Update вернул FALSE — размер не построен.");
            if (!dim.Valid)
                throw new InvalidOperationException("Размер построен невалидным (проверьте точки/ориентацию).");

            // NominalValue читаем ТОЛЬКО после успешного Update и Valid.
            var value = ((IDimensionText)dim).NominalValue;
            // Нулевое значение — ориентация несовместима с точками (напр. horizontal для точек на одной
            // вертикали: |X2-X1|=0). RequireDistinctPoints это не ловит — точки различны по Y.
            if (Math.Abs(value) < 1e-6)
                throw new InvalidOperationException(
                    "Размер измерил нулевое значение — выбранная ориентация несовместима с точками " +
                    "(например, horizontal для точек на одной вертикали; используйте vertical/parallel).");
            return new DrawingDimensionResult { Value = value, ViewNumber = view.Number };
        }
        catch
        {
            try { dim.Delete(); } catch { /* лучшее усилие */ }
            throw;
        }
    }

    /// <summary>Число линейных размеров на виде по номеру (для проверки/диагностики). Размеры живут
    /// в ISymbols2DContainer вида, а НЕ в IView.ObjectCount (тот считает геометрию).</summary>
    public Task<int> GetViewLineDimensionCountAsync(int viewNumber, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.LineDimensions?.Count ?? 0, ct);

    /// <summary>
    /// Поставить диаметральный размер (Ø) окружности на виде. <paramref name="angleDeg"/> — направление
    /// выноски (градусы). При <paramref name="associate"/>=false — свободный размер по (xc,yc,radius),
    /// значение = 2·radius. При <paramref name="associate"/>=true — (xc,yc,radius) служат ключом поиска
    /// спроецированной окружности (Circles), размер привязывается к ней (BaseObject), значение Ø читается
    /// из геометрии. Координаты — ЛОКАЛЬНАЯ СК вида (мм). Возвращает диаметр и номер вида.
    /// </summary>
    public Task<DrawingDimensionResult> AddDiametralDimensionAsync(int viewNumber, double xc, double yc,
        double radius, double angleDeg, bool associate = false, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => AddDiametralDimensionCore(viewNumber, xc, yc, radius, angleDeg, associate), ct);

    private DrawingDimensionResult AddDiametralDimensionCore(int viewNumber, double xc, double yc,
        double radius, double angleDeg, bool associate)
    {
        DrawingValidation.RequireFiniteCoords(xc, yc);
        DrawingValidation.RequirePositiveRadius(radius);
        var angleRad = DimensionAngles.ToRadians(angleDeg);

        // Свободный путь берёт только ISymbols2DContainer (как раньше); IDrawingContainer (геометрия)
        // нужен лишь для ассоциативной привязки — не расширяем поверхность отказа свободного пути.
        IView view; ISymbols2DContainer symbols; IDrawingObject? baseObject = null;
        if (associate)
        {
            var c = RequireViewContainers(viewNumber);
            view = c.View; symbols = c.Symbols;
            baseObject = FindBaseCircle(c.Drawing, xc, yc, radius);
        }
        else
        {
            var c = RequireSymbols2DContainer(viewNumber);
            view = c.View; symbols = c.Symbols;
        }
        var dim = symbols.DiametralDimensions?.Add()
            ?? throw new InvalidOperationException("DiametralDimensions.Add вернул null.");
        try
        {
            if (baseObject != null)
                dim.BaseObject = baseObject;          // ассоциативно: значение Ø читается из геометрии
            else
            { dim.Xc = xc; dim.Yc = yc; dim.Radius = radius; }
            dim.Angle = angleRad;
            // DimensionType (стиль размерной линии) не задаём — оставляем COM-дефолт (полярность bool
            // не подтверждена; дефолт даёт валидный размер с верным значением — проверено спайком).
            ((IDimensionText)dim).AutoNominalValue = true;

            if (!dim.Update())
                throw new InvalidOperationException("IDiametralDimension.Update вернул FALSE — размер не построен.");
            if (!dim.Valid)
                throw new InvalidOperationException("Диаметральный размер построен невалидным.");

            var value = ((IDimensionText)dim).NominalValue; // диаметр = 2·radius
            if (value <= 0 || !double.IsFinite(value)) // инвариант диаметра: строго положительный
                throw new InvalidOperationException($"Диаметральный размер измерил некорректное значение ({value}).");
            return new DrawingDimensionResult { Value = value, ViewNumber = view.Number };
        }
        catch
        {
            try { dim.Delete(); } catch { /* лучшее усилие */ }
            throw;
        }
    }

    /// <summary>Число диаметральных размеров на виде (для проверки попадания в вид — они в
    /// ISymbols2DContainer, а не в IView.ObjectCount).</summary>
    public Task<int> GetViewDiametralDimensionCountAsync(int viewNumber, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.DiametralDimensions?.Count ?? 0, ct);

    /// <summary>
    /// Поставить радиальный размер (R) окружности на виде. <paramref name="angleDeg"/> — направление
    /// выноски (градусы). При <paramref name="associate"/>=false — свободный размер по (xc,yc,radius),
    /// значение = radius. При <paramref name="associate"/>=true — (xc,yc,radius) служат ключом поиска
    /// спроецированной окружности (Circles), размер привязывается к ней (BaseObject), значение R читается
    /// из геометрии. Координаты — ЛОКАЛЬНАЯ СК вида (мм). Возвращает радиус и номер вида.
    /// </summary>
    public Task<DrawingDimensionResult> AddRadialDimensionAsync(int viewNumber, double xc, double yc,
        double radius, double angleDeg, bool associate = false, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => AddRadialDimensionCore(viewNumber, xc, yc, radius, angleDeg, associate), ct);

    private DrawingDimensionResult AddRadialDimensionCore(int viewNumber, double xc, double yc,
        double radius, double angleDeg, bool associate)
    {
        DrawingValidation.RequireFiniteCoords(xc, yc);
        DrawingValidation.RequirePositiveRadius(radius);
        var angleRad = DimensionAngles.ToRadians(angleDeg);

        // Свободный путь берёт только ISymbols2DContainer (как раньше); IDrawingContainer (геометрия)
        // нужен лишь для ассоциативной привязки — не расширяем поверхность отказа свободного пути.
        IView view; ISymbols2DContainer symbols; IDrawingObject? baseObject = null;
        if (associate)
        {
            var c = RequireViewContainers(viewNumber);
            view = c.View; symbols = c.Symbols;
            baseObject = FindBaseCircle(c.Drawing, xc, yc, radius);
        }
        else
        {
            var c = RequireSymbols2DContainer(viewNumber);
            view = c.View; symbols = c.Symbols;
        }
        var dim = symbols.RadialDimensions?.Add()
            ?? throw new InvalidOperationException("RadialDimensions.Add вернул null.");
        try
        {
            if (baseObject != null)
                dim.BaseObject = baseObject;          // ассоциативно: значение R читается из геометрии
            else
            { dim.Xc = xc; dim.Yc = yc; dim.Radius = radius; }
            dim.Angle = angleRad;
            dim.DimensionType = true; // стиль R «от центра» (на величину не влияет — спайк)
            ((IDimensionText)dim).AutoNominalValue = true;

            if (!dim.Update())
                throw new InvalidOperationException("IRadialDimension.Update вернул FALSE — размер не построен.");
            if (!dim.Valid)
                throw new InvalidOperationException("Радиальный размер построен невалидным.");

            var value = ((IDimensionText)dim).NominalValue; // = радиус (не диаметр — спайк)
            if (value <= 0 || !double.IsFinite(value)) // инвариант радиуса: строго положительный
                throw new InvalidOperationException($"Радиальный размер измерил некорректное значение ({value}).");
            return new DrawingDimensionResult { Value = value, ViewNumber = view.Number };
        }
        catch
        {
            try { dim.Delete(); } catch { /* лучшее усилие */ }
            throw;
        }
    }

    /// <summary>Число радиальных размеров на виде (для проверки попадания в вид — они в
    /// ISymbols2DContainer, а не в IView.ObjectCount).</summary>
    public Task<int> GetViewRadialDimensionCountAsync(int viewNumber, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.RadialDimensions?.Count ?? 0, ct);

    /// <summary>
    /// Поставить угловой размер между лучами (xc,yc)→(x1,y1) и (xc,yc)→(x2,y2) (ЛОКАЛЬНАЯ СК вида, мм)
    /// на виде. (x3,y3) — положение размерной дуги (задаёт её радиус = расстояние от вершины).
    /// <paramref name="angleType"/> выбирает, какой из двух углов измерять (min/max/more) — например
    /// 90° или 270° для перпендикуляра. Значение (градусы) измеряется автоматически. Возвращает угол (°) и номер вида.
    /// </summary>
    public Task<DrawingDimensionResult> AddAngularDimensionAsync(int viewNumber, double xc, double yc,
        double x1, double y1, double x2, double y2, double x3, double y3,
        AngleDimensionType angleType, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
            AddAngularDimensionCore(viewNumber, xc, yc, x1, y1, x2, y2, x3, y3, angleType), ct);

    private DrawingDimensionResult AddAngularDimensionCore(int viewNumber, double xc, double yc,
        double x1, double y1, double x2, double y2, double x3, double y3, AngleDimensionType angleType)
    {
        DrawingValidation.RequireFiniteCoords(xc, yc, x1, y1, x2, y2, x3, y3);
        DrawingValidation.RequireDistinctPoints(xc, yc, x1, y1); // вершина ≠ точка стороны 1
        DrawingValidation.RequireDistinctPoints(xc, yc, x2, y2); // вершина ≠ точка стороны 2
        DrawingValidation.RequireDistinctPoints(xc, yc, x3, y3); // вершина ≠ положение дуги (R дуги > 0)
        DrawingValidation.RequireDistinctPoints(x1, y1, x2, y2); // стороны не совпадают (одинаковые лучи)

        var (view, symbols) = RequireSymbols2DContainer(viewNumber);
        var dim = symbols.AngleDimensions?.Add(DrawingObjectTypeEnum.ksDrADimension)
            ?? throw new InvalidOperationException("AngleDimensions.Add вернул null.");
        try
        {
            dim.Xc = xc; dim.Yc = yc;
            dim.X1 = x1; dim.Y1 = y1; dim.X2 = x2; dim.Y2 = y2; dim.X3 = x3; dim.Y3 = y3;
            dim.DimensionType = AngleDimensionTypes.ToKompas(angleType);
            ((IDimensionText)dim).AutoNominalValue = true;

            if (!dim.Update())
                throw new InvalidOperationException("IAngleDimension.Update вернул FALSE — размер не построен.");
            if (!dim.Valid)
                throw new InvalidOperationException("Угловой размер построен невалидным.");

            var value = ((IDimensionText)dim).NominalValue; // градусы
            // Нулевой/некорректный угол — стороны коллинеарны (лучи сонаправлены → угол вырожден).
            if (value <= 0 || !double.IsFinite(value))
                throw new InvalidOperationException(
                    $"Угловой размер измерил некорректное значение ({value}°) — стороны коллинеарны " +
                    "(лучи (xc,yc)→(x1,y1) и (xc,yc)→(x2,y2) сонаправлены).");
            return new DrawingDimensionResult { Value = value, ViewNumber = view.Number };
        }
        catch
        {
            try { dim.Delete(); } catch { /* лучшее усилие */ }
            throw;
        }
    }

    /// <summary>Число угловых размеров на виде (для проверки попадания в вид — они в
    /// ISymbols2DContainer, а не в IView.ObjectCount).</summary>
    public Task<int> GetViewAngularDimensionCountAsync(int viewNumber, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.AngleDimensions?.Count ?? 0, ct);

    // ───────────────────────── Текстовые обозначения (инкремент 6) ─────────────────────────

    /// <summary>
    /// Поставить знак шероховатости на виде в точке (<paramref name="x"/>,<paramref name="y"/>)
    /// (ЛОКАЛЬНАЯ СК вида, мм). <paramref name="value"/> — текст параметра (Ra/Rz; пусто = знак без
    /// значения). <paramref name="signType"/> — тип знака. <paramref name="angleDeg"/> — угол наклона
    /// оси знака (°). Возвращает значение (read-back) и номер вида.
    /// </summary>
    public Task<DrawingAnnotationResult> AddRoughAsync(int viewNumber, double x, double y, string? value,
        RoughSignType signType, double angleDeg, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => AddRoughCore(viewNumber, x, y, value, signType, angleDeg), ct);

    private DrawingAnnotationResult AddRoughCore(int viewNumber, double x, double y, string? value,
        RoughSignType signType, double angleDeg)
    {
        DrawingValidation.RequireFiniteCoords(x, y, angleDeg);
        var text = string.IsNullOrWhiteSpace(value) ? string.Empty : value;

        var (view, symbols) = RequireSymbols2DContainer(viewNumber);
        var roughs = symbols.Roughs
            ?? throw new InvalidOperationException("Коллекция Roughs недоступна на виде.");
        var rough = roughs.Add()
            ?? throw new InvalidOperationException("Roughs.Add вернул null.");
        try
        {
            rough.BranchX0 = x; rough.BranchY0 = y; rough.Angle = angleDeg;
            var rp = rough as IRoughParams
                ?? throw new InvalidOperationException("Знак шероховатости не приводится к IRoughParams.");
            rp.SignType = RoughSignTypes.ToKompas(signType);
            var paramText = rp.RoughParamText
                ?? throw new InvalidOperationException("RoughParamText недоступен (текст значения).");
            paramText.Str = text;

            if (!rough.Update())
                throw new InvalidOperationException("IRough.Update вернул FALSE — знак не построен.");
            if (!rough.Valid)
                throw new InvalidOperationException("Знак шероховатости построен невалидным.");

            var readBack = rp.RoughParamText?.Str ?? string.Empty; // read-back из COM
            return new DrawingAnnotationResult { Value = readBack, ViewNumber = view.Number };
        }
        catch
        {
            try { rough.Delete(); } catch { /* лучшее усилие */ }
            throw;
        }
    }

    /// <summary>Число знаков шероховатости на виде (для проверки попадания в вид).</summary>
    public Task<int> GetViewRoughCountAsync(int viewNumber, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.Roughs?.Count ?? 0, ct);

    /// <summary>
    /// Поставить свободную текстовую надпись на виде в точке (<paramref name="x"/>,<paramref name="y"/>)
    /// (ЛОКАЛЬНАЯ СК вида, мм) стилем по умолчанию. <paramref name="text"/> — содержимое (\n — многострочно).
    /// <paramref name="angleDeg"/> — угол наклона текста (°). Возвращает текст (read-back) и номер вида.
    /// </summary>
    public Task<DrawingAnnotationResult> AddTextAsync(int viewNumber, double x, double y, string text,
        double angleDeg, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => AddTextCore(viewNumber, x, y, text, angleDeg), ct);

    private DrawingAnnotationResult AddTextCore(int viewNumber, double x, double y, string text, double angleDeg)
    {
        DrawingValidation.RequireNonEmptyText(text, nameof(text));
        DrawingValidation.RequireFiniteCoords(x, y, angleDeg);

        var (view, container) = RequireDrawingContainer(viewNumber);
        var texts = container.DrawingTexts
            ?? throw new InvalidOperationException("Коллекция DrawingTexts недоступна на виде.");
        var dt = texts.Add()
            ?? throw new InvalidOperationException("DrawingTexts.Add вернул null.");
        try
        {
            dt.X = x; dt.Y = y; dt.Angle = angleDeg;
            var content = dt as IText
                ?? throw new InvalidOperationException("Надпись не приводится к IText.");
            content.Str = text;

            if (!dt.Update())
                throw new InvalidOperationException("IDrawingText.Update вернул FALSE — надпись не построена.");
            if (!dt.Valid)
                throw new InvalidOperationException("Надпись построена невалидной.");

            var readBack = ((IText)dt).Str ?? string.Empty; // read-back из COM
            return new DrawingAnnotationResult { Value = readBack, ViewNumber = view.Number };
        }
        catch
        {
            try { dt.Delete(); } catch { /* лучшее усилие */ }
            throw;
        }
    }

    /// <summary>Число текстовых надписей на виде (для проверки попадания в вид; текст в
    /// IDrawingContainer, а не в IView.ObjectCount).</summary>
    public Task<int> GetViewTextCountAsync(int viewNumber, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => RequireDrawingContainer(viewNumber).Container.DrawingTexts?.Count ?? 0, ct);

    /// <summary>
    /// Поставить линию-выноску на виде: стрелка из полки указывает в (<paramref name="x"/>,<paramref name="y"/>),
    /// текст <paramref name="text"/> на полке у (<paramref name="textX"/>,<paramref name="textY"/>) — всё в
    /// ЛОКАЛЬНОЙ СК вида (мм). <paramref name="shelfDirection"/> — направление полки (Auto = по умолчанию КОМПАС).
    /// Возвращает текст (read-back) и номер вида.
    /// </summary>
    public Task<DrawingAnnotationResult> AddLeaderAsync(int viewNumber, double x, double y, double textX,
        double textY, string text, ShelfDirection shelfDirection, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => AddLeaderCore(viewNumber, x, y, textX, textY, text, shelfDirection), ct);

    private DrawingAnnotationResult AddLeaderCore(int viewNumber, double x, double y, double textX,
        double textY, string text, ShelfDirection shelfDirection)
    {
        DrawingValidation.RequireNonEmptyText(text, nameof(text));
        DrawingValidation.RequireFiniteCoords(x, y, textX, textY);
        DrawingValidation.RequireDistinctPoints(x, y, textX, textY); // остриё ≠ полка (иначе вырождение)

        var (view, symbols) = RequireSymbols2DContainer(viewNumber);
        var leaders = symbols.Leaders
            ?? throw new InvalidOperationException("Коллекция Leaders недоступна на виде.");
        var baseLeader = leaders.Add(DrawingObjectTypeEnum.ksDrLeader)
            ?? throw new InvalidOperationException("Leaders.Add вернул null.");
        try
        {
            // КРИТИЧЕСКИЙ ПОРЯДОК: AddBranchByPoint ДОЛЖЕН предшествовать SetBranchTextPosition/Update —
            // выноска без ответвлений валит КОМПАС (RPC_E_SERVERFAULT). Проверено спайком.
            var branches = baseLeader as IBranchs
                ?? throw new InvalidOperationException("Выноска не приводится к IBranchs.");
            if (!branches.AddBranchByPoint(0, x, y))        // остриё ответвления 0 (куда указывает стрелка)
                throw new InvalidOperationException("AddBranchByPoint вернул FALSE — ответвление выноски не создано.");
            if (!baseLeader.SetBranchTextPosition(textX, textY)) // положение полки/текста
                throw new InvalidOperationException("SetBranchTextPosition вернул FALSE — полка выноски не задана.");

            var leader = baseLeader as ILeader
                ?? throw new InvalidOperationException("Выноска не приводится к ILeader.");
            var shelf = leader.TextOnShelf
                ?? throw new InvalidOperationException("Текст полки выноски недоступен (TextOnShelf == null).");
            shelf.Str = text;
            if (shelfDirection != ShelfDirection.Auto)
                leader.ShelfDirection = ShelfDirections.ToKompas(shelfDirection);

            if (!baseLeader.Update())
                throw new InvalidOperationException("ILeader.Update вернул FALSE — выноска не построена.");
            if (!baseLeader.Valid)
                throw new InvalidOperationException("Выноска построена невалидной.");

            var readBack = leader.TextOnShelf?.Str ?? string.Empty; // read-back из COM
            return new DrawingAnnotationResult { Value = readBack, ViewNumber = view.Number };
        }
        catch
        {
            try { baseLeader.Delete(); } catch { /* лучшее усилие */ }
            throw;
        }
    }

    /// <summary>Число выносок на виде (для проверки попадания в вид; считает все подтипы выносок).</summary>
    public Task<int> GetViewLeaderCountAsync(int viewNumber, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.Leaders?.Count ?? 0, ct);

    /// <summary>
    /// Задать технические требования активного чертежа (единый блок над основной надписью; перезаписывает
    /// прежние). <paramref name="text"/> — строки через \n. Возвращает число строк.
    /// </summary>
    public Task<int> SetTechnicalRequirementsAsync(string text, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => SetTechnicalRequirementsCore(text), ct);

    private int SetTechnicalRequirementsCore(string text)
    {
        DrawingValidation.RequireNonEmptyText(text, nameof(text));
        var normalized = text.Replace("\r\n", "\n");

        var td = RequireTechnicalDemand();
        var content = td.Text
            ?? throw new InvalidOperationException("Текст технических требований недоступен (Text == null).");
        content.Str = normalized; // Str замещает содержимое (как у штампа)
        if (!td.Update())
            throw new InvalidOperationException("ITechnicalDemand.Update вернул FALSE — требования не заданы.");

        // Число содержательных строк: хвостовые пустые строки отбрасываем ("a\n" → 1).
        return normalized.TrimEnd('\n').Split('\n').Length;
    }

    /// <summary>Прочитать текст технических требований активного чертежа (для проверки/диагностики).</summary>
    public Task<string> ReadTechnicalRequirementsAsync(CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => RequireTechnicalDemand().Text?.Str ?? string.Empty, ct);

    private ITechnicalDemand RequireTechnicalDemand()
    {
        var doc = RequireActiveDrawing() as IDrawingDocument
            ?? throw new InvalidOperationException("Активный чертёж не приводится к IDrawingDocument.");
        return doc.TechnicalDemand
            ?? throw new InvalidOperationException("У чертежа недоступны технические требования (TechnicalDemand == null).");
    }

    /// <summary>Вид + его IDrawingContainer (QI от вида). Для текстовых надписей (DrawingTexts).</summary>
    private (IView View, IDrawingContainer Container) RequireDrawingContainer(int viewNumber)
    {
        var view = FindView(viewNumber);
        var container = view as IDrawingContainer
            ?? throw new InvalidOperationException("Вид не приводится к IDrawingContainer.");
        return (view, container);
    }

    /// <summary>Допуск совпадения окружности с ключом поиска при ассоциативной привязке (мм).</summary>
    private const double CircleMatchToleranceMm = 1.0;

    /// <summary>Вид + оба контейнера (QI от одного вида): ISymbols2DContainer (создание размера) и
    /// IDrawingContainer (поиск геометрии для ассоциативной привязки).</summary>
    private (IView View, ISymbols2DContainer Symbols, IDrawingContainer Drawing) RequireViewContainers(int viewNumber)
    {
        var view = FindView(viewNumber);
        var symbols = view as ISymbols2DContainer
            ?? throw new InvalidOperationException("Вид не приводится к ISymbols2DContainer.");
        var drawing = view as IDrawingContainer
            ?? throw new InvalidOperationException("Вид не приводится к IDrawingContainer.");
        return (view, symbols, drawing);
    }

    /// <summary>Найти окружность вида по ключу (центр + радиус) и вернуть её как IDrawingObject
    /// (для <c>dim.BaseObject</c>). Бросает при отсутствии/неоднозначности.</summary>
    private static IDrawingObject FindBaseCircle(IDrawingContainer drawing, double xc, double yc, double radius)
    {
        var items = CollectCircles(drawing);
        var index = CircularObjectMatch.SelectMatchIndex(
            items.Select(i => i.Geom).ToList(), xc, yc, radius, CircleMatchToleranceMm);
        return items[index].Obj;
    }

    /// <summary>Собрать окружности вида (IDrawingContainer.Circles) с их геометрией и COM-ссылкой.</summary>
    private static List<(IDrawingObject Obj, CircleCandidate Geom)> CollectCircles(IDrawingContainer drawing)
    {
        var result = new List<(IDrawingObject, CircleCandidate)>();
        var circles = drawing.Circles;
        if (circles == null) return result;
        for (var i = 0; i < circles.Count; i++)
        {
            if (circles[i] is not ICircle c) continue;
            result.Add(((IDrawingObject)c, new CircleCandidate(c.Xc, c.Yc, c.Radius)));
        }
        return result;
    }

    /// <summary>Вид + его ISymbols2DContainer (QI от вида). Общий для линейных/диаметральных/… размеров.</summary>
    private (IView View, ISymbols2DContainer Symbols) RequireSymbols2DContainer(int viewNumber)
    {
        var view = FindView(viewNumber);
        var symbols = view as ISymbols2DContainer
            ?? throw new InvalidOperationException("Вид не приводится к ISymbols2DContainer.");
        return (view, symbols);
    }

    /// <summary>Найти вид: при viewNumber ≤ 0 — первый несистемный (Number != 0); иначе по номеру.</summary>
    private IView FindView(int viewNumber)
    {
        var views = RequireActiveDrawingViews();
        foreach (var o in views)
        {
            if (o is not IView v) continue;
            if (viewNumber <= 0)
            {
                if (v.Number != 0) return v; // первый несистемный
            }
            else if (v.Number == viewNumber)
            {
                return v;
            }
        }
        throw new InvalidOperationException(viewNumber <= 0
            ? "На чертеже нет несистемного вида. Сначала создайте виды: drawing_create_standard_views."
            : $"Вид с номером {viewNumber} не найден на чертеже.");
    }

    // Стандартная тройка проекций: ksPtFront=1, ksPtUp=3, ksPtLeft=5 (ksRelativeProjectionTypeEnum).
    private static readonly object[] StandardProjections = { 1, 3, 5 };
    private const int ExpectedCreated = 3;
    private const double GapMm = 20; // зазоры между видами (DX=DY)

    private DrawingViewsResult CreateStandardViewsCore(string partFilePath, double scale, double x, double y)
    {
        var path = DrawingValidation.NormalizeModelPath(partFilePath);
        DrawingValidation.RequireScale(scale);
        if (!double.IsFinite(x) || !double.IsFinite(y))
            throw new ArgumentException("Координаты привязки вида должны быть конечными числами.");
        if (!File.Exists(path))
            throw new FileNotFoundException($"Файл модели не найден: {path}", path);
        if (new FileInfo(path).Length == 0)
            throw new InvalidOperationException($"Файл модели пуст (не сохранён?): {path}");

        var views = RequireActiveDrawingViews();
        // Снимок номеров существующих видов — чтобы потом отличить созданные ИМЕННО этим вызовом
        // (на чертеже уже есть системный вид и могут быть прежние виды).
        var beforeNumbers = CollectViewNumbers(views);

        if (!views.AddStandartViews(path, "#Спереди", StandardProjections, x, y, scale, GapMm, GapMm))
            throw new InvalidOperationException(
                $"AddStandartViews вернул FALSE — виды не созданы (файл модели или проекция недоступны): {path}");

        var newViews = CollectNewViews(views, beforeNumbers);
        try
        {
            if (newViews.Count != ExpectedCreated)
                throw new InvalidOperationException(
                    $"Ожидалось {ExpectedCreated} вида, создано {newViews.Count}.");

            // Гарантия непустоты: у созданных видов должна быть геометрия (иначе модель не
            // спроецировалась). Проверяем ИМЕННО новые виды, не всю коллекцию.
            if (newViews.Sum(v => v.ObjectCount) <= 0)
                throw new InvalidOperationException(
                    "Стандартные виды созданы, но пусты (модель не спроецировалась). Проверьте файл модели.");

            return new DrawingViewsResult
            {
                Created = newViews.Count,
                Total = views.Count,
                ViewNumbers = newViews.Select(v => v.Number).ToArray(),
            };
        }
        catch
        {
            // Откат: удаляем виды, созданные этим вызовом, чтобы чертёж не остался в полу-состоянии.
            foreach (var v in newViews)
                try { v.Delete(); } catch { /* лучшее усилие */ }
            throw;
        }
    }

    /// <summary>
    /// Получить активный чертёж как <see cref="IKompasDocument2D"/>. Раздельные ошибки: нет документа /
    /// тип != ksDocumentDrawing / не приводится к IKompasDocument2D.
    /// </summary>
    private IKompasDocument2D RequireActiveDrawing()
    {
        var doc = _session.Application.ActiveDocument
            ?? throw new InvalidOperationException("Нет активного документа. Создайте чертёж: document_create drawing.");
        if (doc.DocumentType != DocumentTypeEnum.ksDocumentDrawing)
            throw new InvalidOperationException(
                $"Активный документ не является чертежом (тип: {doc.DocumentType}). Создайте чертёж: document_create drawing.");
        return doc as IKompasDocument2D
            ?? throw new InvalidOperationException("Активный документ не приводится к IKompasDocument2D.");
    }

    private IViews RequireActiveDrawingViews()
        => RequireActiveDrawing().ViewsAndLayersManager?.Views
            ?? throw new InvalidOperationException("Не удалось получить коллекцию видов чертежа.");

    private static HashSet<int> CollectViewNumbers(IViews views)
    {
        var set = new HashSet<int>();
        foreach (var o in views)
            if (o is IView v)
                set.Add(v.Number);
        return set;
    }

    private static List<IView> CollectNewViews(IViews views, HashSet<int> beforeNumbers)
    {
        var list = new List<IView>();
        foreach (var o in views)
            if (o is IView v && !beforeNumbers.Contains(v.Number))
                list.Add(v);
        return list;
    }
}
