namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;

/// <summary>
/// WP3.1 (finding #4): the process-wide concurrency gate for trigger-expression evaluation.
///
/// <para>Before WP3.1, <c>ScriptActor</c> and <c>AlarmActor</c> evaluated their compiled
/// Expression triggers on the <see cref="ScriptExecutionScheduler"/> — the same bounded pool
/// of dedicated threads that runs script bodies. Eight scripts blocked in synchronous I/O
/// therefore stalled EVERY Expression trigger on the node behind them, for an unbounded time,
/// and the evaluation's own 2 s timeout did not even start until it was dequeued. An alarm
/// that should have raised in milliseconds simply never raised.</para>
///
/// <para>Trigger expressions are non-blocking <em>by construction</em>:
/// <see cref="TriggerExpressionGlobals"/> exposes only reads over an in-memory snapshot
/// dictionary, and the script trust gate has already denied I/O, network, threading, and
/// reflection long before the expression can deploy. They are short CPU-bound work — exactly
/// what the shared .NET thread pool is for. So they run there, and this semaphore is the only
/// thing bounding their fan-out. A second dedicated pool was considered and rejected: it would
/// add threads, gauges, and a second starvation surface for no isolation gain.</para>
///
/// <para>Per-trigger coalescing in the actors (one evaluation in flight, one pending) already
/// caps waiters at one per Expression trigger, so this gate's queue is bounded by trigger
/// count.</para>
///
/// Mirrors <see cref="ScriptExecutionScheduler.Shared"/>'s lazy-singleton plus injectable-seam
/// shape so tests can drive a gate of size 1 deterministically.
/// </summary>
public sealed class TriggerEvalGate : IDisposable
{
    private readonly SemaphoreSlim _gate;

    private static volatile TriggerEvalGate? _shared;
    private static readonly object SharedLock = new();

    /// <summary>Creates a gate admitting <paramref name="maxConcurrency"/> concurrent evaluations.</summary>
    /// <param name="maxConcurrency">Maximum concurrent trigger-expression evaluations; values below 1 are clamped to 1.</param>
    public TriggerEvalGate(int maxConcurrency)
    {
        MaxConcurrency = Math.Max(1, maxConcurrency);
        _gate = new SemaphoreSlim(MaxConcurrency, MaxConcurrency);
    }

    /// <summary>The configured concurrency limit.</summary>
    public int MaxConcurrency { get; }

    /// <summary>Free permits right now; 0 means the gate is saturated and new evaluations will queue.</summary>
    public int AvailablePermits => _gate.CurrentCount;

    /// <summary>
    /// The process-wide gate, used when no gate is injected. Lazily created from
    /// <see cref="SiteRuntimeOptions.TriggerEvalMaxConcurrency"/>; the first caller wins.
    /// </summary>
    /// <param name="options">Site runtime options supplying the concurrency limit.</param>
    /// <returns>The shared gate instance.</returns>
    public static TriggerEvalGate Shared(SiteRuntimeOptions options)
    {
        var existing = _shared;
        if (existing is not null) return existing;

        lock (SharedLock)
        {
            return _shared ??= new TriggerEvalGate(options.TriggerEvalMaxConcurrency);
        }
    }

    /// <summary>
    /// Waits for a permit. The caller passes the evaluation's own deadline token, which was
    /// armed at ENQUEUE — so time spent queueing here burns the same budget the evaluation
    /// itself would, and a saturated gate produces a timely cancellation (treated as
    /// <see langword="false"/> by both actors) instead of an unbounded stall.
    /// </summary>
    /// <param name="cancellationToken">The evaluation's deadline token.</param>
    /// <returns>A task that completes when a permit is acquired.</returns>
    public Task WaitAsync(CancellationToken cancellationToken) => _gate.WaitAsync(cancellationToken);

    /// <summary>Returns a permit. Must be called exactly once per successful <see cref="WaitAsync"/>.</summary>
    public void Release() => _gate.Release();

    /// <inheritdoc />
    public void Dispose() => _gate.Dispose();
}
