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.DTO;
using adas_core.Domain.Models.MongoModels;
using Microsoft.Extensions.Logging;
using MongoDB.Bson;
namespace adas_core.Application.Services;
///
/// Provides the concrete implementation of the contract,
/// handling device-related service operations defined by the interface.
///
///
public class DeviceService : IDeviceService
{
private readonly IDeviceRepository _deviceRepository;
private readonly IObservationService _observationService;
private readonly IAlarmService _alarmService;
private readonly IConfigObservationService _configObservationService;
private readonly IPointOfCareService _pointOfCareService;
private readonly ILogger _logger;
///
/// Initializes a new instance of the class, injecting the required collaborators used to manage device-related domain operations.
///
/// The that provides persistence access for devices.
/// The used to coordinate point-of-care operations.
/// The used to emit diagnostic and operational logs.
/// The used to record and query observations.
/// The used to manage configured observation rules.
/// The used to raise and resolve alarms.
///
public DeviceService(
IDeviceRepository deviceRepository,
IPointOfCareService pointOfCareService,
ILogger logger,
IObservationService observationService,
IConfigObservationService configObservationService,
IAlarmService alarmService)
{
_deviceRepository = deviceRepository;
_pointOfCareService = pointOfCareService;
_logger = logger;
_observationService = observationService;
_configObservationService = configObservationService;
_alarmService = alarmService;
}
///
/// Converts a into a entity by mapping its properties. Applies a fallback to an empty list when PointOfCareIds is null, and to a new instance when Settings is null.
///
/// The data transfer object containing the device information to convert.
/// A new entity populated with the values from the supplied DTO.
///
public Device ToEntity(DeviceDto dto)
{
return new Device()
{
DeviceType = dto.DeviceType,
MacAddr = dto.MacAddr,
SerialNumber = dto.SerialNumber,
Name = dto.Name,
Battery = dto.Battery,
Color = dto.Color,
Connected = dto.Connected,
Ready = dto.Ready,
Uuid = dto.Uuid,
Key = dto.Key,
CreatedAt = dto.CreatedAt,
UpdatedAt = dto.UpdatedAt,
PointOfCareIds = dto.PointOfCareIds ?? new List(),
Settings = dto.Settings ?? new DeviceSettings()
};
}
///
/// Creates a new device entity from the provided DTO, sets the creation and update timestamps to the current UTC time, and persists it via the device repository.
///
/// The data transfer object containing the device information to be mapped and stored.
/// The created entity after successful insertion, or if the device could not be created.
///
public async Task Create(DeviceDto deviceDto)
{
var device = ToEntity(deviceDto);
device.CreatedAt = DateTime.UtcNow;
device.UpdatedAt = DateTime.UtcNow;
await _deviceRepository.InsertOneAsync(device);
return device;
}
///
/// Deletes an object identified by the specified by delegating to the device repository.
/// Returns true when the repository's delete operation yields a non-null result, and false when the result is null (e.g., the object was not found or could not be deleted).
///
/// The unique identifier of the object to delete.
/// A task that resolves to true if the object was deleted; otherwise, false.
///
public async Task Delete(ObjectId objectId)
{
return await _deviceRepository.DeleteAsync(objectId) != null;
}
///
/// Updates an existing device by mapping the provided DTO to a device entity, setting the update timestamp, and persisting the changes through the repository.
///
/// The data transfer object containing the updated device information.
/// The updated entity, or null if no device is returned.
///
public async Task Update(DeviceDto deviceDto)
{
var device = ToEntity(deviceDto);
device.UpdatedAt = DateTime.UtcNow;
await _deviceRepository.UpdateOneAsync(device.Id, device);
return device;
}
///
/// Processes an incoming device event by locating an existing device or creating a new one, then handling device-type-specific logic. Looks up the device using the MAC address, serial number, UUID, or key in that order, falling back to creating a new record when no match is found. When the device is a button, additional button management logic is invoked.
///
/// The data transfer object containing the device information from the event, used for lookup and creation.
/// The existing or newly created associated with the event.
///
public async Task ReceiveEvent(DeviceDto deviceDto)
{
Device? deviceExist = null;
if (!string.IsNullOrEmpty(deviceDto.MacAddr))
{
deviceExist = await _deviceRepository.FindByMacAddr(deviceDto.MacAddr);
}
if (deviceExist == null && deviceDto.SerialNumber != null)
{
deviceExist = await _deviceRepository.FindBySerialNumber(deviceDto.SerialNumber);
}
if (deviceExist == null && deviceDto.Uuid != null)
{
deviceExist = await _deviceRepository.FindByUuid(deviceDto.Uuid);
}
if (deviceExist == null && deviceDto.Key != null)
{
deviceExist = await _deviceRepository.FindByKey(deviceDto.Key);
}
if (deviceExist == null)
{
deviceExist = ToEntity(deviceDto);
deviceExist.CreatedAt = DateTime.UtcNow;
deviceExist.UpdatedAt = DateTime.UtcNow;
await _deviceRepository.InsertOneAsync(deviceExist);
}
else
{
await _deviceRepository.UpdateDeviceStats(deviceExist.Id, deviceDto);
}
switch (deviceDto.DeviceType)
{
case DeviceType.Unknown:
break;
case DeviceType.Button:
await ManageDeviceButton(deviceExist, deviceDto);
break;
}
return deviceExist;
}
///
/// Handles device button actions by dispatching the configured action type (sending an observation or alarm) when both the device's configured action and the received click event are present.
///
/// The existing device whose configured action settings determine which action to execute.
/// The incoming device event payload providing the click type that triggers the action.
///
private async Task ManageDeviceButton(Device deviceExist, DeviceDto deviceDto)
{
if (deviceExist.Settings?.Action?.Type != null && deviceDto.Event?.ClickType != null)
{
switch (deviceExist.Settings.Action.Type)
{
case DeviceActionType.SendObs:
await SendObservationOnAction(deviceExist.Settings.Action, deviceDto.Event.ClickType.Value, deviceExist.PointOfCareIds);
break;
case DeviceActionType.SendAlarm:
await SendAlarmOnAction(deviceExist.Settings.Action, deviceDto.Event.ClickType.Value, deviceExist.PointOfCareIds);
break;
}
}
}
///
/// Sends a patient observation alarm based on a device action and the type of click event that triggered it.
/// The alarm value is selected from the device action's configuration according to the click type
/// (SingleClick, DoubleClick, or Hold) and is dispatched for each point of care that has an associated patient.
/// The method exits early when the configuration observation cannot be found or when the action has no value defined for the current click type.
///
/// The device action containing the configuration observation and the per-click-type alarm values to use.
/// The click event that triggered the action, which determines which value from the device action is sent.
/// The list of point of care identifiers whose patients should receive the alarm.
///
private async Task SendAlarmOnAction(
DeviceAction settingsAction,
ClickType eventClickType,
List deviceExistPointOfCareIds)
{
var configObs = await _configObservationService.GetConfigById(settingsAction.ConfigObservationId);
if(configObs == null) return;
var obsData = new ObservationData
{
Code = configObs.Code,
CodingSystem = configObs.CodingSystem,
Time = DateTime.UtcNow,
Text = configObs.Name,
};
var obs = new PatientObservationAlarm
{
InactivationState = new InactivationState{ Visual = AlarmEnum.AudioVideoState.Enabled, Audio = AlarmEnum.AudioVideoState.Enabled},
MessageTime = DateTime.UtcNow,
Persist = true,
Code = obsData.Code,
CodingSystem = obsData.CodingSystem,
Name = configObs.Name,
Time = DateTime.UtcNow
};
foreach (var pocId in deviceExistPointOfCareIds)
{
var data = await _pointOfCareService.GetInfo(pocId, null, true);
if (data != null && data.Patient?.Id != null)
{
obs.PatientId = data.Patient.Id;
obs.Patient = data.Patient;
switch (eventClickType)
{
case ClickType.SingleClick:
if(settingsAction.ValueOnSingleClick == null) return;
obs.Value = settingsAction.ValueOnSingleClick;
break;
case ClickType.DoubleClick:
if(settingsAction.ValueOnDoubleClick == null) return;
obs.Value = settingsAction.ValueOnDoubleClick;
break;
case ClickType.Hold:
if(settingsAction.ValueOnHoldClick == null) return;
obs.Value = settingsAction.ValueOnHoldClick;
break;
}
// Process Obs on service
await _alarmService.ProcessAlarmObservations(new List{obs},new List(){new (){Name = configObs.Name, Value = obs.Value, Code = obsData.Code, CodingSystem = obsData.CodingSystem}}, data.Patient, DateTime.UtcNow, obsData);
}
}
}
///
/// Sends a patient observation derived from the configuration linked to a device action, applying the value
/// associated with the specified click type (single, double, or hold) for each point of care patient found.
/// The method short-circuits when the configuration observation is missing or when no value is defined for
/// the given click type.
///
/// The device action whose associated configuration observation and per-click values drive the observation payload.
/// The type of click event that triggered the action; selects which value (single, double, or hold) is assigned to the observation.
/// The list of point of care identifiers whose resolved patients will receive the generated observation.
/// A task that represents the asynchronous send operation; no meaningful business result is returned.
///
private async Task SendObservationOnAction(
DeviceAction settingsAction,
ClickType eventClickType,
List deviceExistPointOfCareIds)
{
var configObs = await _configObservationService.GetConfigById(settingsAction.ConfigObservationId);
if(configObs == null) return;
var obsData = new ObservationData
{
Code = configObs.Code,
CodingSystem = configObs.CodingSystem,
Time = DateTime.UtcNow,
Text = configObs.Name,
};
var obs = new PatientObservation
{
MessageTime = DateTime.UtcNow,
Persist = true,
Code = obsData.Code,
CodingSystem = obsData.CodingSystem,
Name = configObs.Name,
Time = DateTime.UtcNow
};
foreach (var pocId in deviceExistPointOfCareIds)
{
var data = await _pointOfCareService.GetInfo(pocId, null, true);
if (data != null && data.Patient?.Id != null)
{
obs.PatientId = data.Patient.Id;
obs.Patient = data.Patient;
switch (eventClickType)
{
case ClickType.SingleClick:
if(settingsAction.ValueOnSingleClick == null) return;
obs.Value = settingsAction.ValueOnSingleClick;
break;
case ClickType.DoubleClick:
if(settingsAction.ValueOnDoubleClick == null) return;
obs.Value = settingsAction.ValueOnDoubleClick;
break;
case ClickType.Hold:
if(settingsAction.ValueOnHoldClick == null) return;
obs.Value = settingsAction.ValueOnHoldClick;
break;
}
// Process Obs on service
_observationService.ProcessObservations(new List{obs}, data.Patient, DateTime.UtcNow, obsData);
}
}
}
}