using System.Collections;
using EasyCaching.Core;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using XF.Domain.Cache;

namespace XF.EasyCaching
{
    public class DefaultTypedCache<TValue> : ITypedCache<TValue>
    {
        private readonly IServiceScopeFactory _serviceScopeFactory;
        private readonly IHybridCachingProvider _hybridCaching;
        private readonly IRedisCachingProvider _redisCaching;
        private readonly IEasyCachingProvider _caching;
        private readonly TimeSpan _expirationDefault = TimeSpan.FromDays(1);

        public DefaultTypedCache(
            IServiceScopeFactory serviceScopeFactory,
            IHybridCachingProvider hybridCaching,
            IRedisCachingProvider redisCaching,
            IEasyCachingProvider caching
            )
        {
            _serviceScopeFactory = serviceScopeFactory;
            _hybridCaching = hybridCaching;
            _redisCaching = redisCaching;
            _caching = caching;
        }

        public void Set(string key, TValue value, TimeSpan? expiration = null) =>
            _hybridCaching.Set(CombinePrefix(key), value, expiration.GetValueOrDefault(_expirationDefault));

        public async Task SetAsync(string key, TValue value, TimeSpan? expiration = null, CancellationToken cancellationToken = default) =>
            await _hybridCaching.SetAsync(CombinePrefix(key), value, expiration.GetValueOrDefault(_expirationDefault), cancellationToken);

        public TValue? Get(string key) => _hybridCaching.Get<TValue>(CombinePrefix(key)).Value;

        public async Task<TValue> GetAsync(string key, CancellationToken cancellationToken) =>
            (await _hybridCaching.GetAsync<TValue>(CombinePrefix(key), cancellationToken)).Value;

        public void Remove(string key) => _hybridCaching.Remove(CombinePrefix(key));

        public async Task RemoveAsync(string key, CancellationToken cancellationToken) =>
            await _hybridCaching.RemoveAsync(CombinePrefix(key), cancellationToken);

        public bool Exists(string key) => _hybridCaching.Exists(CombinePrefix(key));

        public async Task<bool> ExistsAsync(string key, CancellationToken cancellationToken) =>
            await _hybridCaching.ExistsAsync(CombinePrefix(key), cancellationToken);

        public bool TrySet(string key, TValue value, TimeSpan? expiration = null) =>
            _hybridCaching.TrySet(CombinePrefix(key), value, expiration.GetValueOrDefault(_expirationDefault));

        public async Task<bool> TrySetAsync(string key, TValue value, TimeSpan? expiration = null, CancellationToken cancellationToken = default) =>
            await _hybridCaching.TrySetAsync(CombinePrefix(key), value, expiration.GetValueOrDefault(_expirationDefault), cancellationToken);

        public void SetAll(IDictionary<string, TValue> values, TimeSpan? expiration = null)
        {
            var newDic = new Dictionary<string, TValue>();
            foreach (var kvp in values)
            {
                newDic.Add(CombinePrefix(kvp.Key), kvp.Value);
            }
            _hybridCaching.SetAll(newDic, expiration.GetValueOrDefault(_expirationDefault));
        }

        public async Task SetAllAsync(IDictionary<string, TValue> values, TimeSpan? expiration = null, CancellationToken cancellationToken = default)
        {
            var newDic = new Dictionary<string, TValue>();
            foreach (var kvp in values)
            {
                newDic.Add(CombinePrefix(kvp.Key), kvp.Value);
            }

            await _hybridCaching.SetAllAsync(newDic, expiration.GetValueOrDefault(_expirationDefault), cancellationToken);
        }

        public IDictionary<string, TValue> GetByPrefix()
        {
            var cacheValueDic = _caching.GetByPrefix<TValue>(CreateRegion());
            if (cacheValueDic == null) return new Dictionary<string, TValue>();
            return cacheValueDic.ToDictionary(cacheValue => cacheValue.Key, cacheValue => cacheValue.Value.Value);
        }

