using System.Collections; using adas_core.Application.Services.Interfaces; using adas_core.Domain.Enums; using adas_core.Domain.Models; using adas_core.Domain.Models.GroupedObservations; using adas_core.Domain.Models.MongoModels; using adas_core.Domain.Utils; namespace adas_core.Application.Services; public class ObservationDemoService( IConfigObservationService configObservationService, Lazy alarmService) : IObservationDemoService { /// /// Generates a list of patient observation alarms based on the provided alarm fields, randomly skipping fields and skipping those with no matching configuration. /// /// The patient to associate with the generated alarms. /// The list of fields used to look up alarm configurations and produce alarms. /// A task that represents the asynchronous operation, containing the list of mapped instances that were successfully resolved. public async Task> GenerateAlarmByField(Patient patient, List dataAlarmfields) { var listToReturn = new List(); foreach (var field in dataAlarmfields) { var sendingRandom = new Random(); if (sendingRandom.Next(0, 2) == 0 || string.IsNullOrEmpty(field.Name)) continue; var firstCof = await configObservationService.GetConfigObservationItemsByName(field.Name); var conf = firstCof.FirstOrDefault(); if (conf == null) continue; var alarmObs = new PatientObservationAlarm { Patient = patient, PatientId = patient.Id, AlarmConfig = conf.Alarm, InactivationState = new InactivationState { Audio = AlarmEnum.AudioVideoState.Enabled, Acknowledge = true, Visual = AlarmEnum.AudioVideoState.Enabled }, EventPhase = AlarmEnum.EventPhase.Continue, Type = AlarmEnum.ObservationAlarmType.Sp, Expires = conf.Expires, State = AlarmEnum.ObservationAlarmState.Active, Time = DateTime.UtcNow, Persist = false, MessageTime = DateTime.UtcNow, Code = conf.Code, PriorityLevel = conf.DemoConfig?.PriorityAlarm, Name = conf.Name, ParentData = new ParentDataClass { Code = conf.ParentCode, Name = conf.ParentName, CodingSystem = conf.CodingSystem }, Units = conf.Units, Value = GenerateValueRandom(conf, null) ?? 0 }; var obs = await alarmService.Value.MapObservationsByName(alarmObs); if (obs != null) listToReturn.Add(obs); } return listToReturn; } /// /// Generates a list of patient observations for the specified patient based on the provided data fields, using the corresponding configuration items to populate observation values, thresholds, and metadata. Fields without a name or without a matching configuration are skipped. When a configuration includes demo settings, the resulting observation is adjusted with a randomized initial date and, if required, an end time. /// /// The patient to associate the generated observations with. /// The list of fields used to look up observation configurations and drive observation generation. /// A task that returns the list of generated instances; entries are omitted when no configuration is found for a field. public async Task> GenerateObservationByField(Patient patient, List dataFields) { var listToReturn = new List(); var rnd = new Random(); foreach (var field in dataFields) { if (string.IsNullOrEmpty(field.Name)) continue; var conf = await configObservationService.GetConfigObservationItemsByName(field.Name); var firstCof = conf.FirstOrDefault(); if (firstCof == null) continue; var patientObs = new PatientObservation { Patient = patient, PatientId = patient.Id, Max = firstCof.MaxAlert, Min = firstCof.MinAlert, MaxWarn = firstCof.MaxWarn, MinWarn = firstCof.MinWarn, ShowOnExpired = firstCof.ShowOnExpired, Expires = firstCof.Expires, UiConfiguration = firstCof.UiConfiguration, MessageTime = DateTime.UtcNow, Code = firstCof.Code, ColorOnExpired = firstCof.ColorOnExpired, AlertColor = firstCof.AlertColor, WarnColor = firstCof.WarnColor, CodingSystem = firstCof.CodingSystem, Alarm = firstCof.Alarm, InsertMode = firstCof.InsertMode, Name = field.Name, Time = DateTime.UtcNow, ParentData = new ParentDataClass { Code = firstCof.ParentCode, Name = firstCof.ParentName, CodingSystem = firstCof.CodingSystem }, Units = firstCof.Units, Value = GenerateValueRandom(firstCof, null) ?? 0 }; var result = await configObservationService.Map(patientObs, true); if (firstCof.DemoConfig?.RequireInitDate == true && result != null) { var rndRestNmbHours = rnd.Next(3, 23); var initDate = DateTime.UtcNow.AddHours(-rndRestNmbHours); // Si se requiere fecha de fin, devolvemos un objeto con ambas if (firstCof.DemoConfig?.RequireEndDate == true) { result.Time = initDate; result.EndTime = initDate.AddHours(1); } // Si no, solo devolvemos la fecha inicial result.Time = initDate; } if (result != null) listToReturn.Add(result); } return listToReturn; } /// /// Generates a for the given patient and grouped field, producing time-spaced observations for each configured name and result type based on the field's regularity. /// /// The patient to associate the generated grouped observation with. /// The grouped field configuration that defines the names, results, regularity, and maximum number of observations to generate. /// A task that returns the populated containing the generated observations. public async Task GenerateGroupedObservation(Patient patient, GroupedField groupedField) { if (groupedField.Names.IsNullOrEmpty() && groupedField.Name != null) groupedField.Names.Add(groupedField.Name); var rnd = new Random(); var startTime = DateTime.UtcNow; var go = new GroupedObservation { Name = groupedField.Name, PatientId = patient.Id, Group = groupedField.Group, Observations = [] }; foreach (var groupedFieldName in groupedField.Names) { var conf = await configObservationService.GetConfigObservationItemsByName(groupedFieldName); var firstCof = conf.FirstOrDefault(); if (firstCof == null) continue; foreach (var result in groupedField.Result) { object? prevValue = null; for (var i = 0; i <= groupedField.Max; i++) { var time = startTime; time = groupedField.Regularity switch { GroupedObservationEnum.Regularity.Day => time.AddDays(-i), GroupedObservationEnum.Regularity.Minute => time.AddMinutes(-i), GroupedObservationEnum.Regularity.Second => time.AddSeconds(-i), _ => time.AddHours(-i) }; var val = new GroupedObservation.GroupedObservationObs { Name = groupedFieldName, MaxAlert = firstCof?.MaxAlert, MinAlert = firstCof?.MinAlert, Time = time, IsFilled = false }; var value = new GroupedObservation.GroupedObservationObsValue( GenerateValueRandom(firstCof, prevValue) ?? rnd.Next(0, 30), time); prevValue = value.Value; var propertyInfo = val.GetType().GetProperty(result.ToString()); if (propertyInfo != null && propertyInfo.CanWrite) propertyInfo.SetValue(val, value); var valueType = await configObservationService.GroupedObservationStatus( groupedField, result, groupedFieldName, val, firstCof?.MaxAlert, firstCof?.MinAlert ); value.Type = valueType; go.Observations.Add( val ); } if (firstCof?.DemoConfig?.RequireInitDate.HasValue == true && firstCof.DemoConfig.RequireInitDate.Value) break; } } return go; } /// /// Generates a random value (or list of values) for a given configuration, falling back to "-" when no configuration is available. When the configuration specifies an array, produces multiple random values using the configured count; otherwise returns a single random value. /// /// The configuration observation whose DemoConfig drives the value generation. If null or its DemoConfig is null, the method returns "-". /// The previous value, passed through to PickSingleValue as context for the new random value generation. /// A list of generated values when the configuration indicates an array, or a single generated value otherwise. Null is returned when no value could be picked. private static object? GenerateValueRandom(ConfigObservation? firstCof, object? prevValue) { if (firstCof?.DemoConfig == null) return "-"; // 1. Determinar cuántos valores necesitamos var count = firstCof.DemoConfig?.ValueIsArray == true ? firstCof.DemoConfig?.RandomCount ?? 1 : 1; var results = new List(); for (var i = 0; i < count; i++) { var singleValue = PickSingleValue(firstCof.DemoConfig, prevValue); if (singleValue != null) results.Add(singleValue); } // 2. Si no es un array, devolvemos solo el primer elemento return firstCof.DemoConfig?.ValueIsArray == true ? results : results.FirstOrDefault(); } /// /// Picks a single value from a following a defined priority: first selects randomly from a predefined list of options when SetInValue is enabled, otherwise generates a random numeric value within the configured range, optionally applying a percentage-based variation to a previous value. Returns null if the configuration is null, if the options list is empty, or if random value generation is disabled. /// /// The configuration that defines how the value should be selected, including the list of options, the numeric range, and the selection mode. /// The previously selected value, used as the base for percentage-based variation when generating a new numeric value; ignored when no previous integer value is available. /// A randomly selected value from the configured options or a generated numeric value clamped within the defined range; null when the configuration does not allow a value to be produced. private static object? PickSingleValue(DemoConfig? config, object? prevValue) { if (config == null) return null; var rnd = new Random(); // Prioridad 1: Selección de una lista de opciones (SetInValue) if (config is { SetInValue: true, ValueOption: not null }) { if (config.ValueOption is List { Count: > 0 } options) { var index = rnd.Next(options.Count); return options.ElementAt(index); } // Caso especial si ValueOption llega como un JArray o lista genérica if (config.ValueOption is IEnumerable list) { var tempList = list.Cast().ToList(); return tempList[rnd.Next(tempList.Count)]; } } // Prioridad 2: Generación numérica por rango (SetRandomValue) if (config.SetRandomValue == false) return null; // 1. Extraemos los límites primero para usarlos en cualquier caso var minLimit = config.MinValue ?? 0; var maxLimit = config.MaxValue ?? 100; if (prevValue is not int values) // Caso base: Si no hay valor previo, generamos uno aleatorio dentro del rango permitido return rnd.Next(minLimit, maxLimit + 1); if (values == 0) values = 1; // 2. Calculamos la variación (entre 5% y 10%) var porcentaje = rnd.Next(5, 10) / 100.0; var direccion = rnd.Next(0, 2) == 0 ? -1 : 1; // 3. Calculamos el valor base redondeado var calculado = (int)Math.Round(values + values * porcentaje * direccion); // 4. Forzamos que esté dentro del rango [minLimit, maxLimit] return Math.Clamp(calculado, minLimit, maxLimit); } }