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 /// /// 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 /// public static class CacheKeyTtl { /// /// Converts a nullable number of seconds into a , returning null when the value is not greater than zero. /// /// The number of seconds to convert. /// A representing , or null when is null or less than or equal to zero. private static TimeSpan? SecondsOrNull(int? seconds) => seconds is > 0 ? TimeSpan.FromSeconds(seconds.Value) : null; /// /// Extracts a value from the in-memory TTL settings of the provided cache configuration by applying the specified selector function. /// /// A function that selects the desired value from a instance. /// The cache settings whose in-memory TTL configuration is used as the source. /// The nullable integer value produced by applying to the in-memory . private static int? FromInMemory(Func selector, CacheSettings s) => selector(s.InMemory.Ttl); /// /// Extracts an integer value from the Redis TTL settings of a instance using the provided selector function. /// Falls back to a new when the settings or their Redis TTL configuration is null, and returns null when the selector itself is null. /// /// A function that derives a nullable integer from a instance, or null to short-circuit the operation. /// The cache settings whose Redis TTL configuration will be inspected, or null to use a default . /// The nullable integer produced by invoking the selector on the resolved , or null if the selector is null. private static int? FromRedis(Func? selector, CacheSettings? s) => selector?.Invoke(s?.Redis.Ttl ?? new TtlSettings()); /// /// Retrieves the global in-memory cache time-to-live as a , or null when the cache settings or the global seconds value are not provided. /// /// The cache settings containing the in-memory TTL configuration, or null. /// A representing the global in-memory TTL converted from seconds, or null if is null or the global seconds value is not set. private static TimeSpan? GlobalInMemory(CacheSettings? s) => SecondsOrNull(s?.InMemory.Ttl.GlobalSeconds); /// /// Retrieves the global Redis TTL setting as a nullable , returning null when the cache settings, Redis section, or TTL seconds value is not provided. /// /// The cache settings containing the Redis TTL configuration, or null. /// A representing the global Redis TTL, or null if the configuration is unavailable. private static TimeSpan? GlobalRedis(CacheSettings? s) => SecondsOrNull(s?.Redis.Ttl.GlobalSeconds); /// /// Resuelve TTL para una entidad concreta, respetando el backend configurado para dicha entidad. /// 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 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 }; } /// /// Dada una clave, clasifica la entidad y resuelve el TTL para esa clave. /// public static TimeSpan? ResolveForKey(CacheSettings settings, string key) { var entity = CacheKeyClassifier.Classify(key); return ResolveForEntity(settings, entity); } } // CACHE KEYS (GENERATION) /// /// Generador centralizado de claves de caché. /// - Prefijos normalizados para que el CacheDispatcher clasifique el backend. /// - Overloads "KeyWithTtl" para devolver (key, ttl) en una llamada. /// public static class CacheKeys { #region ConfigObservations /// /// Returns the configuration key string "configObservations:all", typically used to identify a setting or cache entry related to all configuration observations. /// /// The string "configObservations:all". 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 /// /// Builds the base configuration key used to reference a display entry. /// /// The identifier of the display whose base key is being generated. /// A formatted key string combining the display section, the display identifier, and the "base" suffix. public static string DisplayBase(ObjectId displayId) => $"configDisplays:display:{displayId}:base"; /// /// Builds a cache key for a display entity along with its resolved time-to-live (TTL) based on the provided cache settings. /// /// Optional cache settings used to resolve the TTL for display entities. When null, a default TTL is applied. /// The identifier of the display used to compose the base cache key. /// A tuple containing the generated cache key and the resolved TTL for the display entity. 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); } /// /// Builds a configuration display key for the specified display identifier, used to look up or cache the associated configuration entry. /// /// The identifier of the display whose configuration key is being generated. /// A formatted string key in the form "configDisplays:display:{displayId}:config" representing the display's configuration entry. public static string DisplayWithConfig(ObjectId displayId) => $"configDisplays:display:{displayId}:config"; /// /// Builds a cache key and resolves its associated time-to-live (TTL) for a display entity using the provided configuration settings. /// /// The cache settings used to resolve the TTL for the display entity; may be null. /// The identifier of the display entity for which the cache key is generated. /// A tuple containing the generated cache key and the resolved TTL, which may be null if no TTL is configured. 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 /// /// 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". /// /// The unique identifier of the Point of Care entity whose base reference key is being generated. /// A formatted string that combines the "pointOfCare" prefix, the provided , and the "base" suffix to uniquely identify the base resource of the Point of Care. public static string PointOfCareBase(ObjectId pocId) => $"pointOfCare:{pocId}:base"; /// /// Builds a cache key for a Point of Care entity and resolves the associated time-to-live (TTL) from the supplied cache settings. /// /// The cache settings used by the TTL resolver. May be null, in which case the resolver falls back to its default behavior. /// The identifier of the Point of Care entity used to generate the cache key. /// A tuple containing the generated cache key and the resolved TTL, which may be null when no expiration is configured. 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); } /// /// 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). /// /// The identifier of the point of care used to compose the cache key. /// A formatted cache key string in the form pointOfCare:{pocId}:withInfo. public static string PointOfCareWithInfoKeyWithTtl(ObjectId pocId) => $"pointOfCare:{pocId}:withInfo"; /// /// Builds the cache key and resolves the associated time-to-live (TTL) for caching a point of care entry, including its related information. /// /// Optional cache settings used to resolve the TTL for the point of care entity. May be null. /// The identifier of the point of care used to build the cache key. /// A tuple containing the generated cache key and the resolved TTL, which may be null when no TTL is configured. 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 /// /// Builds a cache key for retrieving the latest observations of a patient, optionally scoped to specific fields and a trailing count. /// /// The unique identifier of the patient whose observations are being keyed. /// The collection of field names to include in the key, normalized via Normalize. /// Optional maximum number of recent observations to consider; when null, defaults to 0. /// A formatted cache key string combining the patient id, normalized field names, and the last value. public static string LatestObservations(ObjectId patientId, IEnumerable fieldNames, int? last = null) => $"patients:latestObs:{patientId}:{Normalize(fieldNames)}:{last ?? 0}"; /// /// 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. /// /// The optional cache settings used to resolve the TTL for the cache key. /// The identifier of the patient whose latest observations are being queried. /// The collection of field names to include in the latest observations cache key. /// The optional number of most recent observations to consider when building the cache key. /// A tuple containing the generated cache key and the resolved TTL for the cache entry. public static (string Key, TimeSpan? Ttl) LatestObservationsKeyWithTtl( CacheSettings? settings, ObjectId patientId, IEnumerable fieldNames, int? last = null) { var key = LatestObservations(patientId, fieldNames, last); var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.PatientObservations); return (key, ttl); } #endregion #region PumpObservations /// /// 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. /// /// The unique identifier of the patient whose latest pump observations are being requested. /// The optional number of most recent pump observation entries to include; when null, zero is used as a fallback. /// A formatted cache key string of the form pumpObs:latest:{patientId}:{last}, with last resolved to 0 when null. public static string LatestPumps(ObjectId patientId, int? last = null) => $"pumpObs:latest:{patientId}:{last ?? 0}"; /// /// Builds the cache key and resolves the TTL for the latest pump observations of a patient, optionally limited to a specified number of entries. /// /// The cache settings used to resolve the TTL for pump observation entries. /// The identifier of the patient whose latest pump observations are being cached. /// The optional maximum number of latest pump entries to include in the cache key. /// A tuple containing the generated cache key and the resolved TTL, which may be if no TTL is configured. 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 /// /// Generates a cache key for a patient's appointments scheduled on the current UTC day. /// /// The unique identifier of the patient whose appointments are being keyed. /// A formatted cache key string combining the patient identifier and the current UTC date. public static string PatientAppointmentsToday(ObjectId patientId) { var dateKey = DateTime.UtcNow.ToString("yyyyMMdd"); return $"appointments:patient:{patientId}:{dateKey}"; } /// /// Builds the cache key for a patient's appointments scheduled for today and resolves the associated time-to-live from the provided cache settings. /// /// Optional cache settings used to resolve the TTL for the appointments entity. /// The identifier of the patient whose appointments cache key is being generated. /// A tuple containing the generated cache key and the resolved TTL, which may be null when no TTL is configured. 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); } /// /// Builds a cache key for the appointments associated with the specified Point of Contact for the current UTC day. /// /// The identifier of the Point of Contact whose appointments are being keyed. /// A formatted cache key string combining the PoC identifier and today's UTC date in yyyyMMdd format. public static string PocAppointmentsToday(ObjectId pocId) { var dateKey = DateTime.UtcNow.ToString("yyyyMMdd"); return $"appointments:PoC:{pocId}:{dateKey}"; } /// /// 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. /// /// The cache settings used to resolve the TTL; may be null when no settings are supplied. /// The identifier of the Point of Care whose today's appointments cache key is being generated. /// A tuple containing the generated cache key and the resolved TTL as a nullable . 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}") /// /// Builds a formatted cache key for grouped observations associated with a specific patient, combining the grouped field name and the patient identifier. /// /// The unique identifier of the patient whose grouped observations are being addressed. /// The name of the grouped field used to categorize the observations. /// A formatted string in the pattern groupedObs:{groupedFieldName}:patient:{patientId}. public static string GroupedObs(ObjectId patientId, string groupedFieldName) => $"groupedObs:{groupedFieldName}:patient:{patientId}"; /// /// Builds a cache key for a patient's grouped observations and resolves the associated time-to-live (TTL) from the supplied cache settings. /// /// The cache settings used to resolve the TTL; may be null, in which case a default TTL is applied. /// The identifier of the patient whose grouped observations are being cached. /// The name of the grouped field used to compose the cache key. /// A tuple containing the generated cache key and the resolved TTL, which may be null when no expiry is configured. 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 /// /// 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. /// /// The collection of strings to normalize. /// A pipe-delimited string of the cleaned and sorted values, or an empty string if no valid entries are supplied. public static string Normalize(IEnumerable items) => string.Join("|", items .Where(s => !string.IsNullOrWhiteSpace(s)) .Select(s => s.Trim()) .OrderBy(s => s, StringComparer.Ordinal)); } // CLASSIFIER /// /// Clasificador de claves basado en prefijos. /// Permite al CacheDispatcher seleccionar el backend adecuado (Redis, InMemory, None). /// public static class CacheKeyClassifier { /// /// Classifies a cache key into the corresponding entity type by inspecting its prefix. /// Recognized prefixes include patients:, displays:/configDisplays:, pumpObs:, /// appointments:, groupedObs:, and configObservations: (case-insensitive). If no /// known prefix matches, the method returns . /// /// The cache key to classify. /// The that matches the key's prefix, or when no prefix is recognized. 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 /// /// Patrones para DeleteByPattern (Redis) u operaciones masivas por prefijo. /// public static class CacheKeyPatterns { /// /// 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. /// /// The entity type for which a cache key pattern is being generated. /// A string representing the cache key pattern associated with the given entity type, or "*" if the entity type is not recognized. 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:*", _ => "*" }; /// /// Builds a wildcard search key by appending ":*" to the specified prefix, enabling prefix-based lookups over a hierarchical key namespace. /// /// The key prefix to match against. /// A formatted string combining and ":*" used as a wildcard pattern. public static string ByPrefix(string prefix) => $"{prefix}:*"; /// /// Builds a wildcard search pattern prefixed by the given value and scoped to a specific patient identifier. /// /// The category or context prefix prepended to the pattern. /// The patient identifier used to scope the wildcard pattern. /// A formatted pattern string in the form {prefix}:*:{patientId}*. public static string ByPatient(string prefix, string patientId) => $"{prefix}:*:{patientId}*"; /// /// Builds a formatted string by combining the provided prefix and date with a :*: separator, typically used as a key or identifier pattern. /// /// The prefix segment to include at the beginning of the returned string. /// The date segment to include at the end of the returned string. /// A string formatted as {prefix}:*:{date}. public static string ByDate(string prefix, string date) => $"{prefix}:*:{date}"; } // INSPECTOR (diagnóstico) /// /// Utilidad para extraer información estructurada útil para trazas. /// No usada por la lógica de caché, pero sí por logs y diagnósticos. /// public static class CacheKeyInspector { private static readonly Regex _patientRegex = new(@"patients:(?[^:]+)", RegexOptions.Compiled); private static readonly Regex _groupedRegex = new(@"groupedObs:(?[^:]+):(?[^:]+)", RegexOptions.Compiled); private static readonly Regex _appointmentDayRegex = new(@"appointments:(?.+):(?\d{8})", RegexOptions.Compiled); /// /// Extracts the patient identifier from the given key using a regular expression pattern. /// Returns null if the key does not match the expected patient format. /// /// The input string from which to extract the patient identifier. /// The extracted patient identifier, or null if no match is found. public static string? ExtractPatientId(string key) { var match = _patientRegex.Match(key); return match.Success ? match.Groups["id"].Value : null; } /// /// Extracts the observation field name and patient identifier from a grouped observation key using a regular expression pattern. Returns a tuple of null values when the key does not match the expected grouped format. /// /// The grouped observation key to parse. /// A tuple containing the extracted field name and patient identifier, or (null, null) if the key does not match the pattern. 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); } /// /// 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 yyyyMMdd format. /// /// The input string expected to contain a location and a date in yyyyMMdd format. /// A tuple containing the extracted Location string and the parsed Date; either value may be null when extraction or parsing fails. 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); } } }