using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;

namespace ZB.MOM.WW.MxGateway.Client;

/// <summary>
///     One node in a lazy-loaded Galaxy browse tree. Holds the underlying
///     <see cref="GalaxyObject"/> and exposes <see cref="ExpandAsync"/> to fetch
///     its direct children on demand. Expansion is one-shot: a second call is a
///     no-op. Pagination of large sibling sets is handled internally.
/// </summary>
public sealed class LazyBrowseNode
{
    private readonly GalaxyRepositoryClient _client;
    private readonly BrowseChildrenOptions _options;
    // Won't Fix: this gate is used only via WaitAsync/Release and
    // never via AvailableWaitHandle, so SemaphoreSlim allocates no kernel wait handle —
    // it holds no unmanaged/OS handle to leak. It is pure managed memory whose lifetime
    // is the node's, so the type is intentionally not IDisposable: making it disposable
    // would push per-node disposal onto every tree consumer (thousands of nodes) for no
    // resource benefit.
    private readonly SemaphoreSlim _expandLock = new(1, 1);

    // Published once, under _expandLock, when expansion completes. Lock-free readers
    // see either the empty pre-expansion snapshot or the fully-populated post-expansion
    // snapshot — never a partially-filled list — because the snapshot is built in a local
    // and handed off via Volatile.Write (release) paired with Volatile.Read (acquire).
    private IReadOnlyList<LazyBrowseNode> _children = [];
    private volatile bool _isExpanded;

    /// <summary>Initializes a new instance of the <see cref="LazyBrowseNode"/> class.</summary>
    /// <param name="client">The Galaxy Repository client used to fetch children on expansion.</param>
    /// <param name="object">The underlying Galaxy object for this node.</param>
    /// <param name="hasChildrenHint">Whether the server reports this node has at least one matching descendant.</param>
    /// <param name="options">The browse options to apply when fetching children.</param>
    internal LazyBrowseNode(
        GalaxyRepositoryClient client,
        GalaxyObject @object,
        bool hasChildrenHint,
        BrowseChildrenOptions options)
    {
        _client = client;
        Object = @object;
        HasChildrenHint = hasChildrenHint;
        _options = options;
    }

    /// <summary>The underlying Galaxy object for this node.</summary>
    public GalaxyObject Object { get; }

    /// <summary>True when the server reports this node has at least one matching descendant.</summary>
    public bool HasChildrenHint { get; }

    /// <summary>Direct children loaded by <see cref="ExpandAsync"/>; empty until then.</summary>
    public IReadOnlyList<LazyBrowseNode> Children => Volatile.Read(ref _children);

    /// <summary>True after the first <see cref="ExpandAsync"/> call completes.</summary>
    public bool IsExpanded => _isExpanded;

    /// <summary>
    ///     Fetches direct children from the gateway and populates <see cref="Children"/>.
    ///     Idempotent: subsequent calls are no-ops.
    /// </summary>
    /// <remarks>
    ///     Thread-safe: concurrent callers see exactly one fetch; subsequent callers
    ///     (after the first completes) return immediately. <see cref="Children"/> and
    ///     <see cref="IsExpanded"/> may be read concurrently with an in-flight
    ///     <see cref="ExpandAsync"/> on another thread; the populated children are
    ///     published as an immutable snapshot under a release barrier, so a reader that
    ///     observes <see cref="IsExpanded"/> as <see langword="true"/> always sees the
    ///     fully-populated <see cref="Children"/>, and a reader never enumerates a
    ///     partially-built list.
    /// </remarks>
    /// <param name="cancellationToken">Token to observe for cancellation.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public async Task ExpandAsync(CancellationToken cancellationToken = default)
    {
        if (_isExpanded)
        {
            return;
        }

        await _expandLock.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            if (_isExpanded)
            {
                return;
            }

            // Accumulate into a local list, never the published field, so a lock-free
            // reader can never observe a half-populated collection or enumerate a list
            // that is being mutated mid-append.
            List<LazyBrowseNode> children = [];
            string pageToken = string.Empty;
            HashSet<string> seenPageTokens = new(StringComparer.Ordinal);
            do
            {
                BrowseChildrenRequest request = GalaxyRepositoryClient.BuildBrowseChildrenRequest(_options);
                request.ParentGobjectId = Object.GobjectId;
                request.PageToken = pageToken;

                BrowseChildrenReply reply = await _client
                    .BrowseChildrenRawAsync(request, cancellationToken)
                    .ConfigureAwait(false);

                for (int i = 0; i < reply.Children.Count; i++)
                {
                    bool hint = i < reply.ChildHasChildren.Count && reply.ChildHasChildren[i];
                    children.Add(new LazyBrowseNode(_client, reply.Children[i], hint, _options));
                }

                pageToken = reply.NextPageToken;
                if (!string.IsNullOrWhiteSpace(pageToken) && !seenPageTokens.Add(pageToken))
                {
                    throw new MxGatewayException(
                        $"Galaxy BrowseChildren returned a repeated page token '{pageToken}'.");
                }
            }
            while (!string.IsNullOrWhiteSpace(pageToken));

            // Publish the completed, immutable snapshot (release) before marking the node
            // expanded (the volatile write below). A reader that observes IsExpanded == true
            // is guaranteed to also observe the fully-populated Children.
            Volatile.Write(ref _children, children);
            _isExpanded = true;
        }
        finally
        {
            _expandLock.Release();
        }
    }
}