        public async Task<IDictionary<string, TValue>> GetByPrefixAsync(CancellationToken cancellationToken)
        {
            var cacheValueDic = await _caching.GetByPrefixAsync<TValue>(CreateRegion(), cancellationToken);
            if (cacheValueDic == null) return new Dictionary<string, TValue>();
            return cacheValueDic.ToDictionary(cacheValue => cacheValue.Key, cacheValue => cacheValue.Value.Value);
        }

        public IReadOnlyList<TValue> GetListByPrefix(string? prefix = null)
        {
            var cacheValueDic = _caching.GetByPrefix<TValue>(CreateRegion() + prefix);
            if (cacheValueDic == null) return new List<TValue>();
            return cacheValueDic.Values.Select(d => d.Value).ToList();
        }

        public async Task<IReadOnlyList<TValue>> GetListByPrefixAsync(CancellationToken cancellationToken)
        {
            var cacheValueDic = await _caching.GetByPrefixAsync<TValue>(CreateRegion(), cancellationToken);
            if (cacheValueDic == null) return new List<TValue>();
            return cacheValueDic.Values.Select(d => d.Value).ToList();
        }

        /// <summary>
        /// get the cache value, if cache not exists, set the value to cache then return
        /// </summary>
        public TValue GetOrSet(string key, TValue value, TimeSpan? expiration = null)
        {
            var cacheValue = Get(key);
            if (cacheValue != null) return cacheValue;

            Set(key, value, expiration);
            return value;

        }

        public TValue? GetOrSet(string key, Func<string, TValue> valueFactory, TimeSpan? expiration = null)
        {
            var cacheValue = Get(key);
            if (cacheValue != null) return cacheValue;

            var value = valueFactory.Invoke(key);
            if (value == null) return default;
            Set(key, value, expiration);
            return value;
        }

        public async Task<TValue> GetOrSetAsync(string key, TValue value, TimeSpan? expiration = null, CancellationToken cancellationToken = default)
        {
            var cacheValue = await GetAsync(key, cancellationToken);
            if (cacheValue != null) return cacheValue;

            await SetAsync(key, value, expiration, cancellationToken);
            return value;
        }

        public async Task<TValue?> GetOrSetAsync(string key, Func<string, TValue> valueFactory, TimeSpan? expiration = null, CancellationToken cancellationToken = default)
        {
            var cacheValue = await GetAsync(key, cancellationToken);
            if (cacheValue != null) return cacheValue;

            var value = valueFactory.Invoke(key);
            if (value == null) return default;
            await SetAsync(key, value, expiration, cancellationToken);
            return value;
        }

        public string CombinePrefix(string key) => CreateRegion() + key;


        private string CreateRegion()
        {
            var valueType = typeof(TValue);
            //if (valueType.IsGenericType)
            //{
            //    throw new NotSupportedException("TCachedValue can not be a Generic Type");
            //}

            //if (valueType == typeof(string))
            //{
            //    throw new NotSupportedException("TCachedValue can not be a string");
            //}

            if (string.IsNullOrWhiteSpace(valueType.FullName))
            {
                throw new NotSupportedException("FullName of type TCachedValue can not be null");
            }

            using var scope = _serviceScopeFactory.CreateScope();
            var options = scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<CacheOptions>>();
            var prefix = options.Value.Prefix;

            //if (valueType.TryGetCustomAttribute<CacheAttribute>(false, out var attr))
            //{
            //    if (!string.IsNullOrEmpty(attr.Region))
            //    {
            //        return attr.Region;
            //    }
            //}

            string region;

            if (valueType.IsGenericType && valueType.GetInterfaces().Any(d => d == typeof(IEnumerable)))
            {
                region = "Collection";
            }
            else
            {
                region = valueType.FullName.Replace('.', ':');
            }

            if (!string.IsNullOrEmpty(prefix))
            {
                prefix += ':';
                if (!region.StartsWith(prefix))
                {
                    region = prefix + region;
                }
            }
            return region;
        }
    }
}