using Microsoft.EntityFrameworkCore;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace AsbCloudInfrastructure.EfCache
{
#nullable enable
    /// <summary>
    /// Кеширование запросов EF.<br/>
    /// Кеш не отслеживается ChangeTracker.
    /// </summary>
    public static class EfCacheExtensions
    {
        private static readonly Dictionary<string, CacheItem> caches = new(16);        
        private static readonly TimeSpan semaphoreTimeout = TimeSpan.FromSeconds(25);
        private static readonly SemaphoreSlim semaphore = new(1);
        private static readonly TimeSpan minCacheTime = TimeSpan.FromSeconds(2);

        private class CacheItem
        {
            internal IEnumerable? Data;
            internal DateTime DateObsolete;
            internal DateTime DateObsoleteTotal;
            internal readonly SemaphoreSlim semaphore = new(1);

            internal IEnumerable<TEntity> GetData<TEntity>()
            {
                if (Data is IEnumerable<TEntity> typedData)
                    return typedData;
                throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique.");
            }

            internal IEnumerable<TModel> GetData<TEntity, TModel>(Func<TEntity, TModel> convert, int attempt = 1)
            {
                if (Data is IEnumerable<TModel> typedData)
                    return typedData;
                if (Data is IEnumerable<TEntity> typedEntityData)
                {
                    if (semaphore.Wait(0))
                    {
                        try
                        {
                            var convertedData = typedEntityData.Select(convert).ToList();
                            Data = convertedData;
                            return convertedData;
                        }
                        catch
                        {
                            throw;
                        }
                        finally
                        {
                            semaphore.Release();
                        }
                    }
                    else
                    {
                        if (semaphore.Wait(semaphoreTimeout))
                        {
                            semaphore.Release();
                        }
                        else
                        {
                            semaphore.Release();
                            throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while converting cache data");
                        }
                    }
                }
                if(attempt > 0)
                    return GetData(convert, --attempt);
                throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique.");
            }
        }

        private static CacheItem GetOrAddCache(string tag, Func<IEnumerable> valueFactory, TimeSpan obsolete)
        {
            CacheItem cache;
            while (!caches.ContainsKey(tag))
            {
                if (semaphore.Wait(0))
                {
                    try {
                        cache = new CacheItem();

                        var dateObsolete = DateTime.Now + obsolete;
                        var dateQueryStart = DateTime.Now;
                        var data = valueFactory();
                        var queryTime = DateTime.Now - dateQueryStart;

                        if (dateObsolete - DateTime.Now < minCacheTime)
                            dateObsolete = DateTime.Now + minCacheTime;

                        cache.Data = data;
                        cache.DateObsolete = dateObsolete;
                        cache.DateObsoleteTotal = dateObsolete + queryTime + minCacheTime;
                        caches.Add(tag, cache);
                    }
                    catch
                    {
                        throw; 
                    }
                    finally
                    {
                        semaphore.Release();
                    }                                       
                    break;
                }
                else
                {
                    if (semaphore.Wait(semaphoreTimeout))
                    {
                        semaphore.Release();
                    }
                    else
                    {
                        semaphore.Release();
                        throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting cache");
                    }
                }                    
            }

            cache = caches[tag];

            if (cache.DateObsolete < DateTime.Now)
            {
                if (cache.semaphore.Wait(0))
                {
                    try
                    {
                        var dateObsolete = DateTime.Now + obsolete;
                        var dateQueryStart = DateTime.Now;
                        var data = valueFactory();
                        var queryTime = DateTime.Now - dateQueryStart;

                        if (dateObsolete - DateTime.Now < minCacheTime)
                            dateObsolete = DateTime.Now + minCacheTime;

                        cache.Data = data;
                        cache.DateObsolete = dateObsolete;
                        cache.DateObsoleteTotal = dateObsolete + queryTime + minCacheTime;
                    }
                    catch
                    {
                        throw;
                    }
                    finally
                    {
                        cache.semaphore.Release();
                    }
                }
                else if(cache.DateObsoleteTotal < DateTime.Now)
                {
                    if (cache.semaphore.Wait(semaphoreTimeout))
                    {
                        cache.semaphore.Release();
                    }
                    else
                    {
                        cache.semaphore.Release();
                        throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting cache");
                    }
                }                    
            }
            return cache;
        }

        private static async Task<CacheItem> GetOrAddCacheAsync(string tag, Func<CancellationToken, Task<IEnumerable>> valueFactoryAsync, TimeSpan obsolete, CancellationToken token)
        {
            CacheItem cache;
            while (!caches.ContainsKey(tag))
            {
                if (semaphore.Wait(0))
                {
                    try
                    {
                        cache = new CacheItem();

                        var dateObsolete = DateTime.Now + obsolete;
                        var dateQueryStart = DateTime.Now;
                        var data = await valueFactoryAsync(token);
                        var queryTime = DateTime.Now - dateQueryStart;

                        if (dateObsolete - DateTime.Now < minCacheTime)
                            dateObsolete = DateTime.Now + minCacheTime;

                        cache.Data = data;
                        cache.DateObsolete = dateObsolete;
                        cache.DateObsoleteTotal = dateObsolete + queryTime + minCacheTime;
                        caches.Add(tag, cache);
                    }
                    catch
                    {
                        throw;
                    }
                    finally
                    {
                        semaphore.Release();
                    }
                    break;
                }
                else
                {
                    if (await semaphore.WaitAsync(semaphoreTimeout, token))
                    {
                        semaphore.Release();
                    }
                    else
                    {
                        semaphore.Release();
                        throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting cache");
                    }
                }
            }

            cache = caches[tag];

            if (cache.DateObsolete < DateTime.Now)
            {
                if (cache.semaphore.Wait(0))
                {
                    try
                    {
                        var dateObsolete = DateTime.Now + obsolete;
                        var dateQueryStart = DateTime.Now;
                        var data = await valueFactoryAsync(token);
                        var queryTime = DateTime.Now - dateQueryStart;

                        if (dateObsolete - DateTime.Now < minCacheTime)
                            dateObsolete = DateTime.Now + minCacheTime;

                        cache.Data = data;
                        cache.DateObsolete = dateObsolete;
                        cache.DateObsoleteTotal = dateObsolete + queryTime + minCacheTime;
                    }
                    catch
                    {
                        throw;
                    }
                    finally
                    {
                        cache.semaphore.Release();
                    }
                }
                else if (cache.DateObsoleteTotal < DateTime.Now)
                {
                    if (await cache.semaphore.WaitAsync(semaphoreTimeout, token))
                    {
                        cache.semaphore.Release();
                    }
                    else
                    {
                        cache.semaphore.Release();
                        throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting updated cache");
                    }
                }
            }
            return cache;
        }

        /// <summary>
        /// Кешировать запрос в List&lt;<typeparamref name="TEntity"/>&gt;.
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="query"></param>
        /// <param name="tag">Метка кеша</param>
        /// <param name="obsolescence">Период устаревания данных</param>
        /// <returns></returns>
        public static IEnumerable<TEntity> FromCache<TEntity>(this IQueryable<TEntity> query, string tag, TimeSpan obsolescence)
            where TEntity : class
        {
            IEnumerable factory() => query.AsNoTracking().ToList();
            var cache = GetOrAddCache(tag, factory, obsolescence);
            return cache.GetData<TEntity>();
        }

        /// <summary>
        /// Кешировать запрос с последующим преобразованием из <typeparamref name="TEntity"/> в <typeparamref name="TModel"/>.<br/>
        /// Преобразование выполняется после получения из БД, результат кешируется в List&lt;<typeparamref name="TEntity"/>&gt;.
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <typeparam name="TModel"></typeparam>
        /// <param name="query"></param>
        /// <param name="tag">Метка кеша</param>
        /// <param name="obsolescence">Период устаревания данных</param>
        /// <param name="convert">Преобразование данных БД в DTO</param>
        /// <returns></returns>
        public static IEnumerable<TModel> FromCache<TEntity, TModel>(this IQueryable<TEntity> query, string tag, TimeSpan obsolescence, Func<TEntity, TModel> convert)
            where TEntity : class
        {
            IEnumerable factory () => query.AsNoTracking().ToList();
            var cache = GetOrAddCache(tag, factory, obsolescence);
            return cache.GetData(convert);
        }

        /// <summary>
        /// Асинхронно кешировать запрос в List&lt;<typeparamref name="TEntity"/>&gt;.<br/>
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="query"></param>
        /// <param name="tag">Метка кеша</param>
        /// <param name="obsolescence">Период устаревания данных</param>
        /// <param name="token"></param>
        /// <returns></returns>
        public static async Task<IEnumerable<TEntity>> FromCacheAsync<TEntity>(this IQueryable<TEntity> query, string tag, TimeSpan obsolescence, CancellationToken token = default)
            where TEntity : class
        {
            async Task<IEnumerable> factory(CancellationToken token)
                => await query.AsNoTracking().ToListAsync(token);
            var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token);
            return cache.GetData<TEntity>();
        }

        /// <summary>
        /// Асинхронно кешировать запрос с последующим преобразованием из <typeparamref name="TEntity"/> в <typeparamref name="TModel"/>.<br/>
        /// Преобразование выполняется после получения из БД, результат кешируется в List&lt;<typeparamref name="TModel"/>&gt;.
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <typeparam name="TModel"></typeparam>
        /// <param name="query"></param>
        /// <param name="tag">Метка кеша</param>
        /// <param name="obsolescence">Период устаревания данных</param>
        /// <param name="convert">Преобразование данных БД в DTO</param>
        /// <param name="token"></param>
        /// <returns></returns>
        public static async Task<IEnumerable<TModel>> FromCacheAsync<TEntity, TModel>(this IQueryable<TEntity> query, string tag, TimeSpan obsolescence, Func<TEntity, TModel> convert, CancellationToken token = default)
            where TEntity : class
        {
            async Task<IEnumerable> factory(CancellationToken token)
                => await query.AsNoTracking().ToListAsync(token);            
            var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token);
            return cache.GetData(convert);
        }

        /// <summary>
        /// Очистить кеш
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="query"></param>
        /// <param name="tag">Метка кеша</param>
        public static void DropCache<T>(this IQueryable<T> query, string tag)
        {
            caches.Remove(tag, out var _);
        }
    }
#nullable disable
}