using OnlineSalesAutoCrop.CoreAPI.Models;
using OnlineSalesAutoCrop.CoreAPI.Models.Global;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using StackExchange.Redis;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;

namespace OnlineSalesAutoCrop.CoreAPI
{
	/// <summary>
	/// Customized cache, it may be "Distributed Cache (Redis)" or "Memory Cache",
	/// it depends on value in appsettings.json file.    
	/// If the value of "RedisCacheUrl" field is present it will use "Distributed Cache (Redis)."
	/// If the value of "SqlServerCacheUrl" field is present it will use "Distributed Cache (Sql Server)"
	/// otherwise it will use "Memory Cache". 
	/// </summary>
	public interface IEaseCache : IEnumerable<KeyValuePair<string, object>>
	{
		/// <summary>
		/// Get cached object
		/// </summary>
		/// <typeparam name="T">Any type of object</typeparam>
		/// <param name="key">Key to retrieve data</param>
		/// <param name="key2">2nd part of Key to retrieve data</param>
		/// <param name="value">Instance of an object type T</param>
		/// <returns>True if successful otherwise false</returns>
		bool TryGetValue<T>(string key, string key2, out T value);

		/// <summary>
		/// Set object to cache
		/// </summary>
		/// <typeparam name="T">Any type of object</typeparam>
		/// <param name="key">Key to set data to cache</param>
		/// <param name="key2">2nd part of Key to retrieve data</param>
		/// <param name="value">Instance of an object type T</param>
		/// <param name="options">How many time to keep in cache</param>
		/// <returns>True if successful otherwise false</returns>
		bool Set<T>(string key, string key2, T value, DateTimeOffset options);

		/// <summary>
		/// Delete caches of matched pattern
		/// </summary>
		/// <param name="pattern">Pattern to search</param>
		void Clear(string pattern);
	}

	/// <summary>
	/// 
	/// </summary>
	public class EaseCache : IEaseCache
	{
		private readonly IDatabase _db;
		private readonly IServer _server;
		private readonly EnumCacheType _cacheType;
		private readonly IMemoryCache _memoryCache;
		private readonly IDistributedCache _distributedCache;
		private readonly ConcurrentBag<string> _distributedKeys = [];
		private readonly ConcurrentDictionary<string, ICacheEntry> _cacheEntries = new();

		/// <summary>
		/// 
		/// </summary>
		/// <param name="memoryCache"></param>
		/// <param name="distributedCache"></param>
		/// <param name="settings"></param>
		public EaseCache(IMemoryCache memoryCache, IDistributedCache distributedCache, IOptions<AppSettings> settings)
		{
			CacheConnection cacheCon = new(settings: settings.Value);

			_cacheType = cacheCon.CacheType;
			if (_cacheType == EnumCacheType.Redis)
			{
				_db = cacheCon.Connection.GetDatabase();
				foreach (EndPoint endpoint in cacheCon.Connection.GetEndPoints(configuredOnly: true))
				{
					_server = cacheCon.Connection.GetServer(endpoint);
				}
			}
			else if (_cacheType == EnumCacheType.SqlServer)
			{
				_distributedCache = distributedCache;
			}
			else
			{
				_memoryCache = memoryCache;
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="key"></param>
		/// <param name="key2"></param>
		/// <param name="value"></param>
		/// <param name="options"></param>
		/// <returns></returns>
		public bool Set<T>(string key, string key2, T value, DateTimeOffset options)
		{
			if (!string.IsNullOrEmpty(key2))
				key = $"{key}~{key2}";

			if (_cacheType == EnumCacheType.Redis)
			{
				TimeSpan expiryTime = options.DateTime.Subtract(DateTime.Now);
				return _db.StringSet(key: key, value: JsonConvert.SerializeObject(value), expiry: expiryTime);
			}
			else if (_cacheType == EnumCacheType.SqlServer)
			{
				_distributedKeys.Add(key);
				DistributedCacheEntryOptions expiryTime = new() { SlidingExpiration = options.DateTime.Subtract(DateTime.Now) };
				_distributedCache.SetString(key: key, value: JsonConvert.SerializeObject(value), options: expiryTime);
				return true;
			}
			else
			{
				using ICacheEntry entry = _memoryCache.CreateEntry(key);
				entry.RegisterPostEvictionCallback(PostEvictionCallback);
				_cacheEntries.AddOrUpdate(key: key, addValue: entry, (o, cacheEntry) =>
				{
					cacheEntry.Value = entry;
					return cacheEntry;
				});
				entry.AbsoluteExpiration = options;
				entry.Value = value;

				return true;
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="key"></param>
		/// <param name="value"></param>
		/// <param name="reason"></param>
		/// <param name="state"></param>
		private void PostEvictionCallback(object key, object value, EvictionReason reason, object state)
		{
			if (_cacheType == EnumCacheType.InMemory && reason != EvictionReason.Replaced)
			{
				_cacheEntries.TryRemove(key: $"{key}", value: out _);
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="key"></param>
		/// <param name="key2"></param>
		/// <param name="value"></param>
		/// <returns></returns>
		public bool TryGetValue<T>(string key, string key2, out T value)
		{
			if (!string.IsNullOrEmpty(key2))
				key = $"{key}~{key2}";

			if (_cacheType == EnumCacheType.Redis)
			{
				RedisValue keyValue = _db.StringGet(key);
				if (string.IsNullOrEmpty(keyValue))
				{
					value = default;
					return false;
				}

				value = JsonConvert.DeserializeObject<T>(keyValue);
				return true;
			}
			else if (_cacheType == EnumCacheType.SqlServer)
			{
				string keyValue = _distributedCache.GetString(key);
				if (string.IsNullOrEmpty(keyValue))
				{
					value = default;
					return false;
				}

				value = JsonConvert.DeserializeObject<T>(keyValue);
				return true;
			}
			else
			{
				return _memoryCache.TryGetValue(key: key, value: out value);
			}
		}

		/// <summary>
		/// Delete caches of matched pattern
		/// </summary>
		/// <param name="pattern">Pattern to search</param>
		public void Clear(string pattern)
		{
			if (_cacheType == EnumCacheType.Redis)
			{
				foreach (RedisKey key in _server.Keys(pattern: $"*{pattern}*"))
				{
					_db.KeyDelete(key: key);
				}
			}
			else if (_cacheType == EnumCacheType.SqlServer)
			{
				foreach (string key in _distributedKeys.Where(key => key.Contains(pattern, StringComparison.OrdinalIgnoreCase)))
				{
					_distributedCache.Remove(key: key);
				}
			}
			else
			{
				foreach (string key in _cacheEntries.Keys.Where(key => key.Contains(pattern, StringComparison.CurrentCultureIgnoreCase)))
				{
					_memoryCache.Remove(key: key);
				}
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <returns></returns>
		public IEnumerator<KeyValuePair<string, object>> GetEnumerator() => _cacheEntries.Select(pair => new KeyValuePair<string, object>(pair.Key, pair.Value.Value)).GetEnumerator();

		/// <summary>
		/// 
		/// </summary>
		/// <returns></returns>
		IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
	}
}