using adas_core.Application.Services.Interfaces;
using adas_core.Application.Subscriptions;
using adas_core.Domain.Enums;
using adas_core.Domain.Models;
using adas_core.Domain.Models.AppSettings;
using adas_core.Domain.Models.Masters;
using adas_core.Domain.Models.MongoModels;
using adas_core.Domain.Models.Recording;
using adas_core.Domain.Models.Responses;
using adas_core.Domain.Models.SignalR;
using adas_core.Domain.Utils;
using adas_core.WebSocket.Hubs;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using Serilog;
using ILogger = Serilog.ILogger;
using Patient = adas_core.Domain.Models.MongoModels.Patient;

namespace adas_core.WebSocket;

public class WebSocketMessageService(
    Lazy<IPatientService> patientService,
    Lazy<IObservationService> observationService,
    Lazy<IPumpService> pumpService,
    Lazy<ITreatmentService> treatmentService,
    IGroupedObservationService groupedObservationService,
    IConfigObservationService configObservationService,
    Lazy<IAppointmentService> appointmentService,
    IRelayService relayService,
    IOptions<ApiSettings> apiSettings,
    IOptions<RecordingSettings> recordingSettings,
    Lazy<IRecordingService> recordingService,
    IHubContext<UciHub> hub,
    Lazy<ILightBeaconService> lightBeaconService,
    Lazy<IDiagnosisService> diagnosisService,
    Lazy<IAlarmService> alarmService,
    ISubscribersService subscribersService,
    ISubscriberGroupedService subscriberGroupedService,
    Lazy<IDisplayService> displayService,
    Lazy<IUnitService> unitService,
    Lazy<IAdmissionService> admissionService,
    Lazy<IDischargeService> dischargeService,
    Lazy<INoticeService> noticeService,
    IObservationDemoService observationDemoService
)
    : IClientMessageService
{
    #region Properties

    private readonly ILogger _logger = Log.ForContext<WebSocketMessageService>();

    private readonly bool _sendPumpsZero = apiSettings.Value.SendPumpsZero;

    private IHubContext<UciHub> Hub { get; } = hub;

    #endregion

    #region Methods

    #region Subscriptions

    /// <summary>
    /// Processes a demo-mode request for a WebSocket subscriber: resolves the display configuration
    /// for the subscriber's connection, populates <paramref name="msg"/> with the display's field lists,
    /// and dispatches patient data for every point-of-care associated with the display.
    /// </summary>
    /// <param name="msg">
    /// The inbound <see cref="Message"/> to enrich. Its <c>Data</c> is populated in-place with the
    /// fields, grouped fields, and alarm fields belonging to the resolved display type. When
    /// <c>Data.IsDemo</c> is <see langword="true"/>, each patient's language is randomized.
    /// </param>
    /// <param name="connectionId">The unique identifier of the WebSocket subscriber that originated the request.</param>
    /// <returns>A <see cref="Task"/> that completes when the demo request has been processed and all patient data has been dispatched.</returns>
    private async Task ProcessDemoRequest(Message msg, string connectionId)
    {
        var sub = subscribersService.GetById(connectionId);
        if (sub is { DisplayId: not null })
        {
            var display = await displayService.Value.GetInfo(sub.DisplayId!.Value, msg.Data?.UserName, null,
                msg.Data?.Locale, true, true, false);
            if (display == null)
            {
                _ = SendErrorMessage("Display " + sub.DisplayId.Value + " not found", connectionId);
                return;
            }

            if (display.DisplayConfig == null)
            {
                _ = SendErrorMessage("Display " + sub.DisplayId.Value + " has not config", connectionId);
                return;
            }

            switch (display.DisplayConfig?.Type)
            {
                case DisplayConfigEnums.DisplayType.SmartDisplay:
                    var smartDisplayCast = display.DisplayConfig as SmartDisplay;
                    msg.Data ??= new MessageData();
                    msg.Data.GroupedFields = smartDisplayCast?.GroupedFieldList;
                    msg.Data.Alarmfields = smartDisplayCast?.AlarmFieldList;
                    msg.Data.Fields = smartDisplayCast?.FieldList;
                    break;
                case DisplayConfigEnums.DisplayType.DisplayNurse:
                    var nurseDisplayCast = display.DisplayConfig as DisplayNurse;
                    msg.Data ??= new MessageData();
                    msg.Data.Fields = nurseDisplayCast?.FieldList;
                    break;
                case DisplayConfigEnums.DisplayType.PumpDisplay:
                    var pumpDisplayCast = display.DisplayConfig as PumpDisplay;
                    msg.Data ??= new MessageData();
                    msg.Data.GroupedFields = pumpDisplayCast?.GroupedFieldList;
                    msg.Data.Fields = pumpDisplayCast?.FieldList;
                    break;
                default:
                    _ = SendErrorMessage(
                        "Display " + sub.DisplayId.Value + " with type: " + display.DisplayConfig?.Type + "not mapped",
                        connectionId);
                    break;
            }

            foreach (var displayPointOfCare in display.PointOfCares)
            {
                //var patient = await _patientService.Value.FindByLocation(displayPointOfCare.Location);
                var patient = await patientService.Value.FindByPointOfCareId(displayPointOfCare.Id);
                if (msg.Data is { IsDemo: true })
                {
                    var rnd = new Random();
                    var rndLang = rnd.Next(0, 5);
                    List<string> languages = ["Español", "Ingles", "Portugués", "Aleman", "Catalan"];
                    if (patient?.Person != null) patient.Person.Language = languages[rndLang];
                }

                if (patient != null)
                    _ = SendPatientData(patient, msg, connectionId);
            }
        }
    }

    /// <summary>
    /// Processes a message intended for an admin subscriber by adding it to the subscriber's message display.
    /// The method returns early when the message data or user name is null, ensuring only messages with an identified user are forwarded as admin notifications.
    /// </summary>
    /// <param name="msg">The message to process, whose data and user name are used to identify the subscriber.</param>
    /// <param name="connectionId">The connection identifier of the admin subscriber that will receive the message display.</param>
    private void ProcessAdminSubscriber(Message msg, string connectionId)
    {
        if (msg.Data?.UserName == null) return;
        AddMessageDisplayToSubcriber(msg, connectionId, SubscriptionEnum.WsType.Admin);
    }

    /// <summary>
    /// Processes a "display subscriber" request from a WebSocket connection: resolves the target display,
    /// configures the message payload for the matching display type, and then fans out every related
    /// data stream (patient data, config observations, appointments, admissions, discharges, notices,
    /// recordings, relay states, and beacons) to the subscriber.
    /// </summary>
    /// <param name="msg">
    /// The inbound <see cref="Message"/> to enrich. Its <c>Data</c> is populated in-place with the
    /// locations, location identifiers, fields, grouped fields, alarm fields, and section config
    /// associated with the resolved display. When <c>Data.IsDemo</c> is <see langword="true"/>,
    /// each patient's language is randomized before being dispatched.
    /// </param>
    /// <param name="connectionId">The unique identifier of the WebSocket subscriber that originated the request.</param>
    /// <returns>A <see cref="Task"/> that completes once the synchronous portion of the processing has finished; 
    /// several sub-tasks (display list, recordings, relay states, beacons) are dispatched fire-and-forget.</returns>
    private async Task ProcessDisplaySubscriber(Message msg, string connectionId)
    {
        var id = msg.Data?.DisplayId;
        if (id == null || !ObjectId.TryParse(id, out var idParsed))
        {
            _ = SendErrorMessage("Message Display " + id + " null or not an ObjectId", connectionId);
            return;
        }

        #region [DISPLAY INFO]

        var display =
            await displayService.Value.GetInfo(idParsed, msg.Data?.UserName, null, msg.Data?.Locale, true, true, false);
        if (display == null)
        {
            _ = SendErrorMessage("Display " + id + " not found", connectionId);
            return;
        }

        if (display.DisplayConfig == null)
        {
            _ = SendErrorMessage("Display " + id + " has not config", connectionId);
            return;
        }

        display.PointOfCares.ForEach(poc => msg.Data?.Locations.Add(poc.Location));
        display.PointOfCares.ForEach(poc => msg.Data?.LocationIds.Add(poc.Id));

        switch (display.DisplayConfig?.Type)
        {
            case DisplayConfigEnums.DisplayType.SmartDisplay:
                var smartDisplayCast = display.DisplayConfig as SmartDisplay;
                msg.Data ??= new MessageData();
                msg.Data.GroupedFields = smartDisplayCast?.GroupedFieldList;
                var subs = subscribersService.GetById(connectionId);
                if (subs != null) subs.GroupedFields = smartDisplayCast?.GroupedFieldList;
                msg.Data.Alarmfields = smartDisplayCast?.AlarmFieldList;
                msg.Data.Fields = smartDisplayCast?.FieldList;
                // Sensors?
                // Send PoC List
                await ProcessPocList(display.PointOfCares, connectionId, msg);

                // Send Unit MasterList
                ProcessMasterList(display.UnitId, connectionId, msg.Data.Locale);

                _ = ProcessDisplayList(display.Type, display.Id, msg.Data?.UserName, connectionId);

                break;
            case DisplayConfigEnums.DisplayType.StandarDisplay:
                var standarDisplayCast = display.DisplayConfig as StandarDisplay;
                msg.Data ??= new MessageData();
                msg.Data.GroupedFields = standarDisplayCast?.GroupedFieldList;
                var subsStand = subscribersService.GetById(connectionId);
                if (subsStand != null) subsStand.GroupedFields = standarDisplayCast?.GroupedFieldList;
                msg.Data.Alarmfields = standarDisplayCast?.AlarmFieldList;
                msg.Data.Fields = standarDisplayCast?.FieldList;
                msg.Data.SectionConfig = standarDisplayCast?.SectionConfig;

                await ProcessStandard(msg.Operation, display, connectionId);
                // Sensors?
                break;
            case DisplayConfigEnums.DisplayType.DisplayNurse:
                var nurseDisplayCast = display.DisplayConfig as DisplayNurse;
                msg.Data ??= new MessageData();
                msg.Data.Fields = nurseDisplayCast?.FieldList;
                msg.Data.GroupedFields = nurseDisplayCast?.GroupedFieldList;
                // Send PoC List
                await ProcessPocList(display.PointOfCares, connectionId, msg);

                // Send Unit MasterList
                ProcessMasterList(display.UnitId, connectionId, msg.Data.Locale);

                _ = ProcessDisplayList(display.Type, display.Id, msg.Data?.UserName, connectionId);

                break;
            case DisplayConfigEnums.DisplayType.PumpDisplay:
                var pumpDisplayCast = display.DisplayConfig as PumpDisplay;
                msg.Data ??= new MessageData();
                msg.Data.GroupedFields = pumpDisplayCast?.GroupedFieldList;
                msg.Data.Fields = pumpDisplayCast?.FieldList;
                // Send PoC List
                await ProcessPocList(display.PointOfCares, connectionId, msg);

                // Send Unit MasterList
                ProcessMasterList(display.UnitId, connectionId, msg.Data.Locale);

                _ = ProcessDisplayList(display.Type, display.Id, msg.Data?.UserName, connectionId);
                break;
            default:
                _ = SendErrorMessage("Display " + id + " with type: " + display.DisplayConfig?.Type + "not mapped",
                    connectionId);
                break;
        }

        #endregion

        #region [CONFIG OBSERVATIONS]

        //Solo mandamos el configObservations en este punto cuando NO es de tipo Standard
        var configObservations = await configObservationService.GetAllConfigs();
        if (configObservations.Count > 0 &&
            display.DisplayConfig?.Type != DisplayConfigEnums.DisplayType.StandarDisplay)
            _ = SendAsync(connectionId, OperationType.ConfigObservation, configObservations);

        #endregion

        #region APPOINTMENTS

        //Mandamos todos los appointments de la unidad
        List<PatientAppointment> unitAppointments = [];

        if (msg.Data?.Appointments is true)
        {
            foreach (var poc in display.PointOfCares)
            {
                var location = new PatientLocation(poc.UnitName, poc.Bed, poc.Room);
                unitAppointments.AddRange(await appointmentService.Value.FindByLocation(location));
            }

            if (unitAppointments.Count > 0)
                _ = SendAsync(connectionId, OperationType.NewAppointment, unitAppointments);
        }

        #endregion

        #region [PATIENT DATA]

        foreach (var displayPointOfCare in display.PointOfCares)
        {
            //var patient = await _patientService.Value.FindByLocation(displayPointOfCare.Location);
            var patient = await patientService.Value.FindByPointOfCareId(displayPointOfCare.Id);
            if (msg.Data is { IsDemo: true })
            {
                var rnd = new Random();
                var rndLang = rnd.Next(0, 5);
                List<string> languages = ["Español", "Ingles", "Portugués", "Aleman", "Catalan"];
                if (patient?.Person != null) patient.Person.Language = languages[rndLang];
            }

            if (patient != null)
                _ = SendPatientData(patient, msg, connectionId, configObservations);
        }


        if (msg.Data != null &&
            !msg.Data.LocationIds.IsNullOrEmpty() &&
            display.DisplayConfig?.Type == DisplayConfigEnums.DisplayType.DisplayNurse)
        {
            // Send Admission
            ProcessAdmission(msg.Data.LocationIds, display.UnitId, connectionId, msg.Data.Locale);

            // Send Discharger
            ProcessDischarge(msg.Data.LocationIds, connectionId, msg.Data.Locale);
        }

        #endregion

        #region [NOTICES]

        if (msg.Data?.DisplayId != null &&
            display.DisplayConfig?.Type == DisplayConfigEnums.DisplayType.DisplayNurse)
            ProcessNotice(msg.Data.DisplayId, connectionId);

        #endregion

        #region [RECORDING]

        if (!string.IsNullOrEmpty(recordingSettings.Value.RecordingApiUrl))
            _ = Task.Run(async () =>
            {
                if (msg.Data != null && (msg.Data.RecordingAlerts || msg.Data.Recordings))
                    foreach (var displayPointOfCare in display.PointOfCares)
                        if (displayPointOfCare.Configuration is { Id: not null })
                        {
                            var recording =
                                await recordingService.Value.GetRecordings(displayPointOfCare.Configuration.Id
                                    .Value);

                            if (recording != null)
                            {
                                if (recording.Any(r => r.AlarmType == AlarmEnum.Type.Manual))
                                    _ = SendAsync(connectionId, OperationType.RecordingStatus,
                                        new List<RecordingData>
                                            { recording.First(r => r.AlarmType == AlarmEnum.Type.Manual) });
                                else if (recording.Any(r => r.AlarmType == AlarmEnum.Type.Auto))
                                    _ = SendAsync(connectionId, OperationType.RecordingAlert,
                                        recording.Where(r => r.AlarmType == AlarmEnum.Type.Auto).ToList());
                            }
                        }
            });

        #endregion

        #region [RELAY]

        if (msg.Data is { Relay: true } && !string.IsNullOrEmpty(recordingSettings.Value.RecordingApiUrl))
            _ = Task.Run(async () =>
            {
                foreach (var displayPointOfCare in display.PointOfCares)
                {
                    if (displayPointOfCare.Configuration?.RelayIdList == null)
                        continue;

                    foreach (var relay in displayPointOfCare.Configuration.RelayIdList)
                    {
                        var relayStatus =
                            await relayService.CheckRelayStatus(relay);
                        var relayResult = await relayService.GetById(relay);
                        if (relayResult != null)
                        {
                            relayResult.Status = relayStatus;

                            _ = SendAsync(connectionId, OperationType.Relay, relay);
                        }
                    }
                }
            });

        #endregion

        #region [BEACONS]

        if (msg.Data is { Beacons: true })
            foreach (var displayPointOfCare in display.PointOfCares)
                _ = Task.Run(() => { _ = SendBeaconsData(displayPointOfCare, connectionId); });

        #endregion

        AddMessageDisplayToSubcriber(msg, connectionId, SubscriptionEnum.WsType.Display);
    }

    /// <summary>
    /// Processes a standard subscription by loading configuration observations, associating patients with the display's points of care, and sending the resulting section data to the specified connection. When the subscription type is <see cref="SubscriptionEnum.Type.BoxSubscribe"/>, the box status of the first point of care is also sent.
    /// </summary>
    /// <param name="suscriptionType">The subscription type that determines whether additional box-specific data should be dispatched.</param>
    /// <param name="display">The display whose configuration and points of care will be processed; only valid when its configuration is a <c>StandarDisplay</c> with a non-null section configuration.</param>
    /// <param name="connectionId">The identifier of the client connection that will receive the emitted messages.</param>
    private async Task ProcessStandard(SubscriptionEnum.Type suscriptionType, Display display, string connectionId)
    {
        var standarDisplayCast = display.DisplayConfig as StandarDisplay;
        if (standarDisplayCast?.SectionConfig == null)
            return;

        var configObservations = await configObservationService.GetAllConfigs();
        _ = SendAsync(connectionId, OperationType.ConfigObservation, configObservations);

        foreach (var poc in display.PointOfCares)
        {
            var patient = await patientService.Value.FindByPointOfCareId(poc.Id);
            if (patient == null)
            {
                poc.HasPatient = false;
                continue;
            }

            poc.HasPatient = true;
            poc.Patient = patient;
        }

        var section = new Section
        {
            SectionConfig = standarDisplayCast.SectionConfig,
            SectionTitle = display.Name,
            PointOfCareList = new Dictionary<string, List<PointOfCare>>
                {
                    { "pasillo 1", display.PointOfCares }
                } //TODO no se ha tenido en cuenta las agrupaciones de boxes (Hall?)
        };
        await SendAsync(connectionId, OperationType.Section, section);


        if (suscriptionType == SubscriptionEnum.Type.BoxSubscribe)
        {
            var box = display.PointOfCares.First();

            var boxStatus = await patientService.Value.GetBox(box, true);

            _ = SendAsync(connectionId, OperationType.Box, boxStatus);
        }
    }

    /// <summary>
    /// Retrieves a display section list for the specified user and sends it to the given connection when at least one item is present in the result.
    /// </summary>
    /// <param name="displayType">The type of display used to filter the section list query.</param>
    /// <param name="displayId">The identifier of the display configuration to query.</param>
    /// <param name="dataUserName">The optional user name used to filter the display section results.</param>
    /// <param name="connectionId">The identifier of the connection to which the display list will be sent.</param>
    private async Task ProcessDisplayList(
            DisplayConfigEnums.DisplayType displayType,
            ObjectId displayId,
            string? dataUserName,
            string connectionId)
    {
        var listToSend = await displayService.Value.GetDisplaySectionByUser(displayType, displayId, dataUserName, null);
        if (listToSend.Count > 0)
            await SendAsync(connectionId, OperationType.DisplayList, listToSend);
    }
     
    /// <summary>
    /// Processes a list of PointOfCare objects.
    /// </summary>
    /// <param name="displayPointOfCares">The list of PointOfCare objects to process.</param>
    /// <param name="connectionId">The ID of the connection for which to send the data.</param>
    /// <param name="msg">The message object.</param>
    private async Task ProcessPocList(List<PointOfCare> displayPointOfCares, string connectionId, Message msg)
    {
        //TODO revisar si es necesario que se envíen los pocs aquí si no está en modo demo ¿redundante?
        foreach (var poc in displayPointOfCares)
        {
            if (msg.Data is { IsDemo: true })
            {
                var rnd = new Random();
                var rndLang = rnd.Next(0, 5);
                List<string> languages = ["Español", "Ingles", "Portugués", "Aleman", "Catalan"];
                if (poc.Patient?.Person != null) poc.Patient.Person.Language = languages[rndLang];
            }
            await SendAsync(connectionId, OperationType.NewPointOfCare, poc);
        }
    }

    /// <summary>
    /// Processes a master list by retrieving unit information and dispatching the unit details along with all associated master lists (e.g., allergies, diagnoses, doctors, procedures) to the specified client connection. Logs an error and returns early when the requested unit cannot be found, and logs any exception that occurs during processing.
    /// </summary>
    /// <param name="id">The identifier of the unit whose master lists should be processed and pushed to the client.</param>
    /// <param name="connectionId">The identifier of the client connection that will receive the unit information and master list updates.</param>
    /// <param name="dataLocale">The optional locale used to localize the retrieved unit data and master lists.</param>
    private async void ProcessMasterList(ObjectId id, string connectionId, LocaleEnum? dataLocale)
    {
        try
        {
            var unit = await unitService.Value.GetInfo(id, dataLocale);
            if (unit == null)
            {
                _logger.Error("Error processing Master List id {id}", id);
                return;
            }

            _ = SendAsync(connectionId, OperationType.UpdateUnit, unit);
            _ = SendAsync(connectionId, OperationType.NewMasterList, unit.AllergyList);
            _ = SendAsync(connectionId, OperationType.NewMasterList, unit.AltableOptionList);
            _ = SendAsync(connectionId, OperationType.NewMasterList, unit.DestinationList);
            _ = SendAsync(connectionId, OperationType.NewMasterList, unit.DiagnosisList);
            _ = SendAsync(connectionId, OperationType.NewMasterList, unit.DischargeStatusList);
            _ = SendAsync(connectionId, OperationType.NewMasterList, unit.DoctorList);
            _ = SendAsync(connectionId, OperationType.NewMasterList, unit.DoctorTypeList);
            _ = SendAsync(connectionId, OperationType.NewMasterList, unit.InsulationList);
            _ = SendAsync(connectionId, OperationType.NewMasterList, unit.MobilityOptionList);
            _ = SendAsync(connectionId, OperationType.NewMasterList, unit.OriginList);
            _ = SendAsync(connectionId, OperationType.NewMasterList, unit.PatientStatusList);
            _ = SendAsync(connectionId, OperationType.NewMasterList, unit.ProcedureList);
            _ = SendAsync(connectionId, OperationType.NewMasterList, unit.TestList);
            _ = SendAsync(connectionId, OperationType.NewMasterList, unit.ServiceList);
            _ = SendAsync(connectionId, OperationType.NewMasterList, unit.TherapeuticCeilingList);
            _ = SendAsync(connectionId, OperationType.NewMasterList, unit.TreatmentList);
            _ = SendAsync(connectionId, OperationType.NewMasterList, unit.VisitOptionList);
            _ = SendAsync(connectionId, OperationType.NewMasterList, unit.AccessControlList);
            _ = SendAsync(connectionId, OperationType.NewMasterList, unit.LanguageBarrierList);
            _ = SendAsync(connectionId, OperationType.NewMasterList, unit.PassiveSittingList);
            _ = SendAsync(connectionId, OperationType.NewMasterList, unit.GenericList);
        }
        catch (Exception ex)
        {
            _logger.Error("Exception processing mas. Exception: {ex}", ex);
        }
    }

    /// <summary>
    /// Processes a notice request by resolving the notice associated with the specified display identifier and delivering it to the given connection.
    /// Handles invalid display identifiers, empty result sets, missing notices, and unexpected runtime errors by exiting gracefully and logging the outcome.
    /// </summary>
    /// <param name="displayId">The display identifier used to look up the notice; if it cannot be parsed as an ObjectId, the method returns without further action.</param>
    /// <param name="connectionId">The identifier of the connection to which each retrieved notice is dispatched as a new-notice operation.</param>
    private async void ProcessNotice(string displayId, string connectionId)
    {
        try
        {
            if (!ObjectId.TryParse(displayId, out var id))
                return;

            var notice = await noticeService.Value.GetNoticesByDisplayId(id);
            if (notice != null)
            {
                var noticeList = notice.ToList();
                if (!noticeList.IsNullOrEmpty())
                    noticeList.ForEach(c => _ = SendAsync(connectionId, OperationType.NewNotice, c));
            }
            else
            {
                _logger.Information($"Display id: {displayId} has not notices");
            }
        }
        catch (Exception ex)
        {
            _logger.Error("Exception processing Notice. Exception: {ex}", ex);
        }
    }

    /// <summary>
    /// Processes a collection of discharge locations, retrieving the discharge for each location using the specified locale,
    /// enriching it with the associated patient, and notifying the connected client about new discharges.
    /// Locations with no associated discharge are logged, and any exception encountered is caught and logged.
    /// </summary>
    /// <param name="dataLocations">The list of point-of-care location identifiers to process for discharge data.</param>
    /// <param name="connectionId">The identifier of the client connection used to send the new discharge notification.</param>
    /// <param name="dataLocale">The locale used to retrieve the discharge data.</param>
    private async void ProcessDischarge(List<ObjectId> dataLocations, string connectionId, LocaleEnum dataLocale)
    {
        try
        {
            foreach (var location in dataLocations)
            {
                var discharge =
                    await dischargeService.Value.GetDischargeByPointOfCareIdWithLocale(location, dataLocale);

                if (discharge != null)
                {
                    var patient = await patientService.Value.FindById(discharge.PatientId);
                    discharge.Patient = patient;
                    _ = SendAsync(connectionId, OperationType.NewDischarge, discharge);
                }
                else
                {
                    _logger.Information($"Location {location} has no discharge");
                }
            }
        }
        catch (Exception e)
        {
            _logger.Error("Exception processing Discharge. Exception: {ex}", e);
        }
    }

    /// <summary>
    /// Processes admission data for the specified data locations filtered by locale, and optionally for a display unit without a point of care, broadcasting each admission to the client identified by the connection identifier.
    /// </summary>
    /// <param name="dataLocations">The list of point-of-care location identifiers whose admissions should be retrieved and broadcast.</param>
    /// <param name="displayUnitId">The optional display unit identifier; when provided, admissions assigned to that unit without a point of care are also broadcast.</param>
    /// <param name="connectionId">The identifier of the client connection to which the admission updates are sent.</param>
    /// <param name="dataLocale">The locale used to filter admissions retrieved for the given data locations.</param>
    private async void ProcessAdmission(List<ObjectId> dataLocations, ObjectId? displayUnitId,
            string connectionId, LocaleEnum dataLocale)
    {
        try
        {
            foreach (var location in dataLocations)
            {
                var admission = await admissionService.Value.GetAdmissionByPointOfCareIdAndLocale(location, dataLocale);
                foreach (var adm in admission) await SendAsync(connectionId, OperationType.NewAdmission, adm);
            }

            if (displayUnitId.HasValue)
            {
                var admWithNoPoc = await admissionService.Value.GetAdmissionByUnitIdWithOutPoC(displayUnitId.Value);
                foreach (var adm in admWithNoPoc) _ = SendAsync(connectionId, OperationType.NewAdmission, adm);
            }
        }
        catch (Exception e)
        {
            _logger.Error("Exception processing Admission. Exception: {ex}", e);
        }
    }

    #endregion

    #region Process Messages

    /// <summary>
    /// Processes an incoming message by routing it to the appropriate handler based on the subscription type.
    /// Handles ChatMessage, RecordingSuscribe, PatientData, GroupedObservations, Admin, Display, BoxSubscribe, Demo, and Idle operations.
    /// </summary>
    /// <param name="msg">The message to process, containing the operation type and payload.</param>
    /// <param name="connectionId">The identifier of the connection that originated the message.</param>
    public async Task ProcessMessage(Message msg, string connectionId)
    {
        try
        {
            switch (msg.Operation)
            {
                #region Operation V1

                case SubscriptionEnum.Type.ChatMessage:
                    await ProcessChatMessage(msg, connectionId);
                    break;
                case SubscriptionEnum.Type.RecordingSuscribe:
                    await ProcessRecordingMessage(msg, connectionId);
                    break;
                case SubscriptionEnum.Type.PatientData:
                    await ProcessPatientData(msg, connectionId);
                    break;
                case SubscriptionEnum.Type.Idle:
                    break;
                case SubscriptionEnum.Type.GroupedObservations:
                    _ = ProcessGroupedObservations(msg, connectionId);
                    break;

                #endregion

                case SubscriptionEnum.Type.Admin:
                    ProcessAdminSubscriber(msg, connectionId);
                    break;
                case SubscriptionEnum.Type.Display:
                case SubscriptionEnum.Type.BoxSubscribe:
                    _ = ProcessDisplaySubscriber(msg, connectionId);
                    break;
                case SubscriptionEnum.Type.Demo:
                    _ = ProcessDemoRequest(msg, connectionId);
                    break;
            }
        }
        catch (Exception ex)
        {
            _logger.Error("Error processing message: {msg}. Message: {exMessage}. Exception: {ex}", msg, ex.Message,
                ex);
            throw;
        }
    }

    /// <summary>
    /// Processes grouped observation fields from an incoming message by resolving patients at the specified location and asynchronously generating and sending grouped observations for each patient and field combination.
    /// </summary>
    /// <param name="msg">The message containing the location, optional time zone, and grouped fields to process.</param>
    /// <param name="connectionId">The identifier of the client connection used to send the generated observations.</param>
    /// <returns>A task that represents the asynchronous processing of the grouped observations.</returns>
    private async Task ProcessGroupedObservations(Message msg, string connectionId)
    {
        if (msg.Data is { GroupedFields: not null } && msg.Data.GroupedFields.Any())
        {
            if (msg.Data.Location == null) return;

            List<Patient> patients = [];
            if (msg.Data.Location.Bed != null)
            {
                var patient = await patientService.Value.FindByLocation(msg.Data.Location);
                if (patient != null)
                    patients.Add(patient);
            }
            else if (msg.Data.Location.UnitName != null)
            {
                // var patientList = await _patientService.Value.FindByUnit(msg.Data.Location.UnitName);
                // if (patientList != null)
                //     patients.AddRange(patientList);
            }


            foreach (var patient in patients)
                foreach (var groupedField in msg.Data.GroupedFields)
                    try
                    {
                        var groupedObservation = msg.Data.TimeZoneId != null
                            ? await groupedObservationService.GenerateGroupedObservation(patient.Id, groupedField,
                                msg.Data.TimeZoneId)
                            : await groupedObservationService.GenerateGroupedObservation(patient.Id, groupedField);


                        await SendAsync(connectionId, OperationType.GroupedObservation, groupedObservation);
                    }
                    catch (Exception e)
                    {
                        _logger.Error(
                            "Exception sending Grouped Observations:  person: {patient} DTOMessage: {msg} Exception {eMessage}",
                            patient, msg, e);
                    }
        }
    }

    /// <summary>
    /// Processes an incoming recording message for a specific subscriber, forwarding a recording alert when valid recordings are available.
    /// Bails out early if the subscriber cannot be found by connection ID, or if the message contains no recordings.
    /// </summary>
    /// <param name="msg">The incoming message containing the current recordings to be broadcast.</param>
    /// <param name="connectionId">The identifier of the subscriber's connection used to look up the subscriber and target the alert.</param>
    private async Task ProcessRecordingMessage(Message msg, string connectionId)
    {
        //var subscribers = Subscribers.Instance.GetSubscribers();

        AddMessagetoSubcriber(msg, connectionId);

        var subscriber = subscribersService.GetById(connectionId);
        if (subscriber == null)
            return;
        var recordings = msg.Data?.CurrentRecordings;
        if (recordings is not { Count: > 0 }) return;

        await SendAsync(connectionId, OperationType.RecordingAlert, recordings);
    }

    /// <summary>
    /// Processes incoming patient data messages by resolving the referenced patient either by unique identifier or by location, and forwards the patient data when a match is found.
    /// </summary>
    /// <param name="msg">The message payload carrying the patient reference, either as a <c>PatientId</c> or a location.</param>
    /// <param name="connectionId">The identifier of the connection to which the resolved patient data should be sent.</param>
    private async Task ProcessPatientData(Message msg, string connectionId)
    {
        Patient? patient = new();
        var patientId = msg.Data?.PatientId;

        if (patientId != null)
        {
            patient = await patientService.Value.FindById(ObjectId.Parse(patientId));
        }
        else
        {
            var patientLocation = msg.Data?.Location;
            if (patientLocation != null)
                patient = await patientService.Value.FindByLocation(patientLocation);
        }

        if (patient != null)
            await SendPatientData(patient, msg, connectionId);
    }

    /// <summary>
    /// Processes an incoming chat message by validating the source and destination locations, locating a matching subscriber based on the destination bed and message section, and forwarding the chat message to that subscriber.
    /// Returns early when either the source or destination location is missing, or when no matching subscriber is found.
    /// </summary>
    /// <param name="msg">The incoming message envelope whose <c>Data</c> contains the source location, destination location, section, timestamp, and chat text.</param>
    /// <param name="connectionId">The connection identifier used to deliver the chat message via <c>SendAsync</c>.</param>
    private async Task ProcessChatMessage(Message msg, string connectionId)
    {
        var fromLocation = msg.Data?.Location;
        var toLocation = msg.Data?.ChatMessageToLocation;

        if (fromLocation == null || toLocation == null)
            return;

        var subscriber = subscribersService.GetSubscribers()
            .FirstOrDefault(s =>
                s.Box != null && s.Box == toLocation.Bed && s.Section != null && s.Section == msg.Data?.Section);
        if (subscriber == null)
            return;

        ChatMessage chatMessage = new()
        {
            LocationFrom = fromLocation,
            LocationTo = msg.Data?.ChatMessageToLocation,
            MessageTime = msg.Data?.ChatMessageDate,
            Message = msg.Data?.ChatMessage
        };

        await SendAsync(connectionId, OperationType.ChatMessage, chatMessage);
    }

    #endregion

    #region Send Data

    /// <summary>
    /// Sends an update message to all subscribers associated with the given patient locations. If the location has no bed specified, subscribers are matched by section; otherwise, subscribers are matched by both box (bed) and section.
    /// </summary>
    /// <param name="patientLocations">The list of patient locations used to determine which subscribers should receive the update message.</param>
    public void SendUpdateMessageToBoxes(List<PatientLocation> patientLocations)
    {
        var subscribers = new List<WsSubscriber>();

        foreach (var patientLocation in patientLocations)
            if (string.IsNullOrEmpty(patientLocation.Bed))
            {
                var subs = subscribersService.GetSubscribers().Where(s =>
                    s.SubscriptionType == SubscriptionEnum.WsType.Section && s.Section == patientLocation.UnitName &&
                    !subscribers.Contains(s)).ToList();
                subscribers.AddRange(subs);
            }
            else
            {
                var subs = subscribersService.GetSubscribers().Where(s =>
                    s.SubscriptionType == SubscriptionEnum.WsType.Box && s.Box == patientLocation.Bed &&
                    s.Section == patientLocation.UnitName && !subscribers.Contains(s)).ToList();
                subscribers.AddRange(subs);
            }

        foreach (var subscriber in subscribers) _ = SendAsync(subscriber.Id, OperationType.UpdateApplication, null);
    }


    /// <summary>
    /// Sends patient data to the client identified by <paramref name="connectionId"/>, dispatching the relevant asynchronous operations in parallel based on the data sections present in the message (observations, appointments, alarms, pumps, treatments, diagnosis, and grouped fields).
    /// </summary>
    /// <param name="patient">The patient whose data is being transmitted.</param>
    /// <param name="msg">The message containing the patient data sections to send.</param>
    /// <param name="connectionId">The identifier of the client connection that will receive the data.</param>
    /// <param name="configObservations">Optional collection of configuration observations used when sending the latest alarms.</param>
    /// <param name="ct">Cancellation token to cancel the asynchronous operations.</param>
    private async Task SendPatientData(
            Patient patient,
            Message msg,
            string connectionId,
            ICollection<ConfigObservation>? configObservations = null,
            CancellationToken ct = default)
    {
        var tasks = new List<Task>();

        if (msg.Data?.Fields?.Any() == true)
            tasks.Add(SendLatestObservationsAsync(patient, msg.Data, connectionId, ct));

        if (msg.Data?.Appointments == true)
            tasks.Add(SendPatientAppointmentsAsync(patient, msg.Data, connectionId, ct));

        if (msg.Data?.Alarmfields?.Any() == true)
            tasks.Add(SendLatestAlarmsAsync(patient, msg.Data, configObservations, connectionId));

        if (msg.Data?.Pumps == true)
            tasks.Add(SendLatestPumpsAsync(patient, msg.Data, connectionId, ct));

        if (msg.Data?.Treatments == true)
            tasks.Add(SendTreatmentsAsync(patient, msg.Data, connectionId));

        if (msg.Data?.Diagnosis == true)
            tasks.Add(SendDiagnosisAsync(patient, msg.Data, connectionId));

        if (msg.Data?.GroupedFields?.Any() == true)
            tasks.Add(SendGroupedObsAsync(patient, msg.Data, connectionId, ct));

        await Task.WhenAll(tasks);
    }


    /// <summary>
    /// Sends the latest observations for a patient to a specific client connection. When the message is marked as a demo, observations are generated from the demo service; otherwise, the most recent observations are retrieved from the observation service, ordered by time, and dispatched asynchronously.
    /// </summary>
    /// <param name="patient">The patient whose observations will be retrieved and sent.</param>
    /// <param name="data">The message payload that provides the fields to query and indicates whether a demo data source should be used.</param>
    /// <param name="connectionId">The identifier of the target client connection that will receive the observations.</param>
    /// <param name="ct">The cancellation token used to cancel the observation retrieval operation.</param>
    private async Task SendLatestObservationsAsync(
            Patient patient,
            MessageData data,
            string connectionId,
            CancellationToken ct = default)
    {
        try
        {
            if (data.Fields != null)
            {
                var observations = data is { IsDemo: true }
                    ? await observationDemoService.GenerateObservationByField(patient, data.Fields)
                    : await observationService.Value.FindLastObservationsByField(patient.Id, data.Fields, ct: ct);
                if (observations.Count > 0)
                {
                    observations = observations.OrderBy(p => p.Time).ToList();
                    observations.ForEach(o => _ = SendAsync(connectionId, OperationType.Observation, o));
                }
            }
        }
        catch (OperationCanceledException) { }
        catch (Exception e)
        {
            _logger.Error(
                "Exception sending person Data: {eMessage} person: {patient} Message Data: {data} {eStackTrace} ",
                e.Message, patient, data, e.StackTrace);
        }
    }

    /// <summary>
    /// Asynchronously sends today's appointments for the specified patient to the given subscriber.
    /// Handles cancellation silently and logs any other exceptions encountered while retrieving or sending the appointment data.
    /// </summary>
    /// <param name="patient">The patient whose today's appointments will be retrieved and dispatched.</param>
    /// <param name="msgData">The originating message data used for logging context when an error occurs.</param>
    /// <param name="subscriberId">The identifier of the subscriber that will receive the appointment notifications.</param>
    /// <param name="ct">The cancellation token used to cancel the appointment retrieval operation.</param>
    private async Task SendPatientAppointmentsAsync(
            Patient patient,
            MessageData msgData,
            string subscriberId,
            CancellationToken ct = default)
    {
        // SEND APPOINTMENTS OF BOXES
        try
        {
            var appointments = await appointmentService.Value.GetTodayByPatient(patient.Id, ct);
            foreach (var appointment in appointments)
                _ = SendAsync(subscriberId, OperationType.NewAppointment, appointment);
        }
        catch (OperationCanceledException) { }
        catch (Exception e)
        {
            _logger.Error(
                "Exception sending patient appointments Data: {eMessage} PointOfCare: {location} DTOMessage: {msg} ",
                e.Message, patient, msgData);
        }
    }

    /// <summary>
    /// Asynchronously retrieves and sends the latest alarm observations for a patient to the client identified by the connection ID, choosing between demo data generation and a configuration-aware lookup that prefers non-expired values when all alarm fields are covered by configuration.
    /// </summary>
    /// <param name="patient">The patient whose alarm observations are being retrieved.</param>
    /// <param name="data">The message data containing the alarm fields to evaluate and the demo mode flag.</param>
    /// <param name="configObservations">Optional configuration observations used to select the alarm lookup strategy.</param>
    /// <param name="connectionId">The identifier of the target client connection to which the alarm observations are sent.</param>
    private async Task SendLatestAlarmsAsync(
            Patient patient,
            MessageData data,
            ICollection<ConfigObservation>? configObservations,
            string connectionId)
    {
        try
        {
            data.Alarmfields ??= [];

            var configAlarm = configObservations?
                .Where(c => data.Alarmfields.Any(h => h.Name == c.Name))
                .ToList();
            var observations = data.IsDemo
                ?
                await observationDemoService.GenerateAlarmByField(patient, data.Alarmfields)
                : configAlarm != null && configAlarm.Count == data.Alarmfields.Count
                    ? await alarmService.Value.FindLastValuesNotExpired(patient.Id, data.Alarmfields,
                        configAlarm)
                    :
                    await alarmService.Value.FindLastObservationsByField(patient.Id, data.Alarmfields);

            if (observations.Count > 0)
            {
                observations = observations.OrderBy(p => p.Time).ToList();
                foreach (var observation in observations)
                {
                    var obs = await alarmService.Value.MapObservationsByName(observation);
                    if (obs != null) _ = SendAsync(connectionId, OperationType.Alarm, obs);
                }
            }
        }
        catch (Exception e)
        {
            _logger.Error(
                "Exception sending person Data: {eMessage} person: {patient} Message Data: {data} {eStackTrace} ",
                e.Message, patient, data, e.StackTrace);
        }
    }


    /// <summary>
    /// Asynchronously retrieves and sends the latest pump observations for a patient over the specified connection.
    /// Observations with a value of zero are skipped when <c>_sendPumpsZero</c> is false; any exception raised during retrieval, mapping, or sending is caught and logged.
    /// </summary>
    /// <param name="patient">The patient whose latest pump observations should be retrieved and sent.</param>
    /// <param name="data">Message metadata containing the timestamp used to scope the lookup of recent observations.</param>
    /// <param name="connectionId">The identifier of the connection to which the mapped pump observations are dispatched.</param>
    /// <param name="ct">Token used to cancel the asynchronous operation.</param>
    private async Task SendLatestPumpsAsync(
            Patient patient,
            MessageData data,
            string connectionId,
            CancellationToken ct = default)
    {
        try
        {
            var observations = await pumpService.Value.FindLastPumpObservations(patient.Id, data.Last);
            if (observations.Count > 0)
                foreach (var observation in observations)
                {
                    if (!_sendPumpsZero && observation.Number == 0)
                        continue;

                    var obs = await pumpService.Value.MapPumpObservation(observation);
                    _ = SendAsync(connectionId, OperationType.Pump, obs);
                }
        }
        catch (Exception e)
        {
            _logger.Error(
                "Exception sending pump Data: {eMessage} person: {patient} Message Data: {data} ",
                e.Message, patient, data);
        }
    }


    /// <summary>
    /// Asynchronously retrieves up to the 50 most recent treatments for the specified patient and sends them
    /// over the given connection. If no treatments are found, the method completes without sending any data.
    /// Any exception encountered while loading or sending treatments is caught and logged.
    /// </summary>
    /// <param name="patient">The patient whose treatments should be retrieved and transmitted.</param>
    /// <param name="data">The message data associated with the current operation.</param>
    /// <param name="connectionId">The identifier of the connection used to dispatch the treatment messages.</param>
    private async Task SendTreatmentsAsync(
            Patient patient,
            MessageData data,
            string connectionId)
    {
        try
        {
            var treatments = await treatmentService.Value.GetTreatmentsByPatientId(patient.Id);
            var patientTreatments = treatments.ToList();
            if (patientTreatments.Count != 0)
                foreach (var treatment in patientTreatments.OrderByDescending(t => t.OrderTime)
                             .Take(50))
                    _ = SendAsync(connectionId, OperationType.Treatment, treatment);
        }
        catch (Exception e)
        {
            _logger.Error(
                "Exception sending treatments Data. connection id {connectionId},  person: {patient} Exception: {eMessage} ",
                connectionId, patient, e);
        }
    }


    /// <summary>
    /// Asynchronously retrieves and sends up to 50 most recent diagnosis records for the specified patient over the given connection.
    /// If no diagnoses are found, no messages are sent; any errors encountered are caught and logged.
    /// </summary>
    /// <param name="patient">The patient whose diagnosis records should be retrieved and sent.</param>
    /// <param name="data">The message data associated with the current operation, used for logging purposes.</param>
    /// <param name="connectionId">The identifier of the connection used to send the diagnosis messages.</param>
    private async Task SendDiagnosisAsync(
            Patient patient,
            MessageData data,
            string connectionId)
    {
        try
        {
            var diagnosis = await diagnosisService.Value.GetByPatientId(patient.Id);
            var patientDiagnosis = diagnosis.ToList();
            if (patientDiagnosis.Any())
                foreach (var diag in patientDiagnosis.OrderByDescending(t => t.Time).Take(50))
                    _ = SendAsync(connectionId, OperationType.Diagnosis, diag);
        }
        catch (Exception e)
        {
            _logger.Error(
                "Exception sending Diagnosis Data: {eMessage} person: {patient} message data: {data} ",
                e.Message, patient, data);
        }
    }


    /// <summary>
    /// Asynchronously sends grouped observations for the specified patient over the given connection,
    /// selecting the demo or production generation path and applying the provided timezone when available.
    /// Returns early when <see cref="MessageData.GroupedFields"/> is null and logs any exception encountered
    /// while processing an individual grouped field.
    /// </summary>
    /// <param name="patient">The patient whose grouped observations are being sent.</param>
    /// <param name="data">The message data containing the grouped fields, demo flag, and optional timezone used to drive the generation path.</param>
    /// <param name="connectionId">The identifier of the connection used to dispatch the grouped observations.</param>
    /// <param name="ct">The cancellation token used to cancel the grouped observation generation.</param>
    private async Task SendGroupedObsAsync(
            Patient patient,
            MessageData data,
            string connectionId,
            CancellationToken ct = default)
    {
        var groupedFields = data.GroupedFields;
        if (groupedFields == null)
            return;
        foreach (var groupedField in groupedFields)
            try
            {
                var groupedObservation = data.IsDemo
                    ? await observationDemoService.GenerateGroupedObservation(patient, groupedField)
                    : data.TimeZoneId != null
                        ? await groupedObservationService.GenerateGroupedObservation(patient.Id,
                            groupedField, data.TimeZoneId, ct: ct)
                        : await groupedObservationService.GenerateGroupedObservation(patient.Id,
                            groupedField, ct: ct);
                subscriberGroupedService.CheckOnSubscriptionGroup(groupedField, patient.Id,
                    data.TimeZoneId ?? "Romance Standard Time",
                    connectionId, groupedObservation);


                await SendAsync(connectionId, OperationType.GroupedObservation, groupedObservation);
            }
            catch (Exception e)
            {
                _logger.Error(
                    "Exception sending Grouped Data:  person: {patient} message data: {data} Exception {eMessage}",
                    patient, data, e);
            }
    }

    /// <summary>
    /// Asynchronously sends beacon color information for the specified point of care to the client associated with the given connection identifier.
    /// </summary>
    /// <param name="poc">The point of care whose beacon color and identifiers will be transmitted.</param>
    /// <param name="connectionId">The identifier of the target client connection that will receive the beacon payload.</param>
    private async Task SendBeaconsData(
            PointOfCare poc,
            string connectionId)
    {
        var beaconColor = await lightBeaconService.Value.GetColor(poc);
        BeaconResponse bc = new(beaconColor.ToString(), poc.Id, poc.UnitId);
        _ = SendAsync(connectionId, OperationType.Beacon, bc);
    }

    #endregion

    #region Add Subscriber

    /// <summary>
    /// Maps a message operation to a WebSocket subscription type and either registers a new subscriber or updates an existing one identified by the connection identifier.
    /// </summary>
    /// <param name="msg">The incoming message whose operation determines the subscription type and whose data provides the section and bed/box information.</param>
    /// <param name="connectionId">The unique identifier of the WebSocket connection used to look up or register the subscriber.</param>
    private void AddMessagetoSubcriber(Message msg, string connectionId)
    {
        SubscriptionEnum.WsType? subType = null;
        switch (msg.Operation)
        {
            case SubscriptionEnum.Type.SectionSubscribe:
                subType = SubscriptionEnum.WsType.Section;
                break;
            case SubscriptionEnum.Type.PatientData:
                //    subscriptionType = SubscriptionType.person;
                break;
            case SubscriptionEnum.Type.Idle:
                //    subscriptionType = SubscriptionType.Idle;
                break;
            case SubscriptionEnum.Type.BoxSubscribe:
                subType = SubscriptionEnum.WsType.Box;
                break;
            case SubscriptionEnum.Type.ChatMessage:
                break;
            case SubscriptionEnum.Type.RecordingSuscribe:
                break;
            case SubscriptionEnum.Type.GroupedObservations:
                break;
            case SubscriptionEnum.Type.Sensors:
                break;
            default:
                subType = SubscriptionEnum.WsType.Section;
                break;
        }

        var subscriber = subscribersService.GetSubscribers().FirstOrDefault(s => s.Id == connectionId);

        if (subscriber != null)
        {
            _logger.Debug($"Ws ConnectionId {connectionId} exists, " +
                          (subscriber.Box != msg.Data?.Location?.Bed
                              ? $"updated bed and box from {subscriber.Box ?? "null"} to {msg.Data?.Location?.Bed ?? "null"} section: {msg.Data?.Section} "
                              : $"bed and box {subscriber.Box ?? "null"} not updated ") +
                          (subscriber.Section != msg.Data?.Section
                              ? $"updated section from {subscriber.Section ?? "null"} to {msg.Data?.Section ?? "null"} "
                              : $"section {subscriber.Box ?? "null"} not updated ")
            );
            subscriber.Box = msg.Data?.Location?.Bed;
            subscriber.Section = msg.Data?.Section;
            subscriber.SubscriptionType = subType;
        }
        else
        {
            subscriber = new WsSubscriber(connectionId)
            {
                Box = msg.Data?.Location?.Bed,
                Section = msg.Data?.Section,
                SubscriptionType = subType
            };

            subscribersService.AddSubscriber(subscriber);
            _logger.Debug("Ws ConnectionId NOT exists, created new WebSocket subscriber {subscriber}",
                JsonConvert.SerializeObject(subscriber));
        }
    }

    /// <summary>
    /// Associates an incoming message display with a WebSocket subscriber identified by <paramref name="connectionId"/>, updating the existing subscriber's locations, location ids, subscription type, display id, locale, and user name when present, or creating and registering a new subscriber when no match is found.
    /// </summary>
    /// <param name="msg">The incoming message whose display data (locations, location ids, display id, locale, and user name) is applied to the subscriber.</param>
    /// <param name="connectionId">The unique identifier of the WebSocket connection used to look up an existing subscriber.</param>
    /// <param name="subType">The optional WebSocket subscription type to assign to the subscriber.</param>
    private void AddMessageDisplayToSubcriber(Message msg, string connectionId, SubscriptionEnum.WsType? subType)
    {
        var subscriber = subscribersService.GetSubscribers().FirstOrDefault(s => s.Id == connectionId);

        if (subscriber != null)
        {
            _logger.Debug($"Ws ConnectionId {connectionId} exists, " +
                          (subscriber.Locations != msg.Data?.Locations
                              ? $"updated Locations from {JsonConvert.SerializeObject(subscriber.Locations)} to {JsonConvert.SerializeObject(msg.Data?.Locations)}"
                              : $"Locations: {JsonConvert.SerializeObject(subscriber.Locations)} not updated "));
            subscriber.Locations = msg.Data?.Locations ?? [];
            subscriber.LocationIds = msg.Data?.LocationIds ?? [];
            subscriber.SubscriptionType = subType;
            _ = msg.Data?.DisplayId != null ? subscriber.DisplayId = ObjectId.Parse(msg.Data?.DisplayId) : subscriber.DisplayId = null;
            subscriber.Locale = msg.Data?.Locale;
            subscriber.UserName = msg.Data?.UserName;
        }
        else
        {
            subscriber = new WsSubscriber(connectionId)
            {
                Locations = msg.Data?.Locations ?? [],
                LocationIds = msg.Data?.LocationIds ?? [],
                SubscriptionType = subType,
                DisplayId = msg.Data?.DisplayId != null ? ObjectId.Parse(msg.Data?.DisplayId) : null,
                Locale = msg.Data?.Locale,
                UserName = msg.Data?.UserName
            };

            subscribersService.AddSubscriber(subscriber);
            _logger.Debug("Ws ConnectionId NOT exists, created new WebSocket subscriber {subscriber}",
                JsonConvert.SerializeObject(subscriber));
        }
    }

    #endregion

    #region Send To Client Hub

    /// <summary>
    /// Asynchronously serializes the provided message to JSON using camelCase property naming and string-based enum conversion, then delivers it to the SignalR client identified by <paramref name="subscriberId"/> via the "ReceiveMessage" event.
    /// </summary>
    /// <param name="subscriberId">The identifier of the SignalR client connection that should receive the message.</param>
    /// <param name="type">The optional operation type associated with the message, included in the payload as its string representation when present.</param>
    /// <param name="msg">The message object to serialize and send. May be <c>null</c>.</param>
    public async Task SendAsync(string subscriberId, OperationType? type, object? msg)
    {
        try
        {
            var settings = new JsonSerializerSettings
            {
                ContractResolver = new CamelCasePropertyNamesContractResolver(),
                Converters = new List<JsonConverter> { new StringEnumConverter() }
            };
            var json = JsonConvert.SerializeObject(msg, Formatting.Indented, settings);

            await Hub.Clients.Client(subscriberId).SendAsync("ReceiveMessage", type?.ToString(), json);
        }
        catch (Exception ex)
        {
            _logger.Error("Error serializing message {msg}, Exception: {ex}", msg as MessageData, ex);
            throw;
        }
    }

    /// <summary>
    /// Asynchronously broadcasts a message of the specified operation type to all connected SignalR clients,
    /// serializing the payload using camelCase JSON with ISO date formatting, suppression of null and default values,
    /// and roundtrip DateTime kind preservation.
    /// </summary>
    /// <param name="type">The operation type that identifies the kind of message being broadcast.</param>
    /// <param name="msg">The message payload to serialize and send. Can be null.</param>
    public async Task SendToAllAsync(OperationType type, object? msg)
    {
        var settings = new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver(),
            NullValueHandling = NullValueHandling.Ignore, // NULL ARE NOT SHOWED
                                                          // REMOVED TO RETURN TO FRONTEND IN SPECIFIED TIME ZONE IF Kind Specified. DateTimeZoneHandling = DateTimeZoneHandling.Utc, // DATE IN UTC TIME ZONE
            DateTimeZoneHandling = DateTimeZoneHandling.RoundtripKind,
            DateFormatHandling = DateFormatHandling.IsoDateFormat, // DATE IN ISO FORMAT
            DefaultValueHandling = DefaultValueHandling.Ignore // IGNORE SPECIFIC DEFAULT VALUES
        };

        await Hub.Clients.All.SendAsync("ReceiveMessage", type.ToString(),
            JsonConvert.SerializeObject(msg, Formatting.Indented, settings));
    }

    /// <summary>
    /// Sends a serialized error payload to the client identified by the given connection identifier through the SignalR hub.
    /// </summary>
    /// <param name="errorMessage">The error description to include in the message body sent to the client.</param>
    /// <param name="connectionId">The SignalR connection identifier of the target client that should receive the error message.</param>
    private async Task SendErrorMessage(string errorMessage, string connectionId)
    {
        var msg = JsonConvert.SerializeObject(new Dictionary<string, object>
            {
                { "error", true },
                { "message", errorMessage }
            });

        await Hub.Clients.Client(connectionId).SendAsync("ReceiveMessage", OperationType.Error, msg);
    }

    #endregion

    #endregion
}