using ZB.MOM.WW.GalaxyRepository.Grpc;

namespace ZB.MOM.WW.MxGateway.Server.Dashboard;

/// <summary>
///     One node in the dashboard Browse hierarchy tree. Wraps a Galaxy object
///     and its child objects; the object's attributes are the leaf "tags" the
///     operator can right-click and add to the subscription panel.
/// </summary>
public sealed class DashboardBrowseNode
{
    /// <summary>The underlying Galaxy object for this node.</summary>
    public required GalaxyObject Object { get; init; }

    /// <summary>Child objects contained by this object, sorted areas-first then by name.</summary>
    public List<DashboardBrowseNode> Children { get; } = [];

    /// <summary>The label shown for this node in the tree.</summary>
    public string DisplayName =>
        !string.IsNullOrWhiteSpace(Object.BrowseName) ? Object.BrowseName
        : !string.IsNullOrWhiteSpace(Object.ContainedName) ? Object.ContainedName
        : Object.TagName;

    /// <summary>True when this node is a Galaxy area rather than an instance object.</summary>
    public bool IsArea => Object.IsArea;

    /// <summary>The object's attributes — the browsable tags.</summary>
    public IReadOnlyList<GalaxyAttribute> Attributes => Object.Attributes;

    /// <summary>True when the node has child objects or attributes to expand.</summary>
    public bool HasChildren => Children.Count > 0 || Object.Attributes.Count > 0;

    /// <summary>Whether this node has at least one matching descendant, per the
    /// server's <c>child_has_children</c> projector hint. Controls whether the UI
    /// shows an expand triangle before children have actually loaded.</summary>
    public bool HasChildrenHint { get; init; }

    /// <summary>The lazy-load state for this node's children.</summary>
    public BrowseLoadState LoadState { get; set; } = BrowseLoadState.NotLoaded;

    /// <summary>Short error string if the last load attempt failed; null otherwise.</summary>
    public string? LoadError { get; set; }
}

/// <summary>Lazy-load lifecycle of a browse node's children.</summary>
public enum BrowseLoadState
{
    /// <summary>Children have not been requested yet.</summary>
    NotLoaded,

    /// <summary>A load is in progress.</summary>
    Loading,

    /// <summary>Children have been loaded into <see cref="DashboardBrowseNode.Children"/>.</summary>
    Loaded,

    /// <summary>The last load attempt failed; see <see cref="DashboardBrowseNode.LoadError"/>.</summary>
    Error,
}

/// <summary>
///     Builds the dashboard Browse tree from the flat Galaxy object list held
///     by <c>IGalaxyHierarchyCache</c>. Pure and side-effect free so the
///     parent/child linkage and ordering rules are unit-testable.
/// </summary>
public static class DashboardBrowseTreeBuilder
{
    /// <summary>Builds the root nodes of the Browse tree.</summary>
    /// <param name="objects">The flat Galaxy object list.</param>
    /// <returns>The root nodes, sorted areas-first then alphabetically.</returns>
    public static IReadOnlyList<DashboardBrowseNode> Build(IReadOnlyList<GalaxyObject> objects)
    {
        ArgumentNullException.ThrowIfNull(objects);

        Dictionary<int, DashboardBrowseNode> nodes = new(objects.Count);
        foreach (GalaxyObject galaxyObject in objects)
        {
            // Last write wins on a duplicate gobject id — Galaxy ids are unique
            // in practice, but guard so the dictionary build never throws.
            nodes[galaxyObject.GobjectId] = new DashboardBrowseNode { Object = galaxyObject };
        }

        List<DashboardBrowseNode> roots = [];
        foreach (DashboardBrowseNode node in nodes.Values)
        {
            int parentId = node.Object.ParentGobjectId;
            if (parentId != 0
                && parentId != node.Object.GobjectId
                && nodes.TryGetValue(parentId, out DashboardBrowseNode? parent))
            {
                parent.Children.Add(node);
            }
            else
            {
                roots.Add(node);
            }
        }

        SortRecursive(roots);
        return roots;
    }

    private static void SortRecursive(List<DashboardBrowseNode> nodes)
    {
        nodes.Sort(CompareNodes);
        foreach (DashboardBrowseNode node in nodes)
        {
            SortRecursive(node.Children);
        }
    }

    // Areas sort before instance objects; within a group, by display name.
    private static int CompareNodes(DashboardBrowseNode left, DashboardBrowseNode right)
    {
        if (left.IsArea != right.IsArea)
        {
            return left.IsArea ? -1 : 1;
        }

        return string.Compare(left.DisplayName, right.DisplayName, StringComparison.OrdinalIgnoreCase);
    }
}
