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;
///
/// Provides a concrete implementation of the contract for observing configuration state.
///
///
/// Acts as the default service type that fulfills the configuration observation interface.
///
///
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;
///
/// Initializes a new instance of the class, storing its required dependencies and reading configuration values from the supplied instances to establish internal operational defaults such as the refresh timeout, unknown treatment handling, and retention policy.
///
/// The used to access configuration observation data.
/// The providing API configuration values, including the refresh interval and retention policy defaults.
/// The providing cache configuration values.
/// The used for diagnostic logging.
/// The used to perform unit-related operations.
/// The used to access the current HTTP context.
/// The used to record local audit entries.
/// The used for caching operations.
///
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;
}
///
/// Retrieves all configuration observations using a cache-aside strategy. If no cached value exists, the data is fetched from the repository and cached using the key and TTL determined by the cache settings.
///
/// A token to monitor for cancellation requests.
/// A collection of all entries, sourced from cache when available or from the repository otherwise.
///
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;
}
///
/// Retrieves a compact representation of all configuration observations by returning the total item count.
///
/// A containing the total number of configuration observations.
///
public async Task GetAllCompact()
{
var count = await _configObservationRepository.Count();
return new ConfigObservationDto { ItemCount = count };
}
///
/// Retrieves a paginated list of items from the repository along with the total count, used to build pagination metadata for the response.
///
/// The pagination filter containing the requested page number and page size used to retrieve the items and populate the response metadata.
/// A containing the items for the requested page and the total count of all available items.
///
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);
}
///
/// Retrieves a configuration observation by its unique identifier from the repository.
/// Returns null if no matching configuration observation is found.
///
/// The unique identifier of the configuration observation to retrieve.
/// The configuration observation matching the specified identifier, or null if not found.
///
public async Task GetConfigById(ObjectId id)
{
return await _configObservationRepository.FindById(id) ?? null;
}
///
/// Retrieves the list of configuration names associated with the specified identifier by delegating to the configuration observation repository.
///
/// The identifier used to look up the related configuration names.
/// A task that represents the asynchronous operation, containing the list of configuration names matching the given identifier.
///
public async Task> GetConfigNames(string id)
{
return await _configObservationRepository.GetConfigNames(id);
}
///
/// Retrieves the list of configuration names from the configuration observation repository.
///
/// A task that represents the asynchronous operation. The task result contains a list of configuration names.
///
public async Task> GetConfigNames()
{
return await _configObservationRepository.GetConfigNames();
}
///
/// Determines the retention action to apply for a patient observation by resolving its configured retention policy. Falls back to a "NoDelete" retention policy with no value when the observation has no associated configuration or its retention policy is null.
///
/// The patient observation type, constrained to .
/// The patient observation for which the retention action is being evaluated.
/// A task containing the resolved , or null when no configuration is available.
///
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);
}
///
/// Resolves the status of a grouped patient observation by retrieving the configuration for the given name and mapping the observation through group-specific, result-specific, or default configuration. Returns when no configuration exists for the name or when the mapping does not produce a status.
///
/// The grouped field whose Group key is used to look up group-specific configuration.
/// The grouped observation result whose name is used to look up result-specific configuration.
/// The observation name used to retrieve the configuration.
/// The observation value included in the mapping.
/// The optional minimum reference value included in the mapping.
/// The optional maximum reference value included in the mapping.
/// A task that resolves to the computed from the mapped observation, or when no applicable configuration or mapping status is found.
///
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;
}
///
/// Maps a patient observation to a configured representation by looking up its corresponding configuration
/// and applying the mapping. When no matching configuration is found, returns null if unknown
/// observations should be ignored, or the original observation otherwise. If the resolved configuration
/// has an empty name, the mapping is aborted and null is returned.
///
/// The patient observation to be mapped.
/// If true, the configuration lookup is performed by name only; otherwise the full lookup is used.
/// The mapped observation, the original observation when unknown observations are allowed, or null when mapping is ignored or the configuration is invalid.
///
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);
}
///
/// Removes a configuration observation item by its identifier. Returns null when the item does not exist,
/// otherwise deletes it from the repository and invalidates the cached collection of configuration observations.
///
/// The unique identifier of the configuration observation to remove.
/// The removed if it was found and deleted; otherwise, null.
///
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;
}
///
/// Maps a by resolving the configuration observation for each of its requested give codes. Looks up the configuration by text when both the coding system and identifier are empty, otherwise by coding system and identifier. Returns null when the configuration is unknown and unknown treatments are ignored, or when the resolved configuration has no name; otherwise returns the original treatment.
///
/// The patient treatment whose requested give codes are resolved against the configuration store.
/// The original if a valid configuration is found, or null when the treatment should be discarded.
///
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;
}
///
/// Retrieves a that matches the supplied patient observation, either by name only or by a combination of code, coding system, and parent data fields.
///
/// The patient observation whose matching configuration should be resolved.
/// When true, the lookup is restricted to matching by only; otherwise matching also considers code, coding system, and parent observation data.
/// A containing the matched processed via Process, or null if no configuration items are available or no match is found.
///
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() ?? [];
// }
///
/// Retrieves a that matches the specified coding system and code, processing it before returning.
/// If no matching observation is found, a warning is logged and is returned.
///
/// The coding system identifier used to filter the observation. May be .
/// The code value used to filter the observation. May be .
/// A processed if a match is found; otherwise, .
///
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;
}
///
/// Retrieves a by its name, returning null when the name is not provided or the configuration is not found.
///
/// The name of the configuration to look up; if null or empty, the method returns null.
/// A when a matching configuration is found and successfully processed; otherwise, 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);
}
///
/// Updates an existing identified by its id and returns the updated entity.
/// Throws a not found exception when the configuration observation does not exist, invalidates the related cache entries, and records an audit log of the change.
///
/// The configuration observation payload containing the identifier of the record to update.
/// The updated .
/// Thrown when no configuration observation exists for the supplied id.
///
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;
}
///
/// Retrieves configuration observation items matching the specified name. Returns the matching items if any are found; otherwise logs a warning and throws a .
///
/// The name used to look up the configuration observation items.
/// A collection of items matching the specified name.
/// Thrown when no configuration observation items are found for the given name.
///
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);
}
///
/// Retrieves a single item that matches the specified code, coding system, name, and original name.
/// Throws a not-found exception when no matching item exists in the repository.
///
/// The code used to identify the configuration observation item.
/// The coding system associated with the item.
/// The name of the configuration observation item.
/// The original name of the configuration observation item.
/// The matching item.
/// Thrown when no matching configuration observation item is found.
///
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;
}
///
/// Deletes a single configuration observation item, invalidating the related cache entries and recording an audit log of the operation.
/// Throws a conflict exception if the item does not exist or the delete operation cannot be completed.
///
/// The configuration observation item to delete; its identifier is used to locate the existing record.
/// A task that resolves to true when the item is successfully deleted.
/// Thrown when no configuration observation is found with the specified identifier, or when the underlying delete operation fails.
///
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;
}
///
/// Retrieves configuration observation items that match the specified name.
///
/// The name used to filter configuration observation items.
/// A collection of items matching the specified name.
///
public async Task> GetConfigObservationItemsByName(string name)
{
return await _configObservationRepository.FindAllByName(name);
}
///
/// Creates a new configuration observation after verifying that no existing record shares the same identifier.
///
/// The configuration observation entity to persist.
/// The created if the operation succeeds.
/// Thrown when a configuration observation with the same identifier already exists.
///
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;
}
///
/// Removes a configuration item identified by its name, creating an audit log entry prior to deletion and invalidating the related cache.
/// Throws a conflict exception when no configuration item with the specified name is found.
///
/// The name of the configuration item to remove.
/// The removed , or null if the repository did not return a result.
/// Thrown when no configuration item is found with the specified name.
///
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;
}
///
/// Retrieves a configuration observation by its identifier, using an in-memory cache with a configurable refresh timeout to reduce repository calls.
/// Returns the cached value when available and not yet expired; otherwise fetches from the repository and caches the result, falling back to a new empty when the repository does not find a matching record.
///
/// The unique identifier of the configuration observation to retrieve.
/// The configuration observation obtained from cache or repository, or a new empty instance when no matching record exists.
///
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;
}
///
/// Retrieves a configuration observation by its name, using a time-limited in-memory cache before falling back to the repository.
/// Returns null if is null, empty, or whitespace, or if no matching configuration exists in the cache or repository.
/// Cache hits require a non-expired NextRefresh and use a case-insensitive name comparison.
///
/// The case-insensitive name of the configuration observation to look up.
/// The matching , or null if not found or the name is invalid.
///
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;
}
///
/// Removes expired entries from the cached configuration observations when a refresh timeout is configured,
/// performing an early return if no refresh timeout is set.
///
///
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 _);
}
///
/// Processes a configuration observation by applying the default retention policy when none is set, and normalizing the associated retention policy value based on the selected policy.
///
/// The configuration observation to process. Its retention policy and retention policy value are updated in place.
/// A task containing the processed .
///
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);
}
///
/// Removes expired entries from the cached configuration observation keys based on the configured refresh timeout.
/// If no refresh timeout is set, the method returns without performing any cleanup.
/// Any exceptions encountered during the cleanup are caught and logged.
///
///
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);
}
}
///
/// Maps configuration settings from a onto a ,
/// applying alert and warning thresholds, evaluating dynamic level conditions, and computing the observation status
/// for numeric and string values. Handles and
/// subtypes with their respective properties, including expiration, units, colors, and UI configuration.
///
/// The observation instance to enrich with configuration values. Modified in place.
/// The configuration observation providing thresholds, colors, expiration, and other settings to apply.
/// The mapped observation, returned as-is when the observation's coding system is configured to skip status calculation.
///
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;
}
///
/// Represents a private cached container for configuration observation data.
///
///
/// This type is intended to be used internally to store and reuse configuration observation results.
///
///
private class ConfigObservationCached
{
public DateTime NextRefresh { get; set; }
public ConfigObservation? ConfigObservation { get; set; }
}
///
/// Represents a private cache entry for configuration observation keys, used to store and retrieve previously computed key values associated with configuration observations.
///
///
private class ConfigObservationKeyCached
{
public DateTime NextRefresh { get; set; }
public string? Key { get; set; }
}
}