using adas_core.Application.Services.Interfaces;
using adas_core.Domain.Enums;
using adas_core.Domain.Models.AppSettings;
using adas_core.Domain.Models.GroupedObservations;
using adas_core.Domain.Utils;
using MongoDB.Bson;

namespace adas_core.Application.Services.Caching
{
    /// <summary>
    /// Orquestador de caché. Selecciona el backend (Redis, InMemory, None)
    /// según CacheSettings y la entidad del key.
    /// Implementa ICacheService y delega en el backend elegido.
    /// </summary>
    public class CacheDispatcher(
        RedisService redis,
        CacheService memory,
        NoCacheService noop,
        CacheSettings cacheSettings)
        : ICacheService
    {
        
        // Selección de backend
        /// <summary>
        /// Selects the appropriate cache backend (Redis, in-memory, or no-op) for the given key by classifying the key into an entity type and resolving its configured cache mode.
        /// Unknown entity types default to in-memory caching, and unrecognized modes fall back to the no-op cache service.
        /// </summary>
        /// <param name="key">The cache key used to determine the entity type and the corresponding cache backend.</param>
        /// <returns>The <see cref="ICacheService"/> instance that should handle caching for the supplied key.</returns>
        private ICacheService SelectBackend(string key)
                {
                    var entity = CacheKeyClassifier.Classify(key);
                    var mode = entity switch
                    {
                        CacheEnum.EntityType.Patients            => cacheSettings.Patients,
                        CacheEnum.EntityType.Displays            => cacheSettings.Displays,
                        CacheEnum.EntityType.PumpObservations    => cacheSettings.PumpObservations,
                        CacheEnum.EntityType.Appointments        => cacheSettings.Appointments,
                        CacheEnum.EntityType.GroupedObservations => cacheSettings.GroupedObservations,
                        _                                        => CacheEnum.Mode.Cache
                    };
        
                    return mode switch
                    {
                        CacheEnum.Mode.Redis => redis,
                        CacheEnum.Mode.Cache => memory,
                        _                    => noop
                    };
                }

        // Para GroupedObservations generamos la misma clave compuesta que el resto de servicios,
        // de modo que el clasificador y la política de TTL funcionen igual.
        /// <summary>
        /// Builds a composite key used to identify grouped observations for a specific patient.
        /// </summary>
        /// <param name="gf">The grouped field whose name contributes to the key.</param>
        /// <param name="patientId">The identifier of the patient associated with the grouped observation.</param>
        /// <returns>A formatted key string in the form <c>GroupedObs:{patientId}:{gf.Name}</c>.</returns>
        private static string BuildGroupedKey(GroupedField gf, ObjectId patientId)
                    => $"GroupedObs:{patientId}:{gf.Name}";

        /// <summary>
        /// Selects the appropriate cache backend for the given grouped field and patient identifier by building a grouped key and resolving the backend through the key-based overload.
        /// </summary>
        /// <param name="groupedField">The grouped field used to derive the cache key.</param>
        /// <param name="patientId">The patient identifier used to derive the cache key.</param>
        /// <returns>The <see cref="ICacheService"/> backend associated with the built grouped key.</returns>
        private ICacheService SelectBackend(GroupedField groupedField, ObjectId patientId)
                    => SelectBackend(BuildGroupedKey(groupedField, patientId));

        
        // GetOrSet (KEY string)
        /// <summary>
        /// Asynchronously retrieves the object associated with the specified key from the selected backend, or sets it using the provided factory if it is not already cached.
        /// </summary>
        /// <param name="key">The cache key used to identify the object and to select the appropriate backend.</param>
        /// <param name="factory">A delegate that asynchronously produces the value to store when the key is not present in the selected backend.</param>
        /// <param name="ttl">An optional time-to-live duration for the cached object. If null, the backend's default expiration is applied.</param>
        /// <param name="cancellationToken">A token to observe while waiting for the operation to complete.</param>
        /// <returns>A task that represents the asynchronous operation, containing the retrieved or newly created object of type <typeparamref name="T"/>.</returns>
        public Task<T> GetOrSetObjectAsync<T>(
                    string key,
                    Func<Task<T>> factory,
                    TimeSpan? ttl = null,
                    CancellationToken cancellationToken = default)
                    => SelectBackend(key).GetOrSetObjectAsync(key, factory, ttl, cancellationToken);

        /// <summary>
        /// Asynchronously retrieves the value associated with the specified key from the backend selected for that key,
        /// or loads and stores it using the provided loader function if it is not already present.
        /// Supports an optional time-to-live (TTL) for the cached entry, and the returned value may be null.
        /// </summary>
        /// <param name="key">The key used to identify the cached value and to select the appropriate backend.</param>
        /// <param name="loader">An asynchronous function that produces the value to cache when no existing entry is found.</param>
        /// <param name="ttl">An optional time-to-live duration after which the cached entry expires. If null, the backend's default TTL is used.</param>
        /// <returns>A task that represents the asynchronous operation, containing the cached or loaded string value, or null if no value could be obtained.</returns>
        public Task<string?> GetOrSetValueAsync(
                    string key,
                    Func<Task<string>> loader,
                    TimeSpan? ttl = null)
                    => SelectBackend(key).GetOrSetValueAsync(key, loader, ttl);

        
        // GetOrSet (GroupedField + PatientId)
        /// <summary>
        /// Asynchronously retrieves the object associated with the specified grouped field and patient, or creates and stores it using the provided factory if it does not exist.
        /// The appropriate backend is selected based on the grouped field and patient identifier before the underlying get-or-set operation is performed.
        /// </summary>
        /// <typeparam name="T">The type of the object to retrieve or create.</typeparam>
        /// <param name="groupedField">The grouped field that determines the target backend and identifies the cached object.</param>
        /// <param name="patientId">The identifier of the patient whose object is being retrieved or created.</param>
        /// <param name="factory">The asynchronous factory used to create the object when no cached value is available.</param>
        /// <param name="ttl">An optional time-to-live applied to the cached object. When <c>null</c>, the backend's default expiration is used.</param>
        /// <param name="cancellationToken">The token to observe for canceling the asynchronous operation.</param>
        /// <returns>A task that represents the asynchronous get-or-set operation, containing the retrieved or newly created object.</returns>
        public Task<T> GetOrSetObjectAsync<T>(
                    GroupedField groupedField,
                    ObjectId patientId,
                    Func<Task<T>> factory,
                    TimeSpan? ttl = null,
                    CancellationToken cancellationToken = default)
                    => SelectBackend(groupedField, patientId)
                        .GetOrSetObjectAsync(groupedField, patientId, factory, ttl, cancellationToken);

        
        // Set/Get básicos
        /// <summary>
        /// Sets the value associated with the specified key by selecting the appropriate backend for that key and delegating the assignment to it.
        /// </summary>
        /// <param name="key">The key used to select the backend and identify the value to set.</param>
        /// <param name="value">The value to associate with the specified key.</param>
        public void SetValue(string key, string value)
                    => SelectBackend(key).SetValue(key, value);

        /// <summary>
        /// Retrieves the value associated with the specified key by delegating the lookup to a backend selected for that key.
        /// </summary>
        /// <param name="key">The key used to select the backend and retrieve the associated value.</param>
        /// <returns>The value associated with the key, or <c>null</c> if the selected backend returns no value.</returns>
        public string? GetValue(string key)
                    => SelectBackend(key).GetValue(key);

        /// <summary>
        /// Retrieves an object of type <typeparamref name="T"/> from the backend selected by the given key, with an option to trigger an update.
        /// </summary>
        /// <param name="key">The identifier used to select the appropriate backend and to look up the object.</param>
        /// <param name="upd">Indicates whether the underlying backend should perform an update during retrieval. Defaults to <c>true</c>.</param>
        /// <returns>A task that represents the asynchronous retrieval operation, containing the object of type <typeparamref name="T"/> or <c>null</c> if not found.</returns>
        public Task<T?> GetObjectAsync<T>(string key, bool upd = true)
                    => SelectBackend(key).GetObjectAsync<T>(key, upd);

        /// <summary>
        /// Asynchronously stores an object in the backend selected by the specified key.
        /// </summary>
        /// <typeparam name="T">The type of the object to store.</typeparam>
        /// <param name="key">The key used to select the backend and identify the stored object.</param>
        /// <param name="obj">The object to store in the selected backend.</param>
        /// <param name="upd">Indicates whether an update operation should be performed. Defaults to <c>true</c>.</param>
        /// <returns>A task that represents the asynchronous store operation.</returns>
        public Task SetObjectAsync<T>(string key, T obj, bool upd = true)
                    => SelectBackend(key).SetObjectAsync(key, obj, upd);

        /// <summary>
        /// Retrieves an object of the specified type from the backend selected by the given key, optionally applying a time-to-live and update behavior.
        /// </summary>
        /// <param name="key">The identifier used to select the backend and locate the stored object.</param>
        /// <param name="ttl">An optional time-to-live applied to the object; if null, the backend's default is used.</param>
        /// <param name="upd">A flag indicating whether the retrieval should update the object's state (e.g., refresh expiration).</param>
        /// <returns>A task containing the deserialized object, or null if the object is not found in the selected backend.</returns>
        public Task<T?> GetObjectAsync<T>(string key, TimeSpan? ttl, bool upd)
                    => SelectBackend(key).GetObjectAsync<T>(key, ttl, upd);

        /// <summary>
        /// Asynchronously stores an object in the backend selected for the specified key, with an optional time-to-live and update flag.
        /// </summary>
        /// <param name="key">The key used to select the target backend and identify the object.</param>
        /// <param name="obj">The object to store.</param>
        /// <param name="ttl">The optional time-to-live duration for the stored object.</param>
        /// <param name="upd">Indicates whether to update an existing entry or create a new one.</param>
        /// <returns>A task that represents the asynchronous set operation.</returns>
        public Task SetObjectAsync<T>(string key, T obj, TimeSpan? ttl, bool upd)
                    => SelectBackend(key).SetObjectAsync(key, obj, ttl, upd);

        /// <summary>
        /// Asynchronously deletes the object identified by the specified key by delegating the operation to the backend selected for that key.
        /// </summary>
        /// <param name="key">The identifier of the object to delete, also used to resolve the responsible backend.</param>
        /// <returns>A task that represents the asynchronous delete operation.</returns>
        public Task DeleteObjectAsync(string key)
                    => SelectBackend(key).DeleteObjectAsync(key);

        /// <summary>
        /// Deletes entries matching the specified pattern from both Redis and in-memory storage, returning the total count of deleted entries.
        /// </summary>
        /// <param name="pattern">The pattern used to match entries for deletion in both storage backends.</param>
        /// <returns>The combined total number of entries deleted from Redis and in-memory storage.</returns>
        public async Task<long> DeleteByPatternAsync(string pattern)
                    => await redis.DeleteByPatternAsync(pattern) + await memory.DeleteByPatternAsync(pattern);

        /// <summary>
        /// Clears all cached data from both the in-memory cache and the Redis cache, ensuring that stale entries are removed across all configured cache providers.
        /// </summary>
        public void CleanCache()
                {
                    memory.CleanCache();
                    redis.CleanCache();
                }
    }
}