using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using AdvChkSys.Interfaces;

namespace AdvChkSys.Threading
{
    /// <summary>
    /// Provides asynchronous locking for chunks.
    /// </summary>
    public class ChunkAsyncLock : IDisposable
    {
        // Lock objects for each chunk
        private readonly ConcurrentDictionary<IChunk, SemaphoreSlim> _locks = new();

        // Timer for cleanup
        private readonly Timer _cleanupTimer;

        // Last access time for each lock
        private readonly ConcurrentDictionary<IChunk, DateTime> _lastAccessTime = new();

        // Cleanup interval in minutes
        private readonly int _cleanupIntervalMinutes;

        /// <summary>
        /// Initializes a new instance of the ChunkAsyncLock class.
        /// </summary>
        /// <param name="cleanupIntervalMinutes">Interval in minutes for cleaning up unused locks</param>
        public ChunkAsyncLock(int cleanupIntervalMinutes = 10)
        {
            _cleanupIntervalMinutes = cleanupIntervalMinutes;
            _cleanupTimer = new Timer(CleanupUnusedLocks, null,
                TimeSpan.FromMinutes(cleanupIntervalMinutes),
                TimeSpan.FromMinutes(cleanupIntervalMinutes));
        }

        /// <summary>
        /// Acquires a lock on a chunk asynchronously.
        /// </summary>
        /// <param name="chunk">The chunk to lock</param>
        /// <param name="cancellationToken">Cancellation token</param>
        /// <returns>A disposable that releases the lock when disposed</returns>
        public async Task<IDisposable> LockAsync(IChunk chunk, CancellationToken cancellationToken = default)
        {
            var semaphore = _locks.GetOrAdd(chunk, _ => new SemaphoreSlim(1, 1));
            _lastAccessTime[chunk] = DateTime.UtcNow;

            try
            {
                await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
            }
            catch (OperationCanceledException)
            {
                // If canceled, check if we need to clean up the semaphore
                if (semaphore.CurrentCount == 1)
                {
                    // No one is waiting, try to remove
                    _locks.TryRemove(chunk, out _);
                    _lastAccessTime.TryRemove(chunk, out _);
                    semaphore.Dispose();
                }

                throw;
            }

            return new LockReleaser(semaphore, chunk, this);
        }

        /// <summary>
        /// Tries to acquire a lock on a chunk asynchronously with a timeout.
        /// </summary>
        /// <param name="chunk">The chunk to lock</param>
        /// <param name="timeout">Timeout for acquiring the lock</param>
        /// <param name="cancellationToken">Cancellation token</param>
        /// <returns>A tuple with a boolean indicating success and the lock releaser if successful</returns>
        public async Task<(bool Success, IDisposable? LockReleaser)> TryLockAsync(
            IChunk chunk,
            TimeSpan timeout,
            CancellationToken cancellationToken = default)
        {
            if (chunk == null)
                throw new ArgumentNullException(nameof(chunk));

            var semaphore = _locks.GetOrAdd(chunk, _ => new SemaphoreSlim(1, 1));
            _lastAccessTime[chunk] = DateTime.UtcNow;

            if (await semaphore.WaitAsync(timeout, cancellationToken).ConfigureAwait(false))
            {
                return (true, new LockReleaser(semaphore, chunk, this));
            }

            return (false, null);
        }

        /// <summary>
        /// Cleans up unused locks.
        /// </summary>
        private void CleanupUnusedLocks(object? state)
        {
            var now = DateTime.UtcNow;
            var threshold = now.AddMinutes(-_cleanupIntervalMinutes);

            foreach (var chunk in _lastAccessTime.Keys)
            {
                if (_lastAccessTime.TryGetValue(chunk, out var lastAccess) &&
                    lastAccess < threshold &&
                    _locks.TryGetValue(chunk, out var semaphore))
                {
                    // Only remove if no one is waiting
                    if (semaphore.CurrentCount == 1)
                    {
                        if (_locks.TryRemove(chunk, out var removedSemaphore))
                        {
                            _lastAccessTime.TryRemove(chunk, out _);
                            removedSemaphore.Dispose();
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Disposes resources.
        /// </summary>
        public void Dispose()
        {
            _cleanupTimer.Dispose();

            foreach (var semaphore in _locks.Values)
            {
                semaphore.Dispose();
            }

            _locks.Clear();
            _lastAccessTime.Clear();
        }

        /// <summary>
        /// Releases a lock when disposed.
        /// </summary>
        private class LockReleaser : IDisposable
        {
            private readonly SemaphoreSlim _semaphore;
            private readonly IChunk _chunk;
            private readonly ChunkAsyncLock _parent;
            private bool _disposed;

            public LockReleaser(SemaphoreSlim semaphore, IChunk chunk, ChunkAsyncLock parent)
            {
                _semaphore = semaphore;
                _chunk = chunk;
                _parent = parent;
            }

            public void Dispose()
            {
                if (!_disposed)
                {
                    _semaphore.Release();
                    _parent._lastAccessTime[_chunk] = DateTime.UtcNow;
                    _disposed = true;
                }
            }
        }
    }
}