using NexusShell.Models;

namespace NexusShell.Services.Deployment;

/// <summary>
/// Serial queue — one install/update/remove job at a time.
/// </summary>
public sealed class NexusInstallQueue
{
    private readonly SemaphoreSlim _gate = new(1, 1);
    private readonly Queue<Func<CancellationToken, Task>> _pending = new();
    private int _queueDepth;

    public int QueueDepth => Volatile.Read(ref _queueDepth);

    public event EventHandler<int>? QueueDepthChanged;

    public static TimeSpan DefaultJobTimeout { get; } = TimeSpan.FromMinutes(5);

    public Task<T> EnqueueAsync<T>(
        Func<CancellationToken, Task<T>> work,
        CancellationToken cancellationToken = default) =>
        EnqueueAsync(work, timeout: null, cancellationToken);

    public async Task<T> EnqueueAsync<T>(
        Func<CancellationToken, Task<T>> work,
        TimeSpan? timeout,
        CancellationToken cancellationToken = default)
    {
        var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);

        lock (_pending)
        {
            _queueDepth++;
            QueueDepthChanged?.Invoke(this, _queueDepth);
            _pending.Enqueue(async ct =>
            {
                try
                {
                    var result = await work(ct);
                    tcs.TrySetResult(result);
                }
                catch (OperationCanceledException)
                {
                    tcs.TrySetCanceled(ct);
                }
                catch (Exception ex)
                {
                    tcs.TrySetException(ex);
                }
            });
        }

        _ = ProcessQueueAsync(cancellationToken);

        var effectiveTimeout = timeout ?? DefaultJobTimeout;
        using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        timeoutCts.CancelAfter(effectiveTimeout);

        try
        {
            return await tcs.Task.WaitAsync(timeoutCts.Token);
        }
        catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
        {
            throw new TimeoutException(
                $"Installationskøen overskred timeout ({effectiveTimeout.TotalMinutes:F0} min). Prøv igen.");
        }
    }

    private async Task ProcessQueueAsync(CancellationToken cancellationToken)
    {
        if (!await _gate.WaitAsync(0, cancellationToken))
        {
            return;
        }

        try
        {
            while (true)
            {
                Func<CancellationToken, Task>? job;
                lock (_pending)
                {
                    if (_pending.Count == 0)
                    {
                        break;
                    }

                    job = _pending.Dequeue();
                }

                await job(cancellationToken);

                lock (_pending)
                {
                    _queueDepth--;
                    QueueDepthChanged?.Invoke(this, _queueDepth);
                }
            }
        }
        finally
        {
            _gate.Release();
        }
    }
}
