using System.Collections.Concurrent; using adas_core.Application.Exceptions; using adas_core.Application.Repositories.Interfaces; using adas_core.Application.Services.Interfaces; using adas_core.Domain.Enums; using adas_core.Domain.Models; using adas_core.Domain.Models.AppSettings; using adas_core.Domain.Models.DTO; using adas_core.Domain.Models.Filter; using adas_core.Domain.Models.GroupedObservations; using adas_core.Domain.Models.MongoModels; using adas_core.Domain.Models.Pumps; using adas_core.Domain.Models.Responses; using adas_core.Domain.Utils; using Microsoft.AspNetCore.Http; using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.Scripting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using MongoDB.Bson; namespace adas_core.Application.Services; public class ConfigObservationService : IConfigObservationService { private static readonly ConcurrentDictionary CachedConfigObservations = new(); private static readonly ConcurrentDictionary CachedConfigObservationKeys = new(); private readonly IOptions _apiSettings; private readonly ILocalAuditService _auditService; private readonly IConfigObservationRepository _configObservationRepository; private readonly RetentionPolicy _defaultRetentionPolicy; private readonly int _defaultRetentionPolicyValue; private readonly IHttpContextAccessor _httpContextAccessor; private readonly bool _ignoreUnknownTreatment; private readonly ILogger _logger; private readonly int? _refreshTimeout; private readonly IUnitService _unitService; private readonly ICacheService _cacheService; private readonly CacheSettings? _cacheSettings; private bool IgnoreUnknownObservation => _apiSettings.Value.ConfigObservation?.IgnoreUnknownObservation ?? false; public ConfigObservationService( IConfigObservationRepository configObservationRepository, IOptions apiSettings, IOptions cacheSettings, ILogger logger, IUnitService unitService, IHttpContextAccessor httpContextAccessor, ILocalAuditService auditService, ICacheService cacheService ) { _cacheService = cacheService; _cacheSettings = cacheSettings.Value; _configObservationRepository = configObservationRepository; _apiSettings = apiSettings; _logger = logger; _unitService = unitService; _refreshTimeout = _apiSettings.Value.ConfigObservation?.Refresh; _ignoreUnknownTreatment = _apiSettings.Value.ConfigObservation?.IgnoreUnknownTreatment ?? false; _defaultRetentionPolicyValue = _apiSettings.Value.RetentionPolicyValue; _defaultRetentionPolicy = RetentionPolicy.NoDelete; _httpContextAccessor = httpContextAccessor; _auditService = auditService; } public async Task> GetAllConfigs(CancellationToken ct = default) { var (key, ttl) = CacheKeys.ConfigObservationsAllKeyWithTtl(_cacheSettings); var result = await _cacheService.GetOrSetObjectAsync( key, async () => await _configObservationRepository.FindAll(), ttl, ct); return result; } public async Task GetAllCompact() { var count = await _configObservationRepository.Count(); return new ConfigObservationDto { ItemCount = count }; } public async Task> GetPaginatedItems(PaginationFilter filter) { var result = await _configObservationRepository.GetPaginatedItems(filter); var count = await _configObservationRepository.Count(); return new PaginationResponse(result.ToList(), filter.PageNumber, filter.PageSize, count); } public async Task GetConfigById(ObjectId id) { return await _configObservationRepository.FindById(id) ?? null; } public async Task> GetConfigNames(string id) { return await _configObservationRepository.GetConfigNames(id); } public async Task> GetConfigNames() { return await _configObservationRepository.GetConfigNames(); } public async Task RetentionActions(T obs) where T : BasePatientObservation { var conf = await Get(obs); return conf is { RetentionPolicy: not null } ? new ObservatitonRetentionResult(conf.RetentionPolicy.Value, conf.RetentionPolicyValue) : new ObservatitonRetentionResult(RetentionPolicy.NoDelete, null); } public async Task GroupedObservationStatus(GroupedField groupedField, GroupedObservationEnum.Result result, string name, object value, double? min, double? max) { var conf = await Get(name); if (conf == null) return StatusEnum.Type.Ok; PatientObservation? mapObs; if (conf.Grouped != null && groupedField.Group != null && conf.Grouped.TryGetValue(groupedField.Group, out var grp)) { mapObs = await MapConf(new PatientObservation { Name = name, Value = value, Min = min, Max = max }, grp); if (mapObs != null) return mapObs.Status; } if (conf.Grouped != null && conf.Grouped.ContainsKey(result.ToString())) { mapObs = await MapConf(new PatientObservation { Name = name, Value = value, Min = min, Max = max }, conf.Grouped[result.ToString()]); if (mapObs != null) return mapObs.Status; } mapObs = await MapConf(new PatientObservation { Name = name, Value = value, Min = min, Max = max }, conf); if (mapObs != null) return mapObs.Status; return StatusEnum.Type.Ok; } public async Task Map(T obs, bool onlyByName = false) where T : BasePatientObservation { var conf = onlyByName ? await Get(obs, onlyByName) : await Get(obs); if (conf == null) { _logger.LogDebug("Ignore Unknown Observation. {Name} {Code} {CodingSystem}", obs.Name, obs.Code, obs.CodingSystem); return IgnoreUnknownObservation ? null : obs; } if (string.IsNullOrEmpty(conf.Name)) { _logger.LogError("Config Name is null or empty. {conf}", conf); return null; } return await MapConf(obs, conf); } public async Task RemoveConfigItem(ObjectId id) { var item = await _configObservationRepository.FindById(id); if (item == null) return null; var deleted = await _configObservationRepository.Delete(id); // Invalidar CACHE (colección completa) await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.ConfigObservations)); return deleted; } public async Task Map(PatientTreatment treatment) { ConfigObservation? conf = null; foreach (var requestGiveCode in treatment.RequestedGiveCodes) conf = string.IsNullOrEmpty(requestGiveCode.CodingSystem) && string.IsNullOrEmpty(requestGiveCode.Identifier) ? await Get(requestGiveCode.Text) : await GetByCodeSysAndCode(requestGiveCode.CodingSystem, requestGiveCode.Identifier); if (conf == null) return _ignoreUnknownTreatment ? null : treatment; return conf.Name == null ? null : treatment; } public async Task Get(T obs, bool onlyByName = false) where T : BasePatientObservation { var items = await GetAllConfigs(); if (items.Count == 0) return null; if (onlyByName) { var configItem = items.FirstOrDefault(i => i.Name == obs.Name); if (configItem != null) return await Process(configItem); _logger.LogError("Error getting Config observation Item. Observation: {obs}", obs); return null; } ConfigObservation? item = null; if ((!string.IsNullOrEmpty(obs.Code) && !string.IsNullOrEmpty(obs.CodingSystem)) || obs.ParentData is { Code: not null, CodingSystem: not null }) foreach (var obsConfig in items) { if (obs.Code != null && obs.Code != obsConfig.Code) continue; if (obsConfig.CodingSystem != null && obs.CodingSystem != obsConfig.CodingSystem) continue; if (obsConfig.OriginalName != null && obs.Name != obsConfig.OriginalName) continue; if (obsConfig.ParentCode != null && obs.ParentData?.Code != obsConfig.ParentCode) continue; if (obsConfig.ParentCodingSystem != null && obs.ParentData?.CodingSystem != obsConfig.ParentCodingSystem) continue; if (obsConfig.ParentName != null && obs.ParentData?.Name != obsConfig.ParentName) continue; if (obsConfig.OriginalName == null && obsConfig.Name != null && obs.Name != null && obs.Name.Contains("Alarm") && obs.Name != obsConfig.Name) continue; if (obsConfig.Code == null && obsConfig.CodingSystem == null && obsConfig.ParentCode == null && obsConfig.ParentCodingSystem == null && obs.Name != null && !obs.Name.Contains("Alarm")) continue; if (obsConfig.Code == null && obsConfig is { CodingSystem: not null, ParentCode: null, ParentCodingSystem: null } && obs.Name != null && !obs.Name.Contains("Alarm")) continue; if (obsConfig.Code == null && obsConfig.CodingSystem == null && obsConfig.ParentCode == null && obsConfig.ParentCodingSystem != null && obs.Name != null && !obs.Name.Contains("Alarm")) continue; if (obsConfig.Code == null && obsConfig is { CodingSystem: not null, ParentCode: null, ParentCodingSystem: not null } && obs.Name != null && !obs.Name.Contains("Alarm")) continue; item = obsConfig; break; } else item = items.FirstOrDefault(i => i.Name == obs.Name); return item != null ? await Process(item) : null; } // public async Task> GetAlarmWithRecordingConfig(ObjectId patientId) // { // var configObservationId = await GetConfigObservationKeyFromPatientId(patientId); // var configObservation = await GetConfig(configObservationId); // return configObservation?.Items // .Where(i => i is { CodingSystem: "ADAS_ALARM", Alarm.Recording.Enabled: true }) // .ToList() ?? []; // } public async Task GetByCodeSysAndCode(string? codingSystem, string? code) { var filteredResult = await _configObservationRepository.GetByCodeSysAndCode(codingSystem, code); if (filteredResult != null) return await Process(filteredResult); _logger.LogWarning("Config observation item not found. CodingSystem: {codingSystem} code: {code} ", codingSystem ?? "null", code ?? "null"); return null; } public async Task Get(string? name) { if (string.IsNullOrEmpty(name)) return null; var result = await GetConfigByName(name); if (result == null) { // _logger.LogWarning( // "Config observation item not found. name: {name} configObservationId: {configObservationId} ", // name, name); return null; } return await Process(result); } public async Task UpdateConfig(ConfigObservation configObservationItem) { // if (!configObservationItem.Id.HasValue) // return await _configObservationRepository.InsertOneAsyncAndReturn(configObservationItem); var configObservation = await _configObservationRepository.FindById(configObservationItem.Id) ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); var updatedConfig = await _configObservationRepository.Update(configObservation); // Invalidar CACHE (colección completa) await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.ConfigObservations)); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, configObservation, updatedConfig!); return updatedConfig; } public async Task?> GetConfigObservationItem(string name) { var configObservationItems = await _configObservationRepository.FindAllByName(name); if (configObservationItems.Count != 0) return configObservationItems; _logger.LogWarning("Cant get config item, config not found, name: {name} ", name); throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); } public async Task GetSingleConfigObservationItem(string? code, string? codingSystem, string? name, string? originalName) { var matchingItem = await _configObservationRepository.GetSingleConfigObservationItem(code, codingSystem, name, originalName); if (matchingItem == null) throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundNoMatches); return matchingItem; } public async Task DeleteSingleConfigObservationItem(ConfigObservation configObservationItem) { var configObservation = await _configObservationRepository.FindById(configObservationItem.Id) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); var auxConfigObservation = await _auditService.DeepCopyAsync(configObservation); _ = await _configObservationRepository.DeleteAsync(configObservation.Id) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); // Invalidar CACHE (colección completa) await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.ConfigObservations)); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, auxConfigObservation, configObservation); return true; } public async Task> GetConfigObservationItemsByName(string name) { return await _configObservationRepository.FindAllByName(name); } public async Task CreateConfig(ConfigObservation configObservation) { var existing = await _configObservationRepository.FindById(configObservation.Id); if (existing != null) throw new BadRequestException(HttpEnum.ErrorMessage.ConflictCreationFailed); await _configObservationRepository.InsertOneAsyncAndReturn(configObservation); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, configObservation); return configObservation; } public async Task RemoveConfigItem(string itemName) { var configObservation = await GetConfigByName(itemName) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); var auxConfigObservation = await GetConfigByName(itemName) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, auxConfigObservation, configObservation); var result = await _configObservationRepository.Delete(configObservation.Id!); // Invalidar CACHE (colección completa) await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.ConfigObservations)); return result; } public async Task GetConfig(ObjectId configObservationId) { RefreshCachedConfigObservations(); if (CachedConfigObservations.TryGetValue(configObservationId, out var cached) && DateTime.Now <= cached.NextRefresh) return cached.ConfigObservation; var config = await _configObservationRepository.FindById(configObservationId); cached = new ConfigObservationCached { ConfigObservation = config ?? new ConfigObservation(), NextRefresh = _refreshTimeout.HasValue ? DateTime.Now.AddSeconds(_refreshTimeout.Value) : DateTime.MinValue }; CachedConfigObservations[configObservationId] = cached; return cached.ConfigObservation; } public async Task GetConfigByName(string name) { if (string.IsNullOrWhiteSpace(name)) return null; RefreshCachedConfigObservations(); var cachedItem = CachedConfigObservations.Values .FirstOrDefault(cached => DateTime.Now <= cached.NextRefresh && cached.ConfigObservation is { Name: not null } && cached.ConfigObservation.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); if (cachedItem != null) return cachedItem.ConfigObservation; var config = await _configObservationRepository.FindByName(name); if (config == null) return null; var newCachedItem = new ConfigObservationCached { ConfigObservation = config, NextRefresh = _refreshTimeout.HasValue ? DateTime.Now.AddSeconds(_refreshTimeout.Value) : DateTime.MinValue }; CachedConfigObservations[config.Id!] = newCachedItem; return config; } private void RefreshCachedConfigObservations() { if (!_refreshTimeout.HasValue) return; // Use ConcurrentDictionary's thread-safe features to identify and remove expired keys var keysToRemove = CachedConfigObservations .Where(k => DateTime.Now > k.Value.NextRefresh) .Select(k => k.Key) .ToList(); foreach (var key in keysToRemove) CachedConfigObservations.TryRemove(key, out _); } private Task Process(ConfigObservation confItem) { confItem.RetentionPolicy ??= _defaultRetentionPolicy; if (confItem.RetentionPolicy != RetentionPolicy.NoDelete && !confItem.RetentionPolicyValue.HasValue) { confItem.RetentionPolicyValue = _defaultRetentionPolicyValue; if (confItem.RetentionPolicyValue <= 0) confItem.RetentionPolicyValue = null; } else { confItem.RetentionPolicyValue = null; } return Task.FromResult(confItem); } private void RefreshCachedConfigObservationKeys() { try { if (!_refreshTimeout.HasValue) return; // Use ConcurrentDictionary's thread-safe features to identify and remove expired keys var keysToRemove = CachedConfigObservationKeys .Where(k => DateTime.Now > k.Value.NextRefresh) .Select(k => k.Key) .ToList(); foreach (var key in keysToRemove) CachedConfigObservationKeys.TryRemove(key, out _); } catch (Exception ex) { _logger.LogError("Error refreshing cached config observation keys. Exception: {ex}", ex); } } private async Task MapConf(T obs, ConfigObservation conf) where T : BasePatientObservation { _logger.LogTrace("Mapping observation: {obs}", obs); obs.Name = conf.Name; if (obs is PatientObservation) { var noCalculateStatusWithCodingSystem = _apiSettings.Value.NoCalculateStatusWithCodingSystem; if (noCalculateStatusWithCodingSystem != null && obs.CodingSystem == noCalculateStatusWithCodingSystem) return obs; if (obs is not PatientObservation pobs) return obs; if (conf.MaxAlert != null && (conf.ForceAlert || !pobs.Max.HasValue)) pobs.Max = conf.MaxAlert; if (conf.MinAlert.HasValue && (conf.ForceAlert || !pobs.Min.HasValue)) pobs.Min = conf.MinAlert; if (conf.MaxWarn.HasValue && (conf.ForceWarn || !pobs.MaxWarn.HasValue)) pobs.MaxWarn = conf.MaxWarn; if (conf.MinWarn.HasValue && (conf.ForceWarn || !pobs.MinWarn.HasValue)) pobs.MinWarn = conf.MinWarn; if (conf.LevelCondition != null) try { var evalCondition = await CSharpScript.EvaluateAsync(conf.LevelCondition, globals: pobs); if (int.TryParse(evalCondition.ToString(), out var evalInt)) pobs.Level = evalInt; else _logger.LogError("Error evaluating condition: {evalCondition} for obs: {pobs}", evalCondition, pobs); } catch (CompilationErrorException e) { _logger.LogError("Error evaluating expression for obs: {obs} error: {diagnostics}", obs, e.Diagnostics); } catch (Exception e) { _logger.LogError("Error evaluating expression for obs: {obs} error: {e}", obs, e); } pobs.Status = StatusEnum.Type.Ok; if (pobs.Value.IsNumber()) { if (pobs.Min.HasValue && pobs.Min > pobs.Value.ToDouble() && conf.Alert is null or '<') { pobs.Status = StatusEnum.Type.Alert; pobs.AlertColor = conf.AlertColor; } else if (pobs.Max.HasValue && pobs.Max < pobs.Value.ToDouble() && conf.Alert is null or '>') { pobs.Status = StatusEnum.Type.Alert; pobs.AlertColor = conf.AlertColor; } else if (pobs.MinWarn.HasValue && pobs.MinWarn > pobs.Value.ToDouble() && conf.Alert is null or '<') { pobs.Status = StatusEnum.Type.Warning; pobs.WarnColor = conf.WarnColor; } else if (pobs.MaxWarn.HasValue && pobs.MaxWarn < pobs.Value.ToDouble() && conf.Alert is null or '>') { pobs.Status = StatusEnum.Type.Warning; pobs.WarnColor = conf.WarnColor; } } else if (pobs.Value is string) { if (conf.AlertValues != null && conf.AlertValues.Any() && conf.AlertValues.Contains(pobs.Value)) pobs.Status = StatusEnum.Type.Alert; else if (conf.WarningValues != null && conf.WarningValues.Any() && conf.WarningValues.Contains(pobs.Value)) pobs.Status = StatusEnum.Type.Warning; } if (conf.Expires is > 0) { pobs.Expires = conf.Expires; //check if already is expired var expireTime = obs.Time.ToLocalTime().AddSeconds(Convert.ToDouble(conf.Expires)); pobs.Expired = DateTime.Now.CompareTo(expireTime) > 0; } pobs.ShowOnExpired = conf.ShowOnExpired; if (conf.ColorOnExpired != null) pobs.ColorOnExpired = conf.ColorOnExpired; if (conf.Persist != null) pobs.Persist = conf.Persist; if (conf.UiConfiguration != null && conf.UiConfiguration.Any()) pobs.UiConfiguration = conf.UiConfiguration; if (conf.Alarm != null) pobs.Alarm = conf.Alarm; if (conf.TimeFromMessageTime) if (pobs.MessageTime.CompareTo(DateTime.MinValue) != 0) pobs.Time = pobs.MessageTime; if (pobs.Units == null || conf.ForceUnits) pobs.Units = conf.Units; if (conf.CreateObservation != null) pobs.CreateObservation = conf.CreateObservation; pobs.CheckObservations = conf.CheckObservations; } if (obs is PatientObservationAlarm) { if (obs is not PatientObservationAlarm pobs) return obs; if (conf.Expires is > 0) { pobs.Expires = conf.Expires; //check if already is expired var expireTime = obs.Time.ToLocalTime().AddSeconds(Convert.ToDouble(conf.Expires)); pobs.Expired = DateTime.Now.CompareTo(expireTime) > 0; } if (conf.Persist != null) pobs.Persist = conf.Persist; if (conf.Alarm != null) pobs.AlarmConfig = conf.Alarm; if (conf.AlertColor != null) pobs.AlertColor = conf.AlertColor; if (conf.TimeFromMessageTime) if (pobs.MessageTime.CompareTo(DateTime.MinValue) != 0) pobs.Time = pobs.MessageTime; if (pobs.Units == null || conf.ForceUnits) pobs.Units = conf.Units; if (conf.CreateObservation != null) pobs.CreateObservation = conf.CreateObservation; pobs.CheckObservations = conf.CheckObservations; } _logger.LogTrace("Observation mapped: {obs}", obs); return obs; } private class ConfigObservationCached { public DateTime NextRefresh { get; set; } public ConfigObservation? ConfigObservation { get; set; } } private class ConfigObservationKeyCached { public DateTime NextRefresh { get; set; } public string? Key { get; set; } } }