using System.Net;
using System.Net.Http.Headers;
using System.Text;
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.MongoModels;
using adas_core.Domain.Models.Recording;
using adas_core.Domain.Models.Responses;
using adas_core.Domain.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Serilog;
using Patient = adas_core.Domain.Models.MongoModels.Patient;
namespace adas_core.Application.Services;
///
/// Provides functionality for recording operations as defined by the contract.
///
public class RecordingService : IRecordingService
{
private readonly IAuthService _authService;
private readonly IClientMessageService _clientMessageService;
private readonly HttpClient _httpClient;
private readonly ILogger _logger;
private readonly Lazy _patientService;
private readonly IPublisherService _publisherService;
private readonly RabbitMqSettings _rabbitMqSettings;
private readonly RecordingSettings _recordingSettings;
private readonly bool _startRecordingWithoutPatientNumber;
private readonly ISubscribersService _subscribersService;
private readonly string? _url;
private AccessGrant? _accessGrant;
public RecordingService(IOptions rabbitMqSettings,
IOptions recordingSettings,
ILogger logger,
IHttpClientFactory httpClientFactory,
IPublisherService publisherService,
IAuthService authService,
IOptions apiSettings, IClientMessageService clientMessageService,
ISubscribersService subscribersService,
Lazy patientService)
{
_logger = logger;
_httpClient = httpClientFactory.CreateClient();
_accessGrant ??= new AccessGrant();
_rabbitMqSettings = rabbitMqSettings.Value;
_recordingSettings = recordingSettings.Value ??
throw new Exception("RecordingSettings must be defined on appSettings");
_httpClient.Timeout = new TimeSpan(0, 0, _recordingSettings.HttpClientTimeout);
_url = _recordingSettings.RecordingApiUrl;
_publisherService = publisherService;
_authService = authService;
_clientMessageService = clientMessageService;
_subscribersService = subscribersService;
_patientService = patientService;
RecordingQueueName = _rabbitMqSettings.RecordingQueue;
ErrorRecordingQueueName = $"{RecordingQueueName}_Error";
_startRecordingWithoutPatientNumber = apiSettings.Value.StartRecordingWithoutPatientNumber;
}
private string RecordingQueueName { get; }
private string ErrorRecordingQueueName { get; }
///
/// Sends a cancel recording request to the recording API for the specified patient and point of care.
/// Throws an exception if the recording API URL is not configured or a valid authentication token cannot be obtained.
///
/// The patient whose recording in progress should be cancelled.
/// The point of care associated with the recording.
/// A task that resolves to true if the cancel request succeeded; otherwise, false.
/// Thrown when the recording API URL is not defined or the authentication token is null or empty.
public async Task SendCancelRecordingToRecordingApi(Patient patient, PointOfCare poc)
{
var token = await _authService.GetToken();
if (string.IsNullOrEmpty(_url) || string.IsNullOrEmpty(token))
throw new Exception(
"send recording data to recording api require RecordingOrApiUrl defined and valid token");
var body = JsonConvert.SerializeObject(GenerateRecordingData(patient, poc, DateTime.MinValue,
null, null, null, null, AlarmEnum.Severity.None, ""));
var request =
new HttpRequestMessage(HttpMethod.Post,
$"{_url}/videos/delete-video-in-progress") //las fechas no se mandan
{
Content = new StringContent(body, Encoding.UTF8)
};
request.Headers.TryAddWithoutValidation("Authorization", $"Bearer {token}");
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
var response = await _httpClient.SendAsync(request);
_logger.LogDebug(
"send cancel to recording api status code:{responseStatusCode} url: {url} body: {body}",
response.StatusCode, _url, body);
return response.IsSuccessStatusCode;
}
///
/// Sends the manual recording data to the processing queue, forwarding the recording's start and stop times along with the patient and point of care information.
///
/// The patient associated with the manual recording.
/// The point of care where the recording was performed.
/// The manual recording whose start and stop times are forwarded to the queue.
/// A flag indicating whether the recording is being started or stopped.
public async Task SendRecordingData(Patient patient, PointOfCare poc, ManualRecording manualRecording,
bool start)
{
await SendRecordingDataToQueue(patient, poc, manualRecording.Recording?.StartRecordingTime,
manualRecording.Recording?.StopRecordingTime, null, null, AlarmEnum.Severity.None, null, start);
}
///
/// Sends automatic recording data for the specified patient and point of care to the recording data queue.
/// Validates that the alarm name can be parsed to ; returns false if the name is invalid or if an exception occurs during the send operation.
///
/// The patient associated with the automatic recording.
/// The point of care where the recording was captured.
/// The automatic recording payload, including alarm name, start/stop/event timestamps, severity, and description.
/// A task that resolves to true if the recording data was queued successfully; otherwise, false when the alarm name cannot be parsed or the send operation fails.
public async Task SendAutoRecordingData(Patient patient, PointOfCare poc, RecordingData automaticRecording)
{
try
{
if (!Enum.TryParse(automaticRecording.AlarmName, out var alarmName))
return false;
await SendRecordingDataToQueue(patient, poc, automaticRecording.StartRecordingTime,
automaticRecording.StopRecordingTime, automaticRecording.EventTime,
alarmName, automaticRecording.Severity, automaticRecording.AlarmDescription,
type: AlarmEnum.Type.Auto);
return true;
}
catch (Exception ex)
{
Log.Error("Exception sending automatic recording data. Exception: {ex}", ex);
return false;
}
}
///
/// Sends recording data for a patient to the recording message queue, broadcasting the recording after a successful send.
/// Validates the patient number when the start-without-patient-number feature is disabled, ensures the point of care is in use,
/// defaults a missing alarm description to "UNKNOWN", and retries the send on transient HTTP connection failures up to the configured maximum,
/// routing the last failure to the error recording queue.
///
/// The patient whose recording is being sent; its patient number is validated when required.
/// The point of care (box) that must be in use to allow the recording to be sent.
/// Optional recording date used to build the recording payload.
/// Optional end date used to build the recording payload.
/// Optional event date used to build the recording payload.
/// Optional alarm name to associate with the recording.
/// The alarm severity for the recording.
/// Optional alarm description; when null or empty it is replaced with "UNKNOWN".
/// Flag indicating whether the recording is a start event; defaults to true.
/// The alarm type for the recording; defaults to .
/// Thrown when the point of care status is not , including the serialized point of care in the message.
public async Task SendRecordingDataToQueue(Patient patient, PointOfCare poc, DateTime? date, DateTime? endDate,
DateTime? eventDate, AlarmEnum.Name? alarmName, AlarmEnum.Severity severity, string? alarmDescription,
bool start = true, AlarmEnum.Type type = AlarmEnum.Type.Manual)
{
if (!_startRecordingWithoutPatientNumber && string.IsNullOrEmpty(patient.PatientNumber))
{
_logger.LogError(
"Error sending recording data to queue. Patient number is null or empty. PatientId: {patientId}",
patient.PatientId);
return;
}
if (poc.Status != StatusEnum.PointOfCare.InUse)
throw new Exception($"Box does not contain patients:->{JsonConvert.SerializeObject(poc)}");
if (string.IsNullOrEmpty(alarmDescription)) alarmDescription = "UNKNOWN";
var retryCount = 1;
while (retryCount <= _rabbitMqSettings.MaxRetries)
{
try
{
ApiRequest recordingRequest = new()
{
Recording = GenerateRecordingData(patient, poc, date, endDate, eventDate, null, alarmName,
severity, alarmDescription, type)
};
_logger.LogDebug("Send recording data patient {patient}", patient);
await _publisherService.SendMessage(recordingRequest, RecordingQueueName);
if (recordingRequest.Recording != null)
_ = SendRecordingBroadcast([recordingRequest.Recording]);
return;
}
catch (HttpRequestException ce)
{
retryCount++;
_logger.LogError("Connection error sending recording data to queue. {ce} Retrying...", ce.Message);
}
catch (Exception e)
{
_logger.LogError("Error sending recording data to queue.{eError} {eMessage} ", e, e.Message);
await _publisherService.SendMessage(e.Message, ErrorRecordingQueueName);
throw;
}
if (retryCount == _rabbitMqSettings.MaxRetries)
await _publisherService.SendMessage("Error: Connection retries exceeded.", ErrorRecordingQueueName);
}
}
///
/// Asynchronously retrieves the list of in-progress recordings for the specified room from the recording API.
/// Performs token-based authentication, retries transient HTTP request failures up to the configured maximum, and falls back to an empty list or null when the API is unreachable, the room has no recordings, or retries are exhausted.
///
/// The identifier of the room whose recordings should be fetched.
/// A task that resolves to a list of for the room, an empty list when the request fails or the room has no recordings, or null when the recording API URL or authentication token is not configured or when the maximum number of retries is reached.
public async Task?> GetRecordings(int roomId)
{
var retryCount = 1;
while (retryCount <= _rabbitMqSettings.MaxRetries)
try
{
var token = await _authService.GetToken();
if (string.IsNullOrEmpty(_recordingSettings.RecordingApiUrl) || string.IsNullOrEmpty(token))
{
Log.Warning(
"Not recording url defined at recordingSettings or empty token, ignoring recordings");
return null;
}
var request = new HttpRequestMessage(HttpMethod.Get,
$"{_recordingSettings.RecordingApiUrl}/videos/allInProgress/{roomId}");
request.Headers.TryAddWithoutValidation("Authorization", $"Bearer {token}");
var response = await _httpClient.SendAsync(request);
_logger.LogDebug(
"GetByCodeSysAndCode recordings to recording api status code:{responseStatusCode} url: {url} body",
response.StatusCode, _url);
var responseContent = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
_accessGrant = null;
_logger.LogError(
"error connecting to RecordingOrApi url:{requestRequestUri} errorCode: {responseStatusCode} error {responseContent} token: {}",
request.RequestUri, response.StatusCode, response.Content,
_accessGrant?.AccessToken ?? "not exist");
}
else if (response.StatusCode == HttpStatusCode.NotFound)
{
return [];
}
else
{
_logger.LogError(
"error connecting to RecordingOrApi url:{requestRequestUri} errorCode: {responseStatusCode} error {responseContent}",
request.RequestUri, response.StatusCode, response.Content);
return [];
}
if (retryCount == _rabbitMqSettings.MaxRetries)
await _publisherService.SendMessage("Error: Connection retries exceeded.",
ErrorRecordingQueueName);
return [];
}
var records = JsonConvert.DeserializeObject>(responseContent);
return records;
}
catch (HttpRequestException ce)
{
retryCount++;
_logger.LogError(
"Connection error sending recording data to queue. {ce} Retrying... {retryCount} of {maxRetrys}",
ce.Message, retryCount, _rabbitMqSettings.MaxRetries);
//return new List();
}
catch (Exception e)
{
_logger.LogError("Error getting recordings room: {roomId} error: {eMessage} ", roomId, e.Message);
await _publisherService.SendMessage(e.Message, ErrorRecordingQueueName);
return [];
}
if (retryCount == _rabbitMqSettings.MaxRetries)
{
await _publisherService.SendMessage("Error: Connection retries exceeded.", ErrorRecordingQueueName);
return null;
}
return [];
}
///
/// Sends the recording data from the given API request to the Recording API with authentication, retrying on failure up to the configured maximum and routing unsuccessful requests to the error queue. Clears the access token on 401 responses, maps the returned video data into a recording broadcast message on success, and falls back to the error queue when retries are exhausted or a connection error occurs.
///
/// The API request whose recording data and patient information will be sent to the Recording API.
/// Rethrown via Task.FromException when a non-HTTP error occurs while sending the recording request.
public async Task SaveRequestAsync(ApiRequest apiRequest)
{
var retryCount = 1;
while (retryCount <= _rabbitMqSettings.MaxRetries)
{
try
{
var token = await _authService.GetToken();
_logger.LogDebug("sending recording to Recording or api");
var recordingRequest = apiRequest.Recording;
var request = new HttpRequestMessage(HttpMethod.Post,
$"{_recordingSettings.RecordingApiUrl}/videos/save-recording-data")
{
Content = new StringContent(JsonConvert.SerializeObject(recordingRequest), Encoding.UTF8)
};
request.Headers.TryAddWithoutValidation("Authorization", $"Bearer {token}");
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
var response = await _httpClient.SendAsync(request);
_logger.LogDebug("Save Request to recording api status code:{responseStatusCode}",
response.StatusCode);
var responseContent = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
if (response.StatusCode == HttpStatusCode.Unauthorized) _accessGrant = null;
_logger.LogError(
"error connecting to RecordingOrApi url:{requestRequestUri} errorCode: {responseStatusCode} error {responseContent}",
request.RequestUri, response.StatusCode, response.Content);
retryCount++;
}
else
{
// Response y en Data esta el videoDTO
var resp = JsonConvert.DeserializeObject>(responseContent);
var video = resp?.Data;
if (video == null)
return;
RecordingData recording = new()
{
StartRecordingTime = video.StartDate,
StopRecordingTime = video.EndDate,
EventTime = video.Date,
RoomId = video.RoomId,
AlarmType = video.Alarm,
Store = video.Store,
Status = video.VideoStatus.ToString(),
Patient = new Domain.Models.Recording.Patient
{
FirstName = apiRequest.Patient?.FirstName ??
apiRequest.Recording?.Patient?.FirstName ?? string.Empty,
Id = apiRequest.PatientNumber ?? apiRequest.Recording?.Patient?.Id ?? string.Empty,
LastName = apiRequest.Patient?.LastName ??
apiRequest.Recording?.Patient?.LastName ?? string.Empty
}
};
_ = SendRecordingBroadcast([recording]);
return;
}
}
catch (HttpRequestException ce)
{
retryCount++;
_logger.LogError("Connection error sending recording data to queue. {ce} Retrying...", ce.Message);
await _publisherService.SendMessage(apiRequest, ErrorRecordingQueueName);
}
catch (Exception e)
{
_logger.LogError("Error sending recording request to Recording or {eMessage}", e.Message);
await _publisherService.SendMessage(apiRequest, ErrorRecordingQueueName);
await Task.FromException(e);
}
if (retryCount > _rabbitMqSettings.MaxRetries)
await _publisherService.SendMessage(
$"Error: Connection retries exceeded. for api request: {apiRequest}", ErrorRecordingQueueName);
}
}
///
/// Saves the specified API request. The method is not yet implemented.
///
/// The API request to save.
/// Always thrown because the method has not been implemented.
public Task SaveRequest(ApiRequest apiRequest)
{
//return Task.CompletedTask;
throw new NotImplementedException();
}
///
/// Generates a object for the specified patient at the given point of care, incorporating alarm metadata and timing details.
/// Returns null and logs an error when the patient number is missing, the room identifier is unavailable, or the room identifier cannot be parsed as an integer.
/// The stop recording time is derived from or, when not provided, calculated from ; alarm names are translated to their description via .
///
/// The patient whose recording data is being generated; used to populate patient demographics and validate the patient number.
/// The point of care providing the room identifier for the recording.
/// The optional start time of the recording.
/// The optional stop time of the recording; when omitted, the stop time is derived from .
/// The optional time at which the triggering event occurred.
/// The optional number of minutes added to the current UTC time to compute the stop recording time when is not supplied.
/// The optional alarm name whose description is resolved via the enum utility; stored as a string when present.
/// The severity associated with the alarm.
/// A textual description of the alarm.
/// The alarm type, defaulting to when not specified.
/// A configured instance, or null if any required identifier is invalid.
private RecordingData? GenerateRecordingData(Patient patient, PointOfCare poc, DateTime? startDate,
DateTime? endDate, DateTime? eventTime, int? minToExpired, AlarmEnum.Name? alarmName,
AlarmEnum.Severity severity,
string alarmDescription, AlarmEnum.Type alarm = AlarmEnum.Type.Manual)
{
if (patient.PatientNumber == null)
{
_logger.LogError("Error Generating Recording Data . Patient number is null: {patient}", patient);
return null;
}
var roomId = poc.Configuration?.Id;
if (roomId == null)
{
_logger.LogError("Error Generating Recording Data on roomId");
return null;
}
var patientRecording = new Domain.Models.Recording.Patient
{
Id = patient.PatientNumber,
FirstName = patient.Person?.FirstName ?? "X",
LastName = $"{patient.Person?.SecondName} {patient.Person?.LastName}".TrimEnd()
};
if (!int.TryParse(roomId.ToString(), out var roomIdParsed))
{
_logger.LogError("Error Generating Recording Data on roomId parse: {roomId}", roomId);
return null;
}
var recordingData = new RecordingData
{
Patient = patientRecording,
RoomId = roomIdParsed,
StartRecordingTime = startDate,
StopRecordingTime =
endDate ?? (minToExpired != null ? DateTime.UtcNow.AddMinutes(minToExpired.Value) : null),
EventTime = eventTime,
AlarmType = alarm,
Severity = severity,
AlarmName = alarmName.HasValue ? EnumUtils.GetDescription(alarmName) : null,
AlarmDescription = alarmDescription,
Retry = 0
};
recordingData.GenerateStore();
return recordingData;
}
///
/// Sends a recording status broadcast to WebSocket subscribers associated with the patient's bed and unit.
/// Skips processing when the recording list is empty, logs an error and returns when the patient number is missing,
/// and silently returns when the patient cannot be resolved.
///
/// The list of recording entries to broadcast; the first item is used to resolve the patient and room context.
private async Task SendRecordingBroadcast(List recording)
{
if (!recording.IsNullOrEmpty())
{
var patientNumber = recording[0].Patient?.Id;
if (string.IsNullOrEmpty(patientNumber))
{
_logger.LogError("Box not found by RoomId {roomId}", recording[0].RoomId);
return;
}
var patient = await _patientService.Value.FindByPatientNumber(patientNumber);
if (patient == null)
return;
var type = OperationType.RecordingStatus;
var subscribers = _subscribersService.GetSubscribers().Where(s =>
s.SubscriptionType == SubscriptionEnum.WsType.Box && s.Box == patient.Location.Bed &&
s.Section == patient.Location.UnitName).ToList();
foreach (var subscriber in subscribers)
await _clientMessageService.SendAsync(subscriber.Id, type, recording);
}
}
///
/// Encodes the specified plain text to a Base64 string using UTF-8 encoding.
///
/// The text to encode.
/// The Base64 encoded representation of the input text.
protected static string Base64Encode(string plainText)
{
var plainTextBytes = Encoding.UTF8.GetBytes(plainText);
return Convert.ToBase64String(plainTextBytes);
}
}