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.
    /// Кеш не отслеживается ChangeTracker.
    /// </summary>
    public static class EfCacheDictionaryExtensions
    {
        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 IDictionary? Data;
            internal DateTime DateObsolete;
            internal DateTime DateObsoleteTotal;
            internal readonly SemaphoreSlim semaphore = new(1);
        }

        private static CacheItem GetOrAddCache(string tag, Func<IDictionary> 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<IDictionary>> 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>
        /// Кешировать запрос в Dictionary&lt;<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>&gt;.
        /// </summary>
        /// <typeparam name="TKey">тип ключа</typeparam>
        /// <typeparam name="TEntity">тип значения</typeparam>
        /// <param name="query"></param>
        /// <param name="tag">Метка кеша</param>
        /// <param name="obsolescence">Период устаревания данных</param>
        /// <param name="keySelector">Делегат получения ключа из записи</param>
        /// <returns></returns>
        public static Dictionary<TKey, TEntity> FromCacheDictionary<TKey, TEntity>(
            this IQueryable<TEntity> query,
            string tag,
            TimeSpan obsolescence,
            Func<TEntity, TKey> keySelector)
            where TEntity : class
            where TKey : notnull
        {
            IDictionary factory()
            {
                var queryData = query.AsNoTracking()
                    .ToDictionary(keySelector);
                return queryData;
            }
            var cache = GetOrAddCache(tag, factory, obsolescence);
            if (cache.Data is Dictionary<TKey, TEntity> typedData)
                return typedData;
            throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique.");
        }

        /// <summary>
        /// Кешировать запрос с последующим преобразованием из <typeparamref name="TEntity"/> в <typeparamref name="TModel"/>.<br/>
        /// Преобразование выполняется после получения из БД, результат кешируется в Dictionary&lt;<typeparamref name="TKey"/>, <typeparamref name="TModel"/>&gt;.
        /// </summary>
        /// <typeparam name="TKey">тип ключа</typeparam>
        /// <typeparam name="TEntity">тип значения</typeparam>
        /// <typeparam name="TModel"></typeparam>
        /// <param name="query"></param>
        /// <param name="tag">Метка кеша</param>
        /// <param name="obsolescence">Период устаревания данных</param>
        /// <param name="keySelector">Делегат получения ключа из записи</param>
        /// <param name="convert">Преобразование данных БД в DTO</param>
        /// <returns></returns>
        public static Dictionary<TKey, TModel> FromCacheDictionary<TKey, TEntity, TModel>(
            this IQueryable<TEntity> query,
            string tag,
            TimeSpan obsolescence,
            Func<TEntity, TKey> keySelector,
            Func<TEntity, TModel> convert)
            where TEntity : class
            where TKey : notnull
        {
            IDictionary factory()
            {
                var queryData = query.AsNoTracking()
                    .ToList();
                var data = queryData
                    .ToDictionary(keySelector, convert);
                return data;
            }
            var cache = GetOrAddCache(tag, factory, obsolescence);
            if (cache.Data is Dictionary<TKey, TModel> typedData)
                return typedData;
            throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique.");
        }

        /// <summary>
        /// Асинхронно кешировать запрос в Dictionary&lt;<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>&gt;.
        /// </summary>
        /// <typeparam name="TKey">тип ключа</typeparam>
        /// <typeparam name="TEntity">тип значения</typeparam>
        /// <param name="query"></param>
        /// <param name="tag">Метка кеша</param>
        /// <param name="obsolescence">Период устаревания данных</param>
        /// <param name="keySelector">Делегат получения ключа из записи</param>
        /// <param name="token"></param>
        /// <returns></returns>
        public static async Task<Dictionary<TKey, TEntity>> FromCacheDictionaryAsync<TKey, TEntity>(
            this IQueryable<TEntity> query, 
            string tag, 
            TimeSpan obsolescence, 
            Func<TEntity, TKey> keySelector, 
            CancellationToken token = default)
            where TEntity : class
            where TKey : notnull
        {
            async Task<IDictionary> factory(CancellationToken token)
            {
                var queryData = await query.AsNoTracking().ToDictionaryAsync(keySelector, token);
                return queryData;
            }
            var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token);

            if (cache.Data is Dictionary<TKey, TEntity> typedData)
                return typedData;
            throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique.");
        }

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

            if (cache.Data is Dictionary<TKey, TModel> typedData)
                return typedData;
            throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique.");
        }

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