using System;
using System.Collections.Concurrent;
using System.Threading;
using Jellyfin.Plugin.SRFPlay.Api.Models;
using Microsoft.Extensions.Logging;

namespace Jellyfin.Plugin.SRFPlay.Services;

/// <summary>
/// Service for caching metadata from SRF API.
/// </summary>
public sealed class MetadataCache : IDisposable
{
    private readonly ILogger<MetadataCache> _logger;
    private readonly ConcurrentDictionary<string, CacheEntry<MediaComposition>> _mediaCompositionCache;
    private readonly ReaderWriterLockSlim _lock;
    private bool _disposed;

    /// <summary>
    /// Initializes a new instance of the <see cref="MetadataCache"/> class.
    /// </summary>
    /// <param name="logger">The logger instance.</param>
    public MetadataCache(ILogger<MetadataCache> logger)
    {
        _logger = logger;
        _mediaCompositionCache = new ConcurrentDictionary<string, CacheEntry<MediaComposition>>();
        _lock = new ReaderWriterLockSlim();
    }

    /// <summary>
    /// Disposes resources.
    /// </summary>
    public void Dispose()
    {
        if (!_disposed)
        {
            _lock?.Dispose();
            _disposed = true;
        }
    }

    /// <summary>
    /// Gets cached media composition by URN.
    /// </summary>
    /// <param name="urn">The URN.</param>
    /// <param name="cacheDurationMinutes">The cache duration in minutes.</param>
    /// <returns>The cached media composition, or null if not found or expired.</returns>
    public MediaComposition? GetMediaComposition(string urn, int cacheDurationMinutes)
    {
        if (string.IsNullOrEmpty(urn))
        {
            return null;
        }

        try
        {
            _lock.EnterReadLock();
            try
            {
                if (_mediaCompositionCache.TryGetValue(urn, out var entry))
                {
                    if (entry.IsValid(cacheDurationMinutes))
                    {
                        _logger.LogDebug("Cache hit for URN: {Urn}", urn);
                        return entry.Value;
                    }

                    _logger.LogDebug("Cache entry expired for URN: {Urn}", urn);
                }
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }
        catch (ObjectDisposedException)
        {
            return null;
        }

        return null;
    }

    /// <summary>
    /// Sets media composition in cache.
    /// </summary>
    /// <param name="urn">The URN.</param>
    /// <param name="mediaComposition">The media composition to cache.</param>
    public void SetMediaComposition(string urn, MediaComposition mediaComposition)
    {
        if (string.IsNullOrEmpty(urn) || mediaComposition == null)
        {
            return;
        }

        try
        {
            _lock.EnterWriteLock();
            try
            {
                var entry = new CacheEntry<MediaComposition>(mediaComposition);
                _mediaCompositionCache.AddOrUpdate(urn, entry, (key, oldValue) => entry);
                _logger.LogDebug("Cached media composition for URN: {Urn}", urn);
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }
        catch (ObjectDisposedException)
        {
            // Cache is disposed, ignore
        }
    }

    /// <summary>
    /// Removes media composition from cache.
    /// </summary>
    /// <param name="urn">The URN.</param>
    public void RemoveMediaComposition(string urn)
    {
        if (string.IsNullOrEmpty(urn))
        {
            return;
        }

        try
        {
            _lock.EnterWriteLock();
            try
            {
                if (_mediaCompositionCache.TryRemove(urn, out _))
                {
                    _logger.LogDebug("Removed cached media composition for URN: {Urn}", urn);
                }
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }
        catch (ObjectDisposedException)
        {
            // Cache is disposed, ignore
        }
    }

    /// <summary>
    /// Clears all cached data.
    /// </summary>
    public void Clear()
    {
        try
        {
            _lock.EnterWriteLock();
            try
            {
                _mediaCompositionCache.Clear();
                _logger.LogInformation("Cleared metadata cache");
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }
        catch (ObjectDisposedException)
        {
            // Cache is disposed, ignore
        }
    }

    /// <summary>
    /// Gets the cache statistics.
    /// </summary>
    /// <returns>A tuple with cache count and size estimate.</returns>
    public (int Count, long SizeEstimate) GetStatistics()
    {
        try
        {
            _lock.EnterReadLock();
            try
            {
                var count = _mediaCompositionCache.Count;
                // Rough estimate: average 50KB per entry
                var sizeEstimate = count * 50L * 1024;
                return (count, sizeEstimate);
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }
        catch (ObjectDisposedException)
        {
            return (0, 0);
        }
    }

    /// <summary>
    /// Represents a cached entry with timestamp.
    /// </summary>
    /// <typeparam name="T">The type of cached value.</typeparam>
    private sealed class CacheEntry<T>
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="CacheEntry{T}"/> class.
        /// </summary>
        /// <param name="value">The value to cache.</param>
        public CacheEntry(T value)
        {
            Value = value;
            Timestamp = DateTime.UtcNow;
        }

        /// <summary>
        /// Gets the cached value.
        /// </summary>
        public T Value { get; }

        /// <summary>
        /// Gets the timestamp when the entry was created.
        /// </summary>
        public DateTime Timestamp { get; }

        /// <summary>
        /// Checks if the cache entry is still valid.
        /// </summary>
        /// <param name="cacheDurationMinutes">The cache duration in minutes.</param>
        /// <returns>True if the entry is still valid.</returns>
        public bool IsValid(int cacheDurationMinutes)
        {
            var expirationTime = Timestamp.AddMinutes(cacheDurationMinutes);
            return DateTime.UtcNow < expirationTime;
        }
    }
}
