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

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

        var (view, symbols) = RequireSymbols2DContainer(viewNumber);
        var dim = symbols.DiametralDimensions?.Add()
            ?? throw new InvalidOperationException("DiametralDimensions.Add вернул null.");
        try
        {
            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) окружности/дуги с центром (xc,yc) и радиусом <paramref name="radius"/>
    /// (ЛОКАЛЬНАЯ СК вида, мм) на виде. <paramref name="angleDeg"/> — направление выноски (градусы).
    /// Значение (= радиус) измеряется автоматически. Возвращает радиус и номер вида.
    /// </summary>
    public Task<DrawingDimensionResult> AddRadialDimensionAsync(int viewNumber, double xc, double yc,
        double radius, double angleDeg, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => AddRadialDimensionCore(viewNumber, xc, yc, radius, angleDeg), ct);

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

        var (view, symbols) = RequireSymbols2DContainer(viewNumber);
        var dim = symbols.RadialDimensions?.Add()
            ?? throw new InvalidOperationException("RadialDimensions.Add вернул null.");
        try
        {
            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);

    /// <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;
    }
}
