using System.Globalization;
using System.Text;

namespace Kompas.Mcp.Core.Query;

/// <summary>
/// Чистый (без COM) рендер структурного «паспорта» модели и вспомогательные форматтеры.
/// Вынесено отдельно, чтобы покрыть логику представления юнит-тестами.
/// </summary>
public static class InspectionText
{
    /// <summary>Разделы «паспорта» модели — чтобы не собирать и не печатать лишнее.</summary>
    [Flags]
    public enum ModelSections
    {
        None = 0,
        Box = 1,
        Mass = 2,
        Bodies = 4,
        Topology = 8,
        Tree = 16,
        Variables = 32,
        All = Box | Mass | Bodies | Topology | Tree | Variables,
    }

    private static readonly (string Name, ModelSections Value)[] SectionNames =
    [
        ("box", ModelSections.Box),
        ("mass", ModelSections.Mass),
        ("bodies", ModelSections.Bodies),
        ("topology", ModelSections.Topology),
        ("tree", ModelSections.Tree),
        ("variables", ModelSections.Variables),
    ];

    /// <summary>
    /// Разобрать список разделов («box,mass») в набор флагов. Пусто или «all» — все разделы.
    /// Неизвестное имя — ошибка со списком допустимых (агент исправляется с первой попытки).
    /// </summary>
    public static ModelSections ParseSections(string? spec)
    {
        if (string.IsNullOrWhiteSpace(spec)) return ModelSections.All;

        var result = ModelSections.None;
        foreach (var raw in spec.Split([',', ';', ' '], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
        {
            var name = raw.ToLowerInvariant();
            if (name == "all") return ModelSections.All;

            var known = SectionNames.FirstOrDefault(s => s.Name == name);
            if (known.Name is null)
                throw new ArgumentException(
                    $"Неизвестный раздел '{raw}'. Допустимо: {string.Join(" | ", SectionNames.Select(s => s.Name))} | all.");
            result |= known.Value;
        }
        return result == ModelSections.None ? ModelSections.All : result;
    }

    // Габарит пустой детали КОМПАС отдаёт «пустышкой» ~2·10³⁰⁸ (double близко к максимуму).
    private const double EmptyGabaritThreshold = 1e12;

    /// <summary>true — габарит соответствует пустой модели (sentinel-«пустышка» или нулевой размер).</summary>
    public static bool IsEmptyGabarit(double sizeX, double sizeY, double sizeZ)
    {
        if (double.IsNaN(sizeX) || double.IsNaN(sizeY) || double.IsNaN(sizeZ)) return true;
        if (Math.Abs(sizeX) > EmptyGabaritThreshold ||
            Math.Abs(sizeY) > EmptyGabaritThreshold ||
            Math.Abs(sizeZ) > EmptyGabaritThreshold) return true;
        return sizeX == 0 && sizeY == 0 && sizeZ == 0;
    }

    // Формообразующие операции, наличие которых означает параметрическую историю.
    // HashSet<string> даёт O(1) Contains вместо O(n) по массиву при bulk-проверке граней.
    private static readonly HashSet<string> OpKinds =
        new(StringComparer.Ordinal) { "sketch", "extrusion", "cut_extrusion", "revolve", "cut_revolve", "fillet", "chamfer" };

    /// <summary>
    /// Модель — «голый» импорт без истории построения (типично для STEP): в дереве только начало
    /// координат и тело(а), нет ни формообразующих операций, ни иных узлов-правок.
    /// </summary>
    public static bool IsImported(IReadOnlyList<FeatureInfo> features, int bodyCount)
        => bodyCount > 0
           && !features.Any(f => OpKinds.Contains(f.Kind))
           && features.Count <= bodyCount + 1; // origin + тело(а), ничего сверх

    /// <summary>Сгруппировать типы и вернуть «3 plane, 2 cylinder» (по убыванию числа, тай-брейк по имени).</summary>
    public static string GroupTypes(IReadOnlyList<string> types)
    {
        if (types.Count == 0) return "—";
        // Count материализуем один раз: IGrouping<K,V> — IEnumerable без кеша,
        // двойной вызов g.Count() итерирует группу дважды.
        return string.Join(", ", types
            .GroupBy(t => t)
            .Select(g => (Key: g.Key, Count: g.Count()))
            .OrderByDescending(x => x.Count)
            .ThenBy(x => x.Key, StringComparer.Ordinal)
            .Select(x => $"{x.Count} {x.Key}"));
    }

    /// <summary>
    /// Наименование и обозначение детали в заголовке паспорта. Пустое наименование — это «Деталь»
    /// в дереве: подсказываем, чем его задать, пока модель не ушла в чертёж или спецификацию.
    /// </summary>
    public static string PartIdentityText(ModelDescription d)
    {
        var name = (d.PartName ?? "").Trim();
        var marking = (d.PartMarking ?? "").Trim();
        if (name.Length == 0 && marking.Length == 0)
            return " — деталь без наименования (задайте через set_part_info)";

        var parts = new List<string>(2);
        if (name.Length > 0) parts.Add($"наименование «{name}»");
        if (marking.Length > 0) parts.Add($"обозначение «{marking}»");
        return " — " + string.Join(", ", parts);
    }

    /// <summary>Отрендерить «паспорт» модели в читаемый текст (по умолчанию — все разделы).</summary>
    public static string Render(ModelDescription d, ModelSections sections = ModelSections.All)
    {
        var sb = new StringBuilder();
        sb.AppendLine($"Документ: «{d.Name}» ({d.DocumentTypeName}){PartIdentityText(d)}");

        if (d.IsEmpty)
        {
            sb.Append("Модель пуста: нет тел (габарит не определён).");
            return sb.ToString();
        }

        if (sections.HasFlag(ModelSections.Box) && d.Box is { } box)
            sb.AppendLine(Inv($"Габарит (Д×Ш×В): {box.SizeX:F2} × {box.SizeY:F2} × {box.SizeZ:F2} мм [X {box.MinX:F2}…{box.MaxX:F2}; Y {box.MinY:F2}…{box.MaxY:F2}; Z {box.MinZ:F2}…{box.MaxZ:F2}]"));

        if (sections.HasFlag(ModelSections.Mass) && d.Mass is { } m)
            sb.AppendLine(Inv($"МЦХ: объём {m.Volume:F1} мм³, масса {m.Mass:F4} кг, площадь {m.Area:F1} мм², центр масс ({m.CenterX:F2}, {m.CenterY:F2}, {m.CenterZ:F2})"));

        if (sections.HasFlag(ModelSections.Bodies))
            sb.AppendLine($"Тела: {d.Bodies.Count}" + FormatBodies(d.Bodies));

        if (sections.HasFlag(ModelSections.Topology))
            sb.AppendLine($"Топология: граней {d.FaceTypes.Count} ({GroupTypes(d.FaceTypes)}); " +
                          $"рёбер {d.EdgeTypes.Count} ({GroupTypes(d.EdgeTypes)})");

        if (sections.HasFlag(ModelSections.Tree)) AppendTree(sb, d);
        if (sections.HasFlag(ModelSections.Variables)) AppendVariables(sb, d.Variables);

        return sb.ToString().TrimEnd();
    }

    private static string FormatBodies(IReadOnlyList<BodyInfo> bodies)
    {
        if (bodies.Count == 0) return "";
        var parts = bodies.Select(b => $"тело #{b.Index}{(b.IsSolid ? " (твёрдое)" : "")}, граней {b.FaceCount}");
        return " — " + string.Join("; ", parts);
    }

    private static void AppendTree(StringBuilder sb, ModelDescription d)
    {
        if (d.Imported)
        {
            var names = string.Join(", ", d.Features.Select(f => $"«{f.Name}»"));
            sb.AppendLine("Дерево построения: импортированная модель без истории построения " +
                          "(доступна только топология)." + (names.Length > 0 ? " Узлы: " + names + "." : ""));
            return;
        }

        if (d.Features.Count == 0)
        {
            sb.AppendLine("Дерево построения: нет операций.");
            return;
        }

        sb.AppendLine($"Дерево построения ({d.Features.Count} операц.):");
        foreach (var f in d.Features)
            sb.AppendLine($"  [{f.Index}] «{f.Name}»" + (string.IsNullOrEmpty(f.Detail) ? "" : $" — {f.Detail}"));
    }

    private static void AppendVariables(StringBuilder sb, IReadOnlyList<VariableInfo> vars)
    {
        if (vars.Count == 0) { sb.AppendLine("Переменные: нет"); return; }

        sb.AppendLine($"Переменные ({vars.Count}):");
        foreach (var v in vars)
        {
            var flags = (v.External ? " [внешняя]" : "") + (v.Information ? " [инфо]" : "");
            sb.AppendLine(Inv($"  {v.Name} = {v.Expression} (= {v.Value:G}){flags}"));
        }
    }

    private static string Inv(FormattableString fs) => FormattableString.Invariant(fs);
}
