@* Recursive, read-only UNS browse-tree renderer. Modelled on DriverBrowseTree:
   per-node indent, expand chevron and Bootstrap/theme styling. This component
   owns no state and calls no service — it reads node.Expanded/Loading/Error/
   Children and raises callbacks; the host page owns expansion + lazy loading. *@
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns

@foreach (var root in Roots.Where(r => VisibleUnder(r, false)))
{
    @RenderNode(root, 0, false)
}

@code {
    /// <summary>The top-level UNS nodes to render (typically the enterprise roots).</summary>
    [Parameter, EditorRequired] public IReadOnlyList<UnsNode> Roots { get; set; } = default!;

    /// <summary>Raised for the primary "add child" action of a node (e.g. "+ Area" on a cluster, "+ Tag" on equipment).</summary>
    [Parameter] public EventCallback<UnsNode> OnAddChild { get; set; }

    /// <summary>Raised for the equipment-only "+ Virtual tag" action, kept distinct from OnAddChild ("+ Tag").</summary>
    [Parameter] public EventCallback<UnsNode> OnAddVirtualTag { get; set; }

    /// <summary>Raised when the user edits a node (Area/Line/Equipment/Tag/VirtualTag).</summary>
    [Parameter] public EventCallback<UnsNode> OnEdit { get; set; }

    /// <summary>Raised when the user deletes a node (Area/Line/Equipment/Tag/VirtualTag).</summary>
    [Parameter] public EventCallback<UnsNode> OnDelete { get; set; }

    /// <summary>Raised when the user toggles a node's expansion; the host owns the resulting state + lazy load.</summary>
    [Parameter] public EventCallback<UnsNode> OnToggleExpand { get; set; }

    /// <summary>
    /// Optional case-insensitive substring filter on DisplayName. A node is shown when it matches,
    /// sits under a matching ancestor, or has a matching descendant, and the path to matches
    /// auto-expands. Lazy tag children are only considered once their equipment has been loaded.
    /// </summary>
    [Parameter] public string? Filter { get; set; }

    private RenderFragment RenderNode(UnsNode node, int depth, bool ancestorMatched) => __builder =>
    {
        var indent = $"padding-left:{depth * 18}px";
        var hasExpander = node.Children.Count > 0 || node.HasLazyChildren;
        // Under an active filter the matched path auto-expands, so the chevron and the child block
        // follow `childrenShown` (derived from the filter) rather than the user's node.Expanded flag.
        var matched = ancestorMatched || SelfMatches(node);
        var visibleChildren = HasFilter ? VisibleChildren(node, matched).ToList() : node.Children;
        var childrenShown = HasFilter ? visibleChildren.Count > 0 : node.Expanded;
        <div @key="node.Key" class="d-flex align-items-center gap-1 py-1" style="@indent">
            @if (hasExpander)
            {
                <button type="button" class="btn btn-sm btn-link p-0"
                        @onclick="@(() => OnToggleExpand.InvokeAsync(node))" style="width:18px">
                    @(childrenShown ? "▼" : "▶")
                </button>
            }
            else
            {
                <span style="width:18px"></span>
            }
            <span class="mono small">@node.DisplayName</span>
            @if (node.ChildCount > 0)
            {
                <span class="chip chip-idle ms-1" style="font-size:0.7rem">@node.ChildCount</span>
            }
            @RenderActions(node)
        </div>
        @if (node.Expanded && node.Loading)
        {
            <div class="small text-muted" style="@($"padding-left:{(depth + 1) * 18}px")">
                <span class="spinner-border spinner-border-sm me-1"></span>Loading&hellip;
            </div>
        }
        else if (node.Expanded && node.Error is not null)
        {
            <div class="small text-danger" style="@($"padding-left:{(depth + 1) * 18}px")">
                @node.Error
            </div>
        }
        else if (childrenShown)
        {
            @foreach (var child in visibleChildren)
            {
                @RenderNode(child, depth + 1, matched)
            }
        }
    };

    private RenderFragment RenderActions(UnsNode node) => __builder =>
    {
        switch (node.Kind)
        {
            case UnsNodeKind.Enterprise:
                // No actions on the enterprise root.
                break;

            case UnsNodeKind.Cluster:
                <button type="button" class="btn btn-sm btn-link p-0 ms-2"
                        @onclick="@(() => OnAddChild.InvokeAsync(node))">+ Area</button>
                <a class="btn btn-sm btn-link p-0 ms-2" href="@($"/clusters/{node.ClusterId}")">⚙ settings</a>
                break;

            case UnsNodeKind.Area:
                <button type="button" class="btn btn-sm btn-link p-0 ms-2"
                        @onclick="@(() => OnAddChild.InvokeAsync(node))">+ Line</button>
                <button type="button" class="btn btn-sm btn-link p-0 ms-2"
                        @onclick="@(() => OnEdit.InvokeAsync(node))">Edit</button>
                <button type="button" class="btn btn-sm btn-link p-0 ms-2 text-danger"
                        @onclick="@(() => OnDelete.InvokeAsync(node))">Delete</button>
                break;

            case UnsNodeKind.Line:
                <button type="button" class="btn btn-sm btn-link p-0 ms-2"
                        @onclick="@(() => OnAddChild.InvokeAsync(node))">+ Equipment</button>
                <button type="button" class="btn btn-sm btn-link p-0 ms-2"
                        @onclick="@(() => OnEdit.InvokeAsync(node))">Edit</button>
                <button type="button" class="btn btn-sm btn-link p-0 ms-2 text-danger"
                        @onclick="@(() => OnDelete.InvokeAsync(node))">Delete</button>
                break;

            case UnsNodeKind.Equipment:
                <button type="button" class="btn btn-sm btn-link p-0 ms-2"
                        @onclick="@(() => OnAddChild.InvokeAsync(node))">+ Tag</button>
                <button type="button" class="btn btn-sm btn-link p-0 ms-2"
                        @onclick="@(() => OnAddVirtualTag.InvokeAsync(node))">+ Virtual tag</button>
                <button type="button" class="btn btn-sm btn-link p-0 ms-2"
                        @onclick="@(() => OnEdit.InvokeAsync(node))">Edit</button>
                <button type="button" class="btn btn-sm btn-link p-0 ms-2 text-danger"
                        @onclick="@(() => OnDelete.InvokeAsync(node))">Delete</button>
                break;

            case UnsNodeKind.Tag:
            case UnsNodeKind.VirtualTag:
                <button type="button" class="btn btn-sm btn-link p-0 ms-2"
                        @onclick="@(() => OnEdit.InvokeAsync(node))">Edit</button>
                <button type="button" class="btn btn-sm btn-link p-0 ms-2 text-danger"
                        @onclick="@(() => OnDelete.InvokeAsync(node))">Delete</button>
                break;
        }
    };

    /// <summary>Whether a non-empty filter is currently active.</summary>
    private bool HasFilter => !string.IsNullOrWhiteSpace(Filter);

    /// <summary>Whether the node's own DisplayName matches the active filter (case-insensitive substring).</summary>
    private bool SelfMatches(UnsNode node)
    {
        var f = Filter?.Trim();
        return !string.IsNullOrEmpty(f)
            && node.DisplayName.Contains(f, StringComparison.OrdinalIgnoreCase);
    }

    /// <summary>
    /// Whether the node should appear under the active filter: it self-matches, sits under a matched
    /// ancestor, or has a (loaded) descendant that matches. Unloaded lazy tag children can't be
    /// inspected, so an unexpanded equipment matches on its own name only. The structural tree is
    /// bounded, so this recursive walk is cheap.
    /// </summary>
    private bool VisibleUnder(UnsNode node, bool ancestorMatched)
    {
        if (!HasFilter) { return true; }
        if (ancestorMatched || SelfMatches(node)) { return true; }
        return node.Children.Any(c => VisibleUnder(c, false));
    }

    /// <summary>
    /// The children to render under the active filter: the whole child list when the node (or an
    /// ancestor) matched, otherwise only the branches that still contain a match.
    /// </summary>
    private IEnumerable<UnsNode> VisibleChildren(UnsNode node, bool matched) =>
        matched ? node.Children : node.Children.Where(c => VisibleUnder(c, false));
}
