using System.Collections.Concurrent;

namespace ZB.MOM.WW.OtOpcUa.Server.Redundancy;

/// <summary>
///     Tracks in-progress publish-generation apply leases keyed on
///     <c>(ConfigGenerationId, PublishRequestId)</c>. Per decision #162 a sealed lease pattern
///     ensures <see cref="IsApplyInProgress"/> reflects every exit path (success / exception /
///     cancellation) because the IAsyncDisposable returned by <see cref="BeginApplyLease"/>
///     decrements unconditionally.
/// </summary>
/// <remarks>
///     A watchdog loop calls <see cref="PruneStale"/> periodically with the configured
///     <see cref="ApplyMaxDuration"/>; any lease older than that is force-closed so a crashed
///     publisher can't pin the node at <see cref="ServiceLevelBand.PrimaryMidApply"/>.
/// </remarks>
public sealed class ApplyLeaseRegistry
{
    private readonly ConcurrentDictionary<LeaseKey, DateTime> _leases = new();
    private readonly TimeProvider _timeProvider;

    public TimeSpan ApplyMaxDuration { get; }

    public ApplyLeaseRegistry(TimeSpan? applyMaxDuration = null, TimeProvider? timeProvider = null)
    {
        ApplyMaxDuration = applyMaxDuration ?? TimeSpan.FromMinutes(10);
        _timeProvider = timeProvider ?? TimeProvider.System;
    }

    /// <summary>
    ///     Register a new lease. Returns an <see cref="IAsyncDisposable"/> whose disposal
    ///     decrements the registry; use <c>await using</c> in the caller so every exit path
    ///     closes the lease.
    /// </summary>
    public IAsyncDisposable BeginApplyLease(long generationId, Guid publishRequestId)
    {
        var key = new LeaseKey(generationId, publishRequestId);
        _leases[key] = _timeProvider.GetUtcNow().UtcDateTime;
        return new LeaseScope(this, key);
    }

    /// <summary>True when at least one apply lease is currently open.</summary>
    public bool IsApplyInProgress => !_leases.IsEmpty;

    /// <summary>Current open-lease count — diagnostics only.</summary>
    public int OpenLeaseCount => _leases.Count;

    /// <summary>Force-close any lease older than <see cref="ApplyMaxDuration"/>. Watchdog tick.</summary>
    /// <returns>Number of leases the watchdog closed on this tick.</returns>
    public int PruneStale()
    {
        var now = _timeProvider.GetUtcNow().UtcDateTime;
        var closed = 0;
        foreach (var kv in _leases)
        {
            if (now - kv.Value > ApplyMaxDuration && _leases.TryRemove(kv.Key, out _))
                closed++;
        }
        return closed;
    }

    private void Release(LeaseKey key) => _leases.TryRemove(key, out _);

    private readonly record struct LeaseKey(long GenerationId, Guid PublishRequestId);

    private sealed class LeaseScope : IAsyncDisposable
    {
        private readonly ApplyLeaseRegistry _owner;
        private readonly LeaseKey _key;
        private int _disposed;

        public LeaseScope(ApplyLeaseRegistry owner, LeaseKey key)
        {
            _owner = owner;
            _key = key;
        }

        public ValueTask DisposeAsync()
        {
            if (Interlocked.Exchange(ref _disposed, 1) == 0)
                _owner.Release(_key);
            return ValueTask.CompletedTask;
        }
    }
}
