using adas_core.Application.Services.Interfaces;
using adas_core.Domain.Models.AppSettings;
using adas_core.Domain.Utils;
using adas_core.Domain.Enums;
using adas_core.Domain.Models.GroupedObservations;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using MongoDB.Bson;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using StackExchange.Redis;

namespace adas_core.Application.Services.Caching
{
    public class RedisService : ICacheService
    {
        private readonly ILogger<RedisService> _logger;
        private readonly CacheSettings _cacheSettings;
        private readonly LockManagerService _lockManager;
        private ConnectionMultiplexer? _connection;
        private IDatabase? _database;
        private IServer? _server;

        public IDatabase? Database => _database;
        private bool _isRedisAvailable;

        public RedisService(
            IOptions<CacheSettings> options,
            ILogger<RedisService> logger,
            LockManagerService lockManager)
        {
            _cacheSettings = options.Value;
            _logger = logger;
            _lockManager = lockManager;

            if (!string.IsNullOrEmpty(_cacheSettings.Redis.ConnectionString))
                _ = InitializeRedisConnectionAsync();
        }

        
        // GET OR SET (string key)
        public async Task<T> GetOrSetObjectAsync<T>(
            string key,
            Func<Task<T>> factory,
            TimeSpan? ttl = null,
            CancellationToken cancellationToken = default)
        {
            if (!_isRedisAvailable)
                return await factory();

            var direct = await GetObjectAsync<T>(key);

            if (direct is not null)
                return direct;

            return await _lockManager.WithLockAsync(
                $"getorset:{key}",
                TimeSpan.FromSeconds(5),
                async () =>
                {
                    var again = await GetObjectAsync<T>(key);
                    if (again is not null)
                        return again;

                    var created = await factory();

                    if (created != null)
                        await SetObjectAsync(key, created, ttl, true);

                    return created!;
                },
                cancellationToken);
        }

        public async Task<string?> GetOrSetValueAsync(
            string key,
            Func<Task<string>> loader,
            TimeSpan? ttl = null)
        {
            if (!_isRedisAvailable)
                return await loader();

            var direct = GetValue(key);
            if (direct is not null)
                return direct;

            return await _lockManager.WithLockAsync(
                $"getorset:{key}",
                TimeSpan.FromSeconds(5),
                async () =>
                {
                    var again = GetValue(key);
                    if (again is not null)
                        return again;

                    var created = await loader();
                    SetValue(key, created);
                    
                    return created;
                });
        }

        
        // GET OR SET (GroupedField + patientId)
        private static string BuildGroupedKey(GroupedField gf, ObjectId patientId)
            => $"GroupedObs:{patientId}:{gf.Name}";

        public async Task<T> GetOrSetObjectAsync<T>(
            GroupedField groupedField,
            ObjectId patientId,
            Func<Task<T>> factory,
            TimeSpan? ttl = null,
            CancellationToken cancellationToken = default)
        {
            var key = BuildGroupedKey(groupedField, patientId);

            if (!_isRedisAvailable)
                return await factory();

            var direct = await GetObjectAsync<T>(key);
            if (direct is not null)
                return direct;

            return await _lockManager.WithLockAsync(
                $"getorset:{key}",
                TimeSpan.FromSeconds(5),
                async () =>
                {
                    var again = await GetObjectAsync<T>(key);
                    if (again is not null)
                        return again;

                    var created = await factory();

                    if (created != null)
                        await SetObjectAsync(key, created, ttl, true);

                    return created!;
                },
                cancellationToken);
        }

        
        // BASIC OPERATIONS
        public void SetValue(string key, string value)
            => _database?.StringSet(key, value, GetEntityTtl(key), true);

        public string? GetValue(string key)
        {
            var val = _database?.StringGet(key);
            if (val.HasValue && ShouldRenewTtl(key))
                _database?.KeyExpire(key, GetEntityTtl(key));
            return val;
        }

        public async Task<T?> GetObjectAsync<T>(string key, bool updateExpiration = true)
        {
            if (!_isRedisAvailable)
                return default;

            var json = await _database!.StringGetAsync(key);
            if (json.IsNullOrEmpty)
                return default;

            if (updateExpiration)
                _database!.KeyExpire(key, GetEntityTtl(key));

            var settings = new JsonSerializerSettings
            {
                ContractResolver = new CamelCasePropertyNamesContractResolver(),
                Converters = new List<JsonConverter> { new StringEnumConverter(), new ObjectIdConverter() }
            };
            try
            {
                return JsonConvert.DeserializeObject<T>(json!, settings);
            }
            catch (Exception e)
            {
                _logger.LogError("Error deserializing object in RedisService {e}",e.ToString());
                throw new Exception($"Error deserializing object in RedisService {e}", e);
            }
        }

        public async Task SetObjectAsync<T>(
            string key,
            T obj,
            bool updateExpiration = true)
            => await SetObjectAsync(key, obj, null, updateExpiration);

        public async Task SetObjectAsync<T>(
            string key,
            T obj,
            TimeSpan? ttlOverride,
            bool updateExpiration)
        {
            if (!_isRedisAvailable)
                return;
            var settings = new JsonSerializerSettings
            {
                ContractResolver = new CamelCasePropertyNamesContractResolver(),
                Converters = new List<JsonConverter> { new StringEnumConverter(), new ObjectIdConverter() }
            };
            var json = JsonConvert.SerializeObject(obj, settings);
            await _database!.StringSetAsync(key, json, ttlOverride ?? GetEntityTtl(key), true);
        }

        public async Task DeleteObjectAsync(string key)
        {
            if (_isRedisAvailable)
                await _database!.KeyDeleteAsync(key);
        }

        public async Task<long> DeleteByPatternAsync(string pattern)
        {
            if (!_isRedisAvailable || _server == null)
                return 0;

            var keys = _server.Keys(pattern: pattern).ToArray();

            foreach (var key in keys)
                await _database!.KeyDeleteAsync(key);

            return keys.Length;
        }

        public void CleanCache()
            => _server?.FlushDatabase();

        
        // TTL
        private TimeSpan? GetEntityTtl(string key)
        {
            var entity = CacheKeyClassifier.Classify(key);
            var ttl = _cacheSettings.Redis.Ttl;

            int? seconds = entity switch
            {
                CacheEnum.EntityType.Patients => ttl.PatientsSeconds,
                CacheEnum.EntityType.Displays => ttl.DisplaysSeconds,
                _ => ttl.GlobalSeconds
            };

            return seconds > 0 ? TimeSpan.FromSeconds(seconds.Value) : null;
        }

        private bool ShouldRenewTtl(string key)
            => GetEntityTtl(key) != null;

        
        // INITIALIZATION
        private async Task InitializeRedisConnectionAsync()
        {
            _isRedisAvailable = false;
            try
            {
                if (_cacheSettings.Redis.ConnectionString == null)
                    return;
               
                _connection = await ConnectionMultiplexer.ConnectAsync(_cacheSettings.Redis.ConnectionString);
                _database = _connection.GetDatabase();
                _server = _connection.GetServer(_cacheSettings.Redis.ConnectionString);

                _isRedisAvailable = true;
            }
            catch(Exception ex)
            {
                _logger.LogError( "Error Initializing Redis Connection. Exception:{ex}", ex.Message);
            }
        }

        public Task<T?> GetObjectAsync<T>(string key, TimeSpan? ttlOverride, bool updateExpiration)
            => GetObjectAsync<T>(key, updateExpiration);
    }
}