using System.Text.RegularExpressions;
using MongoDB.Bson;
using adas_core.Domain.Enums;
using adas_core.Domain.Models.AppSettings;

namespace adas_core.Domain.Utils
{
    
    //  TTL RESOLVER POR ENTIDAD / CLAVE
    /// <summary>
    /// Resuelve TTLs a partir de CacheSettings teniendo en cuenta:
    ///  - El backend seleccionado por entidad (None | Cache | Redis)
    ///  - El TTL de la entidad para ese backend
    ///  - Caída a GlobalSeconds del backend si no hay TTL específico
    ///  - Si el TTL resultante es null o menor o igual a 0 sin expiración
    /// </summary>
    public static class CacheKeyTtl
    {
        /// <summary>
        /// Converts a nullable number of seconds into a <see cref="TimeSpan"/>, returning <c>null</c> when the value is not greater than zero.
        /// </summary>
        /// <param name="seconds">The number of seconds to convert.</param>
        /// <returns>A <see cref="TimeSpan"/> representing <paramref name="seconds"/>, or <c>null</c> when <paramref name="seconds"/> is <c>null</c> or less than or equal to zero.</returns>
        private static TimeSpan? SecondsOrNull(int? seconds)
                    => seconds is > 0
                        ? TimeSpan.FromSeconds(seconds.Value)
                        : null;

        /// <summary>
        /// Extracts a value from the in-memory TTL settings of the provided cache configuration by applying the specified selector function.
        /// </summary>
        /// <param name="selector">A function that selects the desired <see cref="int?"/> value from a <see cref="TtlSettings"/> instance.</param>
        /// <param name="s">The cache settings whose in-memory TTL configuration is used as the source.</param>
        /// <returns>The nullable integer value produced by applying <paramref name="selector"/> to the in-memory <see cref="TtlSettings"/>.</returns>
        private static int? FromInMemory(Func<TtlSettings, int?> selector, CacheSettings s)
                    => selector(s.InMemory.Ttl);

        /// <summary>
        /// Extracts an integer value from the Redis TTL settings of a <see cref="CacheSettings"/> instance using the provided selector function.
        /// Falls back to a new <see cref="TtlSettings"/> when the settings or their Redis TTL configuration is null, and returns null when the selector itself is null.
        /// </summary>
        /// <param name="selector">A function that derives a nullable integer from a <see cref="TtlSettings"/> instance, or null to short-circuit the operation.</param>
        /// <param name="s">The cache settings whose Redis TTL configuration will be inspected, or null to use a default <see cref="TtlSettings"/>.</param>
        /// <returns>The nullable integer produced by invoking the selector on the resolved <see cref="TtlSettings"/>, or null if the selector is null.</returns>
        private static int? FromRedis(Func<TtlSettings, int?>? selector, CacheSettings? s)
                    => selector?.Invoke(s?.Redis.Ttl ?? new TtlSettings());

        /// <summary>
        /// Retrieves the global in-memory cache time-to-live as a <see cref="TimeSpan"/>, or <c>null</c> when the cache settings or the global seconds value are not provided.
        /// </summary>
        /// <param name="s">The cache settings containing the in-memory TTL configuration, or <c>null</c>.</param>
        /// <returns>A <see cref="TimeSpan"/> representing the global in-memory TTL converted from seconds, or <c>null</c> if <paramref name="s"/> is <c>null</c> or the global seconds value is not set.</returns>
        private static TimeSpan? GlobalInMemory(CacheSettings? s)
                    => SecondsOrNull(s?.InMemory.Ttl.GlobalSeconds);

        /// <summary>
        /// Retrieves the global Redis TTL setting as a nullable <see cref="TimeSpan"/>, returning <c>null</c> when the cache settings, Redis section, or TTL seconds value is not provided.
        /// </summary>
        /// <param name="s">The cache settings containing the Redis TTL configuration, or <c>null</c>.</param>
        /// <returns>A <see cref="TimeSpan"/> representing the global Redis TTL, or <c>null</c> if the configuration is unavailable.</returns>
        private static TimeSpan? GlobalRedis(CacheSettings? s)
                    => SecondsOrNull(s?.Redis.Ttl.GlobalSeconds);

        /// <summary>
        /// Resuelve TTL para una entidad concreta, respetando el backend configurado para dicha entidad.
        /// </summary>
        public static TimeSpan? ResolveForEntity(CacheSettings? settings, CacheEnum.EntityType entity)
        {
            // Selección del backend según la entidad
            if (settings == null) return null;
            
            var mode = entity switch
            {
                CacheEnum.EntityType.Patients            => settings.Patients,
                CacheEnum.EntityType.Displays            => settings.Displays,
                CacheEnum.EntityType.PumpObservations    => settings.PumpObservations,
                CacheEnum.EntityType.Appointments        => settings.Appointments,
                CacheEnum.EntityType.PontOfCare          => settings.PointOfCares,
                CacheEnum.EntityType.GroupedObservations => settings.GroupedObservations,
                CacheEnum.EntityType.PatientObservations => settings.PatientObservations,
                _                                        => CacheEnum.Mode.Cache
            };

            if (!settings.IsEnabled || mode == CacheEnum.Mode.None)
                return null;

            // Selector de TTL específico por entidad
            Func<TtlSettings, int?> selector = entity switch
            {
                CacheEnum.EntityType.Patients            => x => x.PatientsSeconds,
                CacheEnum.EntityType.Displays            => x => x.DisplaysSeconds,
                CacheEnum.EntityType.PumpObservations    => x => x.PumpObservationsSeconds,
                CacheEnum.EntityType.Appointments        => x => x.AppointmentsSeconds,
                CacheEnum.EntityType.PontOfCare          => x => x.PointOfCaresSeconds,
                CacheEnum.EntityType.GroupedObservations => x => x.GroupedObservationsSeconds,
                CacheEnum.EntityType.PatientObservations => x => x.PatientObservationsSeconds,
                CacheEnum.EntityType.ConfigObservations  => x => x.ConfigObservationsSeconds,
                _                                        => x => x.GlobalSeconds
            };

            // Resolver en función del backend elegido para la entidad
            return mode switch
            {
                CacheEnum.Mode.Cache => SecondsOrNull(FromInMemory(selector, settings))
                                        ?? GlobalInMemory(settings),

                CacheEnum.Mode.Redis => SecondsOrNull(FromRedis(selector, settings))
                                        ?? GlobalRedis(settings),

                _ => null
            };
            
        }

        /// <summary>
        /// Dada una clave, clasifica la entidad y resuelve el TTL para esa clave.
        /// </summary>
        public static TimeSpan? ResolveForKey(CacheSettings settings, string key)
        {
            var entity = CacheKeyClassifier.Classify(key);
            return ResolveForEntity(settings, entity);
        }
    }

    
    //  CACHE KEYS (GENERATION)
    /// <summary>
    /// Generador centralizado de claves de caché.
    /// - Prefijos normalizados para que el CacheDispatcher clasifique el backend.
    /// - Overloads "KeyWithTtl" para devolver (key, ttl) en una llamada.
    /// </summary>
    public static class CacheKeys
    {
        #region ConfigObservations
        
        /// <summary>
        /// Returns the configuration key string "configObservations:all", typically used to identify a setting or cache entry related to all configuration observations.
        /// </summary>
        /// <returns>The string "configObservations:all".</returns>
        public static string ConfigObservationsAll()
                    => "configObservations:all";

        public static (string Key, TimeSpan? Ttl) ConfigObservationsAllKeyWithTtl(
            CacheSettings? settings)
        {
            // lo cual usará CacheEnum.EntityType.ConfigObservations (debes añadirlo)
            var key = ConfigObservationsAll();
            var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.ConfigObservations);
            return (key, ttl);
        }
        #endregion
        
        #region DisplayConfig
        /// <summary>
        /// Builds the base configuration key used to reference a display entry.
        /// </summary>
        /// <param name="displayId">The identifier of the display whose base key is being generated.</param>
        /// <returns>A formatted key string combining the display section, the display identifier, and the "base" suffix.</returns>
        public static string DisplayBase(ObjectId displayId)
                    => $"configDisplays:display:{displayId}:base";
        
        /// <summary>
        /// Builds a cache key for a display entity along with its resolved time-to-live (TTL) based on the provided cache settings.
        /// </summary>
        /// <param name="settings">Optional cache settings used to resolve the TTL for display entities. When null, a default TTL is applied.</param>
        /// <param name="displayId">The identifier of the display used to compose the base cache key.</param>
        /// <returns>A tuple containing the generated cache key and the resolved TTL for the display entity.</returns>
        public static (string Key, TimeSpan? Ttl) DisplayBaseKeyWithTtl(
                    CacheSettings? settings,
                    ObjectId displayId)
                {
                    var key = DisplayBase(displayId);
                    var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.Displays);
                    return (key, ttl);
                }
        
        /// <summary>
        /// Builds a configuration display key for the specified display identifier, used to look up or cache the associated configuration entry.
        /// </summary>
        /// <param name="displayId">The identifier of the display whose configuration key is being generated.</param>
        /// <returns>A formatted string key in the form "configDisplays:display:{displayId}:config" representing the display's configuration entry.</returns>
        public static string DisplayWithConfig(ObjectId displayId)
                    => $"configDisplays:display:{displayId}:config";

        /// <summary>
        /// Builds a cache key and resolves its associated time-to-live (TTL) for a display entity using the provided configuration settings.
        /// </summary>
        /// <param name="settings">The cache settings used to resolve the TTL for the display entity; may be <c>null</c>.</param>
        /// <param name="displayId">The identifier of the display entity for which the cache key is generated.</param>
        /// <returns>A tuple containing the generated cache key and the resolved TTL, which may be <c>null</c> if no TTL is configured.</returns>
        public static (string Key, TimeSpan? Ttl) DisplayWithConfigKeyWithTtl(
                    CacheSettings? settings,
                    ObjectId displayId)
                {
                    var key = DisplayWithConfig(displayId);
                    var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.Displays);
                    return (key, ttl);
                }
        
        #endregion
        
        #region PointOfCare
        /// <summary>
        /// Generates a formatted identifier string for a Point of Care base entity, used as a cache key or lookup reference in the format "pointOfCare:{pocId}:base".
        /// </summary>
        /// <param name="pocId">The unique identifier of the Point of Care entity whose base reference key is being generated.</param>
        /// <returns>A formatted string that combines the "pointOfCare" prefix, the provided <paramref name="pocId"/>, and the "base" suffix to uniquely identify the base resource of the Point of Care.</returns>
        public static string PointOfCareBase(ObjectId pocId)
                    => $"pointOfCare:{pocId}:base";
        
        /// <summary>
        /// Builds a cache key for a Point of Care entity and resolves the associated time-to-live (TTL) from the supplied cache settings.
        /// </summary>
        /// <param name="settings">The cache settings used by the TTL resolver. May be null, in which case the resolver falls back to its default behavior.</param>
        /// <param name="pocId">The identifier of the Point of Care entity used to generate the cache key.</param>
        /// <returns>A tuple containing the generated cache key and the resolved TTL, which may be null when no expiration is configured.</returns>
        public static (string Key, TimeSpan? Ttl) PointOfCareBaseKeyWithTtl(
                    CacheSettings? settings, ObjectId pocId)
                {
                    var key = PointOfCareBase(pocId);
                    var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.PontOfCare);
                    return (key, ttl);
                }

        /// <summary>
        /// Builds a cache key for a point of care entry that includes its associated information and is intended to be stored with a time-to-live (TTL).
        /// </summary>
        /// <param name="pocId">The identifier of the point of care used to compose the cache key.</param>
        /// <returns>A formatted cache key string in the form <c>pointOfCare:{pocId}:withInfo</c>.</returns>
        public static string PointOfCareWithInfoKeyWithTtl(ObjectId pocId)
                    => $"pointOfCare:{pocId}:withInfo";

        /// <summary>
        /// Builds the cache key and resolves the associated time-to-live (TTL) for caching a point of care entry, including its related information.
        /// </summary>
        /// <param name="settings">Optional cache settings used to resolve the TTL for the point of care entity. May be <c>null</c>.</param>
        /// <param name="pocId">The identifier of the point of care used to build the cache key.</param>
        /// <returns>A tuple containing the generated cache key and the resolved TTL, which may be <c>null</c> when no TTL is configured.</returns>
        public static (string Key, TimeSpan? Ttl) PointOfCareWithInfoKeyWithTtl(
                    CacheSettings? settings, ObjectId pocId)
                {
                    var key = PointOfCareWithInfoKeyWithTtl(pocId);
                    var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.PontOfCare);
                    return (key, ttl);
                }
        
        #endregion
        
        #region Observations
        /// <summary>
        /// Builds a cache key for retrieving the latest observations of a patient, optionally scoped to specific fields and a trailing count.
        /// </summary>
        /// <param name="patientId">The unique identifier of the patient whose observations are being keyed.</param>
        /// <param name="fieldNames">The collection of field names to include in the key, normalized via <c>Normalize</c>.</param>
        /// <param name="last">Optional maximum number of recent observations to consider; when <c>null</c>, defaults to <c>0</c>.</param>
        /// <returns>A formatted cache key string combining the patient id, normalized field names, and the last value.</returns>
        public static string LatestObservations(ObjectId patientId, IEnumerable<string> fieldNames, int? last = null)
                    => $"patients:latestObs:{patientId}:{Normalize(fieldNames)}:{last ?? 0}";

        /// <summary>
        /// Generates a cache key for the latest patient observations along with its associated Time-To-Live (TTL) duration.
        /// The TTL is resolved from the provided cache settings for the PatientObservations entity type.
        /// </summary>
        /// <param name="settings">The optional cache settings used to resolve the TTL for the cache key.</param>
        /// <param name="patientId">The identifier of the patient whose latest observations are being queried.</param>
        /// <param name="fieldNames">The collection of field names to include in the latest observations cache key.</param>
        /// <param name="last">The optional number of most recent observations to consider when building the cache key.</param>
        /// <returns>A tuple containing the generated cache key and the resolved TTL for the cache entry.</returns>
        public static (string Key, TimeSpan? Ttl) LatestObservationsKeyWithTtl(
                    CacheSettings? settings, ObjectId patientId, IEnumerable<string> fieldNames, int? last = null)
                {
                    var key = LatestObservations(patientId, fieldNames, last);
                    var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.PatientObservations);
                    return (key, ttl);
                }

        #endregion
        
        #region PumpObservations
        /// <summary>
        /// Builds a cache key for retrieving the latest pump observations associated with the specified patient, using an optional entry count that defaults to zero when not provided.
        /// </summary>
        /// <param name="patientId">The unique identifier of the patient whose latest pump observations are being requested.</param>
        /// <param name="last">The optional number of most recent pump observation entries to include; when null, zero is used as a fallback.</param>
        /// <returns>A formatted cache key string of the form <c>pumpObs:latest:{patientId}:{last}</c>, with <c>last</c> resolved to <c>0</c> when null.</returns>
        public static string LatestPumps(ObjectId patientId, int? last = null)
                    => $"pumpObs:latest:{patientId}:{last ?? 0}";

        /// <summary>
        /// Builds the cache key and resolves the TTL for the latest pump observations of a patient, optionally limited to a specified number of entries.
        /// </summary>
        /// <param name="settings">The cache settings used to resolve the TTL for pump observation entries.</param>
        /// <param name="patientId">The identifier of the patient whose latest pump observations are being cached.</param>
        /// <param name="last">The optional maximum number of latest pump entries to include in the cache key.</param>
        /// <returns>A tuple containing the generated cache key and the resolved TTL, which may be <see langword="null"/> if no TTL is configured.</returns>
        public static (string Key, TimeSpan? Ttl) LatestPumpsKeyWithTtl(
                    CacheSettings settings, ObjectId patientId, int? last = null)
                {
                    var key = LatestPumps(patientId, last);
                    var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.PumpObservations);
                    return (key, ttl);
                }
        
        #endregion

        #region  Appointments
        /// <summary>
        /// Generates a cache key for a patient's appointments scheduled on the current UTC day.
        /// </summary>
        /// <param name="patientId">The unique identifier of the patient whose appointments are being keyed.</param>
        /// <returns>A formatted cache key string combining the patient identifier and the current UTC date.</returns>
        public static string PatientAppointmentsToday(ObjectId patientId)
                {
                    var dateKey = DateTime.UtcNow.ToString("yyyyMMdd");
                    return $"appointments:patient:{patientId}:{dateKey}";
                }

        /// <summary>
        /// Builds the cache key for a patient's appointments scheduled for today and resolves the associated time-to-live from the provided cache settings.
        /// </summary>
        /// <param name="settings">Optional cache settings used to resolve the TTL for the appointments entity.</param>
        /// <param name="patientId">The identifier of the patient whose appointments cache key is being generated.</param>
        /// <returns>A tuple containing the generated cache key and the resolved TTL, which may be null when no TTL is configured.</returns>
        public static (string Key, TimeSpan? Ttl) PatientAppointmentsTodayKeyWithTtl(
                    CacheSettings? settings,
                    ObjectId patientId)
                {
                    var key = PatientAppointmentsToday(patientId);
                    var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.Appointments);
                    return (key, ttl);
                }
        
        /// <summary>
        /// Builds a cache key for the appointments associated with the specified Point of Contact for the current UTC day.
        /// </summary>
        /// <param name="pocId">The identifier of the Point of Contact whose appointments are being keyed.</param>
        /// <returns>A formatted cache key string combining the PoC identifier and today's UTC date in <c>yyyyMMdd</c> format.</returns>
        public static string PocAppointmentsToday(ObjectId pocId)
                {
                    var dateKey = DateTime.UtcNow.ToString("yyyyMMdd");
                    return $"appointments:PoC:{pocId}:{dateKey}";
                }

        /// <summary>
        /// Builds a cache key for today's appointments associated with a Point of Care and resolves its corresponding time-to-live (TTL) using the provided cache settings for the Point of Care entity type.
        /// </summary>
        /// <param name="settings">The cache settings used to resolve the TTL; may be <c>null</c> when no settings are supplied.</param>
        /// <param name="pocId">The identifier of the Point of Care whose today's appointments cache key is being generated.</param>
        /// <returns>A tuple containing the generated cache key and the resolved TTL as a nullable <see cref="TimeSpan"/>.</returns>
        public static (string Key, TimeSpan? Ttl) PocAppointmentsTodayKeyWithTtl(
                    CacheSettings? settings,
                    ObjectId pocId)
                {
                    var key = PocAppointmentsToday(pocId);
                    var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.PontOfCare);
                    return (key, ttl);
                }

        #endregion
        
        #region Grouped Observations (claves: "groupedObs:{field}:patient:{id}")
        /// <summary>
        /// Builds a formatted cache key for grouped observations associated with a specific patient, combining the grouped field name and the patient identifier.
        /// </summary>
        /// <param name="patientId">The unique identifier of the patient whose grouped observations are being addressed.</param>
        /// <param name="groupedFieldName">The name of the grouped field used to categorize the observations.</param>
        /// <returns>A formatted string in the pattern <c>groupedObs:{groupedFieldName}:patient:{patientId}</c>.</returns>
        public static string GroupedObs(ObjectId patientId, string groupedFieldName)
                    => $"groupedObs:{groupedFieldName}:patient:{patientId}";

        /// <summary>
        /// Builds a cache key for a patient's grouped observations and resolves the associated time-to-live (TTL) from the supplied cache settings.
        /// </summary>
        /// <param name="settings">The cache settings used to resolve the TTL; may be <c>null</c>, in which case a default TTL is applied.</param>
        /// <param name="patientId">The identifier of the patient whose grouped observations are being cached.</param>
        /// <param name="groupedFieldName">The name of the grouped field used to compose the cache key.</param>
        /// <returns>A tuple containing the generated cache key and the resolved TTL, which may be <c>null</c> when no expiry is configured.</returns>
        public static (string Key, TimeSpan? Ttl) GroupedObsKeyWithTtl(
                    CacheSettings? settings, ObjectId patientId, string groupedFieldName)
                {
                    var key = GroupedObs(patientId, groupedFieldName);
                    var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.GroupedObservations);
                    return (key, ttl);
                }
        
        #endregion

        //Helpers
        /// <summary>
        /// Normalizes a collection of strings by removing null or whitespace entries, trimming the remaining values, sorting them using ordinal (case-sensitive) comparison, and joining them with a pipe (|) delimiter to produce a canonical, comparable representation.
        /// </summary>
        /// <param name="items">The collection of strings to normalize.</param>
        /// <returns>A pipe-delimited string of the cleaned and sorted values, or an empty string if no valid entries are supplied.</returns>
        public static string Normalize(IEnumerable<string> items)
                    => string.Join("|", items
                        .Where(s => !string.IsNullOrWhiteSpace(s))
                        .Select(s => s.Trim())
                        .OrderBy(s => s, StringComparer.Ordinal));
    }


    //  CLASSIFIER
    /// <summary>
    /// Clasificador de claves basado en prefijos.
    /// Permite al CacheDispatcher seleccionar el backend adecuado (Redis, InMemory, None).
    /// </summary>
    public static class CacheKeyClassifier
    {
        /// <summary>
        /// Classifies a cache key into the corresponding entity type by inspecting its prefix.
        /// Recognized prefixes include <c>patients:</c>, <c>displays:</c>/<c>configDisplays:</c>, <c>pumpObs:</c>,
        /// <c>appointments:</c>, <c>groupedObs:</c>, and <c>configObservations:</c> (case-insensitive). If no
        /// known prefix matches, the method returns <see cref="CacheEnum.EntityType.Unknown"/>.
        /// </summary>
        /// <param name="key">The cache key to classify.</param>
        /// <returns>The <see cref="CacheEnum.EntityType"/> that matches the key's prefix, or <see cref="CacheEnum.EntityType.Unknown"/> when no prefix is recognized.</returns>
        public static CacheEnum.EntityType Classify(string key)
                {
                    if (key.StartsWith("patients:", StringComparison.OrdinalIgnoreCase))
                        return CacheEnum.EntityType.Patients;
        
                    if (key.StartsWith("displays:", StringComparison.OrdinalIgnoreCase) ||
                        key.StartsWith("configDisplays:", StringComparison.OrdinalIgnoreCase))
                        return CacheEnum.EntityType.Displays;
        
                    if (key.StartsWith("pumpObs:", StringComparison.OrdinalIgnoreCase))
                        return CacheEnum.EntityType.PumpObservations;
        
                    if (key.StartsWith("appointments:", StringComparison.OrdinalIgnoreCase))
                        return CacheEnum.EntityType.Appointments;
        
                    if (key.StartsWith("groupedObs:", StringComparison.OrdinalIgnoreCase))
                        return CacheEnum.EntityType.GroupedObservations;
                    
                    if (key.StartsWith("configObservations:", StringComparison.OrdinalIgnoreCase))
                        return CacheEnum.EntityType.ConfigObservations;
                    
                    return CacheEnum.EntityType.Unknown;
                }
    }

    //  PATTERNS
    /// <summary>
    /// Patrones para DeleteByPattern (Redis) u operaciones masivas por prefijo.
    /// </summary>
    public static class CacheKeyPatterns
    {
        /// <summary>
        /// Returns a cache key pattern corresponding to the specified entity type, mapping each supported entity (Patients, Displays, PumpObservations, Appointments, GroupedObservations, and ConfigObservations) to its dedicated cache key prefix. For any unrecognized entity type, the wildcard pattern "*" is returned as a fallback to target all cache entries.
        /// </summary>
        /// <param name="type">The entity type for which a cache key pattern is being generated.</param>
        /// <returns>A string representing the cache key pattern associated with the given entity type, or "*" if the entity type is not recognized.</returns>
        public static string ForEntity(CacheEnum.EntityType type)
                    => type switch
                    {
                        CacheEnum.EntityType.Patients            => "patients:*",
                        CacheEnum.EntityType.Displays            => "displays:*",
                        CacheEnum.EntityType.PumpObservations    => "pumpObs:*",
                        CacheEnum.EntityType.Appointments        => "appointments:*",
                        CacheEnum.EntityType.GroupedObservations => "groupedObs:*",
                        CacheEnum.EntityType.ConfigObservations  => "configObservation:*",
                        _                                        => "*"
                    };

        /// <summary>
        /// Builds a wildcard search key by appending ":*" to the specified prefix, enabling prefix-based lookups over a hierarchical key namespace.
        /// </summary>
        /// <param name="prefix">The key prefix to match against.</param>
        /// <returns>A formatted string combining <paramref name="prefix"/> and ":*" used as a wildcard pattern.</returns>
        public static string ByPrefix(string prefix)
                    => $"{prefix}:*";

        /// <summary>
        /// Builds a wildcard search pattern prefixed by the given value and scoped to a specific patient identifier.
        /// </summary>
        /// <param name="prefix">The category or context prefix prepended to the pattern.</param>
        /// <param name="patientId">The patient identifier used to scope the wildcard pattern.</param>
        /// <returns>A formatted pattern string in the form <c>{prefix}:*:{patientId}*</c>.</returns>
        public static string ByPatient(string prefix, string patientId)
                    => $"{prefix}:*:{patientId}*";

        /// <summary>
        /// Builds a formatted string by combining the provided prefix and date with a <c>:*:</c> separator, typically used as a key or identifier pattern.
        /// </summary>
        /// <param name="prefix">The prefix segment to include at the beginning of the returned string.</param>
        /// <param name="date">The date segment to include at the end of the returned string.</param>
        /// <returns>A string formatted as <c>{prefix}:*:{date}</c>.</returns>
        public static string ByDate(string prefix, string date)
                    => $"{prefix}:*:{date}";
    }

    //  INSPECTOR (diagnóstico)
    /// <summary>
    /// Utilidad para extraer información estructurada útil para trazas.
    /// No usada por la lógica de caché, pero sí por logs y diagnósticos.
    /// </summary>
    public static class CacheKeyInspector
    {
        private static readonly Regex _patientRegex =
            new(@"patients:(?<id>[^:]+)", RegexOptions.Compiled);

        private static readonly Regex _groupedRegex =
            new(@"groupedObs:(?<field>[^:]+):(?<id>[^:]+)", RegexOptions.Compiled);

        private static readonly Regex _appointmentDayRegex =
            new(@"appointments:(?<location>.+):(?<date>\d{8})", RegexOptions.Compiled);

        /// <summary>
        /// Extracts the patient identifier from the given key using a regular expression pattern.
        /// Returns <c>null</c> if the key does not match the expected patient format.
        /// </summary>
        /// <param name="key">The input string from which to extract the patient identifier.</param>
        /// <returns>The extracted patient identifier, or <c>null</c> if no match is found.</returns>
        public static string? ExtractPatientId(string key)
                {
                    var match = _patientRegex.Match(key);
                    return match.Success ? match.Groups["id"].Value : null;
                }

        /// <summary>
        /// Extracts the observation field name and patient identifier from a grouped observation key using a regular expression pattern. Returns a tuple of <c>null</c> values when the key does not match the expected grouped format.
        /// </summary>
        /// <param name="key">The grouped observation key to parse.</param>
        /// <returns>A tuple containing the extracted field name and patient identifier, or <c>(null, null)</c> if the key does not match the pattern.</returns>
        public static (string? Field, string? PatientId) ExtractGroupedObservationInfo(string key)
                {
                    var match = _groupedRegex.Match(key);
                    return !match.Success
                        ? (null, null)
                        : (match.Groups["field"].Value, match.Groups["id"].Value);
                }

        /// <summary>
        /// Extracts appointment location and date information from the provided key by matching against a predefined appointment-day pattern.
        /// Returns a null location and null date if the key does not match the expected pattern, and returns the matched location with a null date if the date portion cannot be parsed using the <c>yyyyMMdd</c> format.
        /// </summary>
        /// <param name="key">The input string expected to contain a location and a date in <c>yyyyMMdd</c> format.</param>
        /// <returns>A tuple containing the extracted <c>Location</c> string and the parsed <c>Date</c>; either value may be <c>null</c> when extraction or parsing fails.</returns>
        public static (string? Location, DateTime? Date) ExtractAppointmentInfo(string key)
                {
                    var match = _appointmentDayRegex.Match(key);
                    if (!match.Success)
                        return (null, null);
        
                    var location = match.Groups["location"].Value;
                    var dateStr  = match.Groups["date"].Value;
        
                    if (DateTime.TryParseExact(
                        dateStr, "yyyyMMdd", null,
                        System.Globalization.DateTimeStyles.None,
                        out var date))
                        return (location, date);
        
                    return (location, null);
                }
    }
}