using System.Collections.Concurrent;

namespace ZB.MOM.WW.ScadaBridge.DeploymentManager;

/// <summary>
/// Per-instance operation lock. Only one mutating operation (deploy, disable, enable, delete)
/// may be in progress per instance at a time. Different instances can proceed in parallel.
///
/// Implementation: ConcurrentDictionary of ref-counted SemaphoreSlim(1,1) keyed by instance
/// unique name. The lock is released on completion, timeout, or failure.
/// Lost on central failover (acceptable per design -- in-progress treated as failed).
///
/// Each entry is ref-counted. The semaphore is created on the
/// first acquire/wait, shared while there are waiters or a holder, and removed +
/// <see cref="IDisposable.Dispose"/>d when the last reference is released — so the dictionary
/// does not accumulate one kernel wait handle per distinct instance name forever.
/// </summary>
public class OperationLockManager
{
    private readonly object _gate = new();
    private readonly Dictionary<string, LockEntry> _locks = new(StringComparer.Ordinal);

    /// <summary>
    /// Number of lock entries currently tracked. Used for diagnostics and to
    /// verify that semaphores are reclaimed.
    /// </summary>
    public int TrackedLockCount
    {
        get
        {
            lock (_gate)
            {
                return _locks.Count;
            }
        }
    }

    /// <summary>
    /// Acquires the operation lock for the given instance. Returns a disposable that releases the lock.
    /// Throws TimeoutException if the lock cannot be acquired within the timeout.
    /// </summary>
    /// <param name="instanceUniqueName">The unique name of the instance to lock.</param>
    /// <param name="timeout">Maximum time to wait for the lock before throwing <see cref="TimeoutException"/>.</param>
    /// <param name="cancellationToken">Cancellation token to abort the wait.</param>
    /// <returns>An <see cref="IDisposable"/> that releases the lock when disposed.</returns>
    public async Task<IDisposable> AcquireAsync(string instanceUniqueName, TimeSpan timeout, CancellationToken cancellationToken = default)
    {
        // Reserve a reference (creating the entry if needed) BEFORE waiting, so a
        // concurrent waiter for the same instance shares the same semaphore and
        // the entry survives until every waiter/holder has released it.
        LockEntry entry;
        lock (_gate)
        {
            if (!_locks.TryGetValue(instanceUniqueName, out entry!))
            {
                entry = new LockEntry();
                _locks[instanceUniqueName] = entry;
            }
            entry.RefCount++;
        }

        try
        {
            if (!await entry.Semaphore.WaitAsync(timeout, cancellationToken))
            {
                throw new TimeoutException(
                    $"Could not acquire operation lock for instance '{instanceUniqueName}' within {timeout.TotalSeconds}s. " +
                    "Another mutating operation is in progress.");
            }
        }
        catch (Exception) when (DropReferenceOnFailure(instanceUniqueName, entry))
        {
            // DropReferenceOnFailure always returns false; the filter just runs
            // the cleanup so the reservation is not leaked when WaitAsync throws
            // or times out (TimeoutException / OperationCanceledException). The
            // exception still propagates. The semaphore was NOT entered on any
            // of these paths, so only the reference is dropped.
            throw;
        }

        return new LockRelease(this, instanceUniqueName, entry);
    }

    /// <summary>
    /// Checks whether a lock is currently held for the given instance (for diagnostics).
    /// </summary>
    /// <param name="instanceUniqueName">The unique name of the instance to check.</param>
    /// <returns><c>true</c> if a lock is currently held; otherwise <c>false</c>.</returns>
    public bool IsLocked(string instanceUniqueName)
    {
        lock (_gate)
        {
            return _locks.TryGetValue(instanceUniqueName, out var entry) && entry.Semaphore.CurrentCount == 0;
        }
    }

    private bool DropReferenceOnFailure(string instanceUniqueName, LockEntry entry)
    {
        ReleaseReference(instanceUniqueName, entry, semaphoreWasEntered: false);
        return false;
    }

    /// <summary>
    /// Drops one reference to the entry. When <paramref name="semaphoreWasEntered"/>
    /// is true the semaphore is released first. When the reference count reaches
    /// zero the entry is removed from the dictionary and the semaphore disposed.
    /// </summary>
    private void ReleaseReference(string instanceUniqueName, LockEntry entry, bool semaphoreWasEntered)
    {
        lock (_gate)
        {
            // Release the semaphore (handing the lock to any waiter) and drop the
            // reference under the same gate, so the dispose decision below cannot
            // race with the Release on an entry that another caller is reclaiming.
            if (semaphoreWasEntered)
            {
                entry.Semaphore.Release();
            }

            entry.RefCount--;
            if (entry.RefCount <= 0 &&
                _locks.TryGetValue(instanceUniqueName, out var current) &&
                ReferenceEquals(current, entry))
            {
                _locks.Remove(instanceUniqueName);
                entry.Semaphore.Dispose();
            }
        }
    }

    private sealed class LockEntry
    {
        public readonly SemaphoreSlim Semaphore = new(1, 1);

        /// <summary>Number of in-flight acquires (waiters + the current holder). Guarded by <see cref="_gate"/>.</summary>
        public int RefCount;
    }

    private sealed class LockRelease : IDisposable
    {
        private readonly OperationLockManager _owner;
        private readonly string _instanceUniqueName;
        private readonly LockEntry _entry;
        private int _disposed;

        /// <summary>Initializes the release handle with its owner, instance name, and lock entry.</summary>
        /// <param name="owner">The owning <see cref="OperationLockManager"/>.</param>
        /// <param name="instanceUniqueName">The instance unique name whose lock is held.</param>
        /// <param name="entry">The ref-counted semaphore entry to release on dispose.</param>
        public LockRelease(OperationLockManager owner, string instanceUniqueName, LockEntry entry)
        {
            _owner = owner;
            _instanceUniqueName = instanceUniqueName;
            _entry = entry;
        }

        /// <summary>Releases the operation lock idempotently.</summary>
        public void Dispose()
        {
            if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0)
            {
                _owner.ReleaseReference(_instanceUniqueName, _entry, semaphoreWasEntered: true);
            }
        }
    }
}
