namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;

/// <summary>
/// Bounded-parallel execution of a per-item operation. Used where a protocol offers no
/// bulk form of a command and the only alternative is N serial round trips — today the
/// MxAccess Gateway's supervisory advise, which exists per item only (the worker's
/// <c>SubscribeBulk</c> issues plain advises). Pipelining 37,500 tags at a window of 16
/// replaces 37,500 serial round trips with ~2,344 windows.
///
/// <para>
/// Factored out of the gateway client so the concurrency bound is unit-testable without
/// a live MXAccess session.
/// </para>
/// </summary>
internal static class BulkPipeline
{
    /// <summary>
    /// Runs <paramref name="operation"/> for every item with at most
    /// <paramref name="maxParallelism"/> in flight, preserving result order. A per-item
    /// fault is captured into that item's result slot via <paramref name="onError"/>
    /// rather than aborting the pipeline; cancellation propagates.
    /// </summary>
    /// <typeparam name="TItem">Input item type.</typeparam>
    /// <typeparam name="TResult">Per-item result type.</typeparam>
    /// <param name="items">Items to process.</param>
    /// <param name="maxParallelism">Maximum operations in flight; values &lt; 1 are treated as 1.</param>
    /// <param name="operation">The per-item operation.</param>
    /// <param name="onError">Maps an item plus its exception onto a result row.</param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>One result per item, in input order.</returns>
    public static async Task<TResult[]> RunAsync<TItem, TResult>(
        IReadOnlyList<TItem> items,
        int maxParallelism,
        Func<TItem, CancellationToken, Task<TResult>> operation,
        Func<TItem, Exception, TResult> onError,
        CancellationToken ct = default)
    {
        var results = new TResult[items.Count];
        if (items.Count == 0)
            return results;

        // Deliberately NOT disposed: on cancellation Task.WhenAll rethrows while sibling
        // operations may still be unwinding, and a disposed SemaphoreSlim would turn that
        // into an ObjectDisposedException. SemaphoreSlim without AvailableWaitHandle holds
        // no unmanaged resource, so letting it be collected is safe.
        var gate = new SemaphoreSlim(Math.Max(1, maxParallelism));
        var tasks = new Task[items.Count];
        for (var i = 0; i < items.Count; i++)
        {
            var index = i;
            var item = items[i];
            tasks[i] = RunOneAsync(item, index);
        }

        await Task.WhenAll(tasks).ConfigureAwait(false);
        return results;

        async Task RunOneAsync(TItem item, int index)
        {
            await gate.WaitAsync(ct).ConfigureAwait(false);
            try
            {
                results[index] = await operation(item, ct).ConfigureAwait(false);
            }
            catch (OperationCanceledException)
            {
                throw;
            }
            catch (Exception ex)
            {
                results[index] = onError(item, ex);
            }
            finally
            {
                gate.Release();
            }
        }
    }
}
