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 { private static TimeSpan? SecondsOrNull(int? seconds) => seconds is > 0 ? TimeSpan.FromSeconds(seconds.Value) : null; private static int? FromInMemory(Func selector, CacheSettings s) => selector(s.InMemory.Ttl); private static int? FromRedis(Func? selector, CacheSettings? s) => selector?.Invoke(s?.Redis.Ttl ?? new TtlSettings()); private static TimeSpan? GlobalInMemory(CacheSettings? s) => SecondsOrNull(s?.InMemory.Ttl.GlobalSeconds); 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 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 public static string DisplayBase(ObjectId displayId) => $"configDisplays:display:{displayId}:base"; 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); } public static string DisplayWithConfig(ObjectId displayId) => $"configDisplays:display:{displayId}:config"; 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 public static string PointOfCareBase(ObjectId pocId) => $"pointOfCare:{pocId}:base"; 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); } public static string PointOfCareWithInfoKeyWithTtl(ObjectId pocId) => $"pointOfCare:{pocId}:withInfo"; 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 public static string LatestObservations(ObjectId patientId, IEnumerable fieldNames, int? last = null) => $"patients:latestObs:{patientId}:{Normalize(fieldNames)}:{last ?? 0}"; 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 public static string LatestPumps(ObjectId patientId, int? last = null) => $"pumpObs:latest:{patientId}:{last ?? 0}"; 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 public static string PatientAppointmentsToday(ObjectId patientId) { var dateKey = DateTime.UtcNow.ToString("yyyyMMdd"); return $"appointments:patient:{patientId}:{dateKey}"; } 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); } public static string PocAppointmentsToday(ObjectId pocId) { var dateKey = DateTime.UtcNow.ToString("yyyyMMdd"); return $"appointments:PoC:{pocId}:{dateKey}"; } 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}") public static string GroupedObs(ObjectId patientId, string groupedFieldName) => $"groupedObs:{groupedFieldName}:patient:{patientId}"; 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 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 { 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 { 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:*", _ => "*" }; public static string ByPrefix(string prefix) => $"{prefix}:*"; public static string ByPatient(string prefix, string patientId) => $"{prefix}:*:{patientId}*"; 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); public static string? ExtractPatientId(string key) { var match = _patientRegex.Match(key); return match.Success ? match.Groups["id"].Value : null; } 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); } 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); } } }