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.MongoModels;
using adas_core.Domain.Models.Pumps;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;

namespace adas_core.Application.Customizations.HRYC;

/// <summary>
/// Implements the HRYC-specific clinical calculations that derive secondary
/// <see cref="PatientObservation"/> values (NEWS alarms, IROX, Resp_Rate_Calculated, Hydric_Balance_Calculated,
/// Weight_Diff, Diuresis_Weight, Delta_Pressure, Daily_Balance_Calculated, Allergies, DVE, Drainage_Height,
/// Resp_Type, and Hour_Balance) from incoming raw observations and active configurations loaded from
/// <see cref="ApiSettings"/>.
/// </summary>
public class CalculatedObservations : ICalculatedObservations
{
    private readonly IOptions<ApiSettings> _apiSettings;
    private readonly List<string> _codesForInvasiveVentilation = [];

    private readonly IConfigObservationService _configObservationService;

    //private readonly Lazy<IBoxService> _boxService;
    private readonly List<string> _highFrequencyVentilation = [];
    private readonly Lazy<ILightBeaconService> _lightBeaconService;
    private readonly ILogger<CalculatedObservations> _logger;
    private readonly List<string> _nonInvasiveVentilation = [];
    private readonly Lazy<IObservationService> _observationService;
    private readonly Lazy<IPatientService> _patientService;


    /// <summary>
    /// Initializes a new instance of the <see cref="CalculatedObservations"/> class,
    /// resolving its dependencies (observation, patient, light-beacon, and config-observation services,
    /// plus the logger) from the supplied <see cref="IServiceProvider"/> and loading the configured
    /// code catalogues (high-frequency ventilation, non-invasive ventilation, and invasive ventilation)
    /// from <see cref="ApiSettings"/>.
    /// </summary>
    /// <param name="serviceProvider">The application's service provider used to resolve the required dependencies and configuration.</param>
    public CalculatedObservations(IServiceProvider serviceProvider)
    {
        _observationService = serviceProvider.GetRequiredService<Lazy<IObservationService>>(); //observationService;
        _patientService = serviceProvider.GetRequiredService<Lazy<IPatientService>>();
        _apiSettings = serviceProvider.GetRequiredService<IOptions<ApiSettings>>(); //apiSettings;
        //_boxService = serviceProvider.GetRequiredService<Lazy<IBoxService>>();
        _logger = serviceProvider.GetRequiredService<ILogger<CalculatedObservations>>();
        _lightBeaconService = serviceProvider.GetRequiredService<Lazy<ILightBeaconService>>();
        _configObservationService =
            serviceProvider.GetRequiredService<IConfigObservationService>(); //configObservationService;


        var highFrenquencyVentilation = _apiSettings.Value.HighFrequencyVentilation ?? null;
        highFrenquencyVentilation?.ForEach(x => _highFrequencyVentilation.Add(x.Trim()));


        var nonInvasiveVentilation = _apiSettings.Value.NonInvasiveVentilation ?? null;
        nonInvasiveVentilation?.ForEach(x => _nonInvasiveVentilation.Add(x.Trim()));

        var codesForInvasiveVentilation = //TODO comprobar si se refiere a InvasiveVentilation
            _apiSettings.Value.InvasiveVentilation ?? null;
        codesForInvasiveVentilation?.ForEach(x => _codesForInvasiveVentilation.Add(x.Trim()));
    }


    /// <summary>
    /// Dispatches the supplied raw observation to the appropriate HRYC calculation based on its
    /// <see cref="BasePatientObservation.Name"/>. The set of supported calculations includes
    /// <c>Resp_Mode</c> (ventilation type), <c>Diuresis</c> / <c>Weight_Current</c>
    /// (weight difference and diuresis-per-kilogram), <c>AllergiesObs</c>, <c>DrainagesObs</c>,
    /// <c>PEEP</c> / <c>Pleateu_Pressure</c> (driving pressure), <c>Daily_Balance</c>,
    /// <c>Hydric_Balance</c> / <c>Hour_Balance</c> (time shifting), <c>FR</c> / <c>Vent_Rate</c>
    /// (respiratory rate), <c>SpO2</c> / <c>FiO2</c> (IROX), and <c>NEWS</c> (alarms).
    /// </summary>
    /// <typeparam name="T">The concrete observation type, deriving from <see cref="BasePatientObservation"/>.</typeparam>
    /// <param name="obs">The observation to map or transform in-place.</param>
    /// <param name="onlyByName">
    /// Reserved for future use. When <see langword="true"/>, restricts the mapping strategy to
    /// name-based lookups only.
    /// </param>
    /// <returns>
    /// A <see cref="Task{T}"/> that resolves to the (possibly transformed) observation,
    /// or <see langword="null"/> when the input observation has no name.
    /// </returns>
    public async Task<T?> Map<T>(T obs, bool onlyByName) where T : BasePatientObservation
    {
        _logger.LogTrace("Mapping {name} observation {obs}", obs.Name, obs);

        switch (obs.Name)
        {
            case "Resp_Mode":
                _logger.LogTrace("Mapping Resp mode observation {obs}", obs);
                await CalculateVentilationMode(obs);
                break;
            case "Diuresis":
            case "Weight_Current":
                if (obs.Name == "Weight_Current") await CalculateWeight_DiffObservation(obs);
                await CalculateDiureis_WeightObservation(obs);
                break;
            case "AllergiesObs":
                await CalculateAllergiesObservation(obs);
                break;
            case "DrainagesObs":
                await CalculateDrainagesObservation(obs);
                break;
            case "PEEP":
            case "Pleateu_Pressure":
                await CalculateDelta_PressureObservation(obs);
                break;
            case "Daily_Balance":
                await CalculateDaily_BalanceObservation(obs);
                break;
            case "Hydric_Balance":
                await CalculateHydricBalanceCalculated(obs);
                obs = (T)await CalculateHydricBalance(obs);
                break;
            case "Hour_Balance":
                obs = (T)await CalculateHourBalance(obs);
                break;
            case "FR":
                await CalculateRespRate(obs);
                await CalculateIrox(obs);
                break;
            case "Vent_Rate":
                await CalculateRespRate(obs);
                break;
            case "SpO2":
            case "FiO2":
                await CalculateIrox(obs);
                break;
            case "NEWS":
                await CalculateNews(obs);
                break;
        }

        return obs;
    }


    /// <summary>
    /// Placeholder mapping for a <see cref="PatientTreatment"/>. The HRYC customization does not
    /// currently derive observations from treatments.
    /// </summary>
    /// <param name="treatment">The treatment to map.</param>
    /// <returns>Never returns a result.</returns>
    /// <exception cref="NotImplementedException">Always thrown because treatment mapping is not implemented in the HRYC customization.</exception>
    public Task<PatientTreatment> Map(PatientTreatment treatment)
    {
        throw new NotImplementedException();
    }


    /// <summary>
    /// Identity mapping for a <see cref="PumpObservation"/>: returns the supplied instance unchanged,
    /// wrapped in a completed task. Provided to satisfy the customization contract; pump observations
    /// do not require HRYC-specific calculation.
    /// </summary>
    /// <param name="pumpObservation">The pump observation to map.</param>
    /// <returns>A task containing the same <see cref="PumpObservation"/> instance that was passed in.</returns>
    public async Task<PumpObservation> Map(PumpObservation pumpObservation)
    {
        return await Task.FromResult(pumpObservation);

    }

    /// <summary>
    /// No-op implementation of the medicine-observation calculation hook. The HRYC customization does
    /// not derive Medication or ERMedication observations from the active medicine list.
    /// </summary>
    /// <param name="activeMedicines">The list of active medicines (ignored).</param>
    /// <param name="patientId">The unique identifier of the patient (ignored).</param>
    /// <returns>A completed task.</returns>
    public Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
    {
        return Task.CompletedTask;
    }

    /// <summary>
    /// No-op implementation of the active-bolus calculation hook. The HRYC customization does not
    /// derive an OpiateBoluses observation.
    /// </summary>
    /// <param name="patientId">The unique identifier of the patient (ignored).</param>
    /// <returns>A completed task.</returns>
    public Task CalculateActiveBolus(ObjectId patientId)
    {
        return Task.CompletedTask;
    }

    /// <summary>
    /// Returns an empty enumerable of active treatments. The HRYC customization does not currently
    /// maintain a per-patient active-treatment cache.
    /// </summary>
    /// <param name="id">The unique identifier of the patient (ignored).</param>
    /// <returns>A completed task containing an empty <see cref="IEnumerable{PatientTreatment}"/>.</returns>
    public Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
    {
        return Task.FromResult(new List<PatientTreatment?>().AsEnumerable());
    }

    /// <summary>
    /// Identity mapping for a <see cref="PatientDiagnosis"/>: returns the supplied instance unchanged,
    /// wrapped in a completed task. Provided to satisfy the customization contract.
    /// </summary>
    /// <param name="diagnosis">The <see cref="PatientDiagnosis"/> to map.</param>
    /// <returns>A task containing the same <see cref="PatientDiagnosis"/> instance that was passed in.</returns>
    public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
    {
        return Task.FromResult(diagnosis);
    }



    /// <summary>
    /// Resolves time-inconsistencies for a new patient observation when it arrives with the same
    /// (down-to-the-second) timestamp as the latest stored observation of the same name. The new
    /// observation's <c>Time</c> is shifted forward by one second to avoid duplicate-key collisions.
    /// </summary>
    /// <param name="newObservation">The new patient observation to evaluate for time inconsistencies.</param>
    /// <returns>
    /// A task that resolves to the (possibly time-shifted) <see cref="PatientObservation"/>.
    /// Returns the input unchanged when its <c>Name</c> is null or empty, logging the error.
    /// </returns>
    public async Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
    {
        if (string.IsNullOrEmpty(newObservation.Name))
        {
            _logger.LogError(
                "Error FixTimeInconsistencyWithLast newObservation name ia null or empty. Observation: {newObservation}",
                newObservation);
            return newObservation;
        }

        var lastObservations = await _observationService.Value.FindLastObservations(newObservation.PatientId, 1,
            [newObservation.Name]);
        var lastObservation = lastObservations.FirstOrDefault();

        if (lastObservation != null && DateTime.Compare(
                new DateTime(lastObservation.Time.Year, lastObservation.Time.Month,
                    lastObservation.Time.Day, lastObservation.Time.Hour,
                    lastObservation.Time.Minute, lastObservation.Time.Second),
                new DateTime(newObservation.Time.Year, newObservation.Time.Month,
                    newObservation.Time.Day, newObservation.Time.Hour,
                    newObservation.Time.Minute, newObservation.Time.Second)
            ) >= 0)
            newObservation.Time = lastObservation.Time.AddSeconds(1);

        return newObservation;
    }

    /// <summary>
    /// Pre-maps a batch of patient observations, optimising the order in which they are persisted
    /// when both <c>Vent_Rate</c> and <c>FR</c> are present. If both observations are present with
    /// non-zero values, <c>Vent_Rate</c> is inserted first (and removed from the returned list) so
    /// that <see cref="CalculateRespRate"/> can derive <c>Resp_Rate_Calculated</c> correctly.
    /// </summary>
    /// <param name="listToInsert">The list of patient observations to pre-map. Modified in-place.</param>
    /// <returns>
    /// A task that resolves to the resulting list of observations (with <c>Vent_Rate</c> removed
    /// when it was inserted synchronously, or the original list otherwise).
    /// </returns>
    public async Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToInsert)
    {
        var ventRate = listToInsert.FirstOrDefault(obs => obs.Name == "MDC_VENT_RESP_RATE");
        var respRate = listToInsert.FirstOrDefault(obs => obs.Name == "MDC_RESP_RATE");

        // Check if obs for Vent_Rate and FR is on the list to insert
        if (ventRate == null || respRate == null) return listToInsert;

        var ventRateValue = double.TryParse(ventRate.Value.ToString(), out var v1) ? v1 : 0;
        // ventRate is on the list to insert but its value is 0
        if (ventRateValue == 0)
        {
            _logger.LogDebug("Find ventRate on PreMap request not processed because its value is 0");
            return listToInsert;
        }

        var respRateValue = double.TryParse(ventRate.Value.ToString(), out var v2) ? v2 : 0;
        // respRate is on the list to insert but its value is 0
        if (respRateValue == 0)
        {
            _logger.LogDebug("Find respRate on PreMap request not processed because its value is 0");
            return listToInsert;
        }


        // Exists both obs and have value != 0
        // In that case we need to calculate Resp_Rate_Calculated just with the value of Vent_Rate
        try
        {
            _logger.LogDebug(
                "Find on PreMap Vent_Rate and FR in the same request process Vent_Rate first to calculate Resp_Rate_Calculated");
            await _observationService.Value.InsertObservation(ventRate);
            listToInsert.Remove(ventRate);
        }
        catch (Exception e)
        {
            _logger.LogError("Error while PreMap on insert obs: {ventRate} exception: {e}", ventRate, e);
        }

        return listToInsert;
    }

    /// <summary>
    /// Identity mapping for a source alarm observation paired with a <see cref="PatientObservationAlarm"/>:
    /// returns the supplied observation unchanged, wrapped in a completed task. Provided to satisfy the
    /// customization contract.
    /// </summary>
    /// <param name="obs">The patient observation that triggered the alarm.</param>
    /// <param name="alarmToInsert">The alarm metadata to be inserted alongside the observation.</param>
    /// <returns>A task containing the same <see cref="PatientObservation"/> instance that was passed in.</returns>
    public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
    {
        return Task.FromResult(obs);
    }

    /// <summary>
    /// Placeholder alarm dispatch hook used by the customization contract. The HRYC customization
    /// dispatches NEWS alarms through its private <c>SendAlarm(BasePatientObservation, AlarmEnum.Name)</c>
    /// overload; calling this public overload always throws.
    /// </summary>
    /// <param name="obs">The patient observation that triggered the alarm.</param>
    /// <param name="name">The alarm display name.</param>
    /// <param name="code">The optional alarm code from <see cref="AlarmEnum.Name"/>.</param>
    /// <returns>Never returns a result.</returns>
    /// <exception cref="NotImplementedException">Always thrown because this overload is not used in the HRYC customization.</exception>
    public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
    {
        throw new NotImplementedException();
    }

    /// <summary>
    /// Translates a NEWS (National Early Warning Score) value into an alarm level: a value below 5
    /// (or an unparsable value) is reported as <c>NewsOff</c>, between 5 and 7 as <c>NewsWarning</c>,
    /// and 7 or above as <c>NewsAlert</c>. Returns early when the patient cannot be located.
    /// </summary>
    /// <param name="obs">The base patient observation that triggered the calculation, expected to be a <see cref="PatientObservation"/> whose <c>Name</c> is <c>NEWS</c>.</param>
    /// <returns>A task that represents the asynchronous alarm evaluation.</returns>
    private async Task CalculateNews(BasePatientObservation obs)
    {
        var pobs = (PatientObservation)obs;
        var parsed = int.TryParse(pobs.Value.ToString(), out var valueParsed);
        var patient = await _patientService.Value.FindById(obs.PatientId);
        if (patient == null) return;
        //var beaconConfig = await _boxService.Value.GetBeaconConfig(new PatientLocation(patient.PointOfCare, patient.Bed));

        //power off beacon

        //recuperar la beacon del paciente y encenderla apagarla si el valor es > 5&6 o 7 alert

        if ((parsed && valueParsed < 5) || !parsed)
            //await _balizaService.Value.PowerOffLed(beaconCnf);
            //await _observationService.Value.SendObsBroadcast(obs);
            await SendAlarm(obs, AlarmEnum.Name.NewsOff);
        else
            switch (valueParsed)
            {
                case >= 5 and < 7:
                    _logger.LogDebug("beacon yellow alert  by NEWS for patient: {patientid}", obs.PatientId);
                    await SendAlarm(obs, AlarmEnum.Name.NewsWarning);
                    //await _balizaService.Value.SendBeaconColor(patient, LightBeaconColor.YELLOW);
                    break;
                case >= 7:
                    _logger.LogDebug("beacon red alert  by NEWS for patient: {patientid}", obs.PatientId);
                    await SendAlarm(obs, AlarmEnum.Name.NewsAlert);
                    //await _balizaService.Value.SendBeaconColor(patient, LightBeaconColor.RED);
                    break;
            }

        //if (obsAlarm == null) return;
    }

    /// <summary>
    /// Builds a synthetic <c>Alarm_&lt;Name&gt;</c> observation, looks up its configuration, and when
    /// the configured alarm and its beacon are enabled, dispatches a light-beacon colour change for
    /// the patient's point-of-care. The alarm observation is always persisted. Errors are logged and
    /// swallowed.
    /// </summary>
    /// <param name="obs">The base patient observation that triggered the alarm (used to derive <c>PatientId</c> and <c>Time</c>).</param>
    /// <param name="name">The <see cref="AlarmEnum.Name"/> describing the alarm kind (for example, <c>NewsOff</c>, <c>NewsWarning</c>, <c>NewsAlert</c>).</param>
    /// <returns>A task that represents the asynchronous alarm dispatch.</returns>
    private async Task SendAlarm(BasePatientObservation obs, AlarmEnum.Name name)
    {
        var pobs = (PatientObservation)obs;
        PatientObservation nObs = new()
        {
            CodingSystem = "ADAS_ALARM",
            Code = name.ToString(),
            Name = $"Alarm_{name}",
            PatientId = pobs.PatientId,
            Time = pobs.Time
        };
        try
        {
            //ConfigObservations
            var configObs = await _configObservationService.Get(new PatientObservation
            {
                Name = nObs.Name,
                PatientId = obs.PatientId
            }
            );
            if (configObs == null)
            {
                _logger.LogError("Config observation is null on send alarm NEWS for {nObs}", nObs);
                return;
            }

            if (configObs is { Alarm.Enabled: true })
            {
                nObs.Alarm = configObs.Alarm;

                if (configObs.Alarm.Beacon is { Enabled: true })
                {
                    var patient = await _patientService.Value.FindById(obs.PatientId);
                    if (patient != null)
                    {
                        _logger.LogDebug(
                            "PatientId: {nObsPatientId}. Send Beacon alarmName {configObsAlarmBeaconBeaconColorValue}",
                            nObs.PatientId, configObs.Alarm.Beacon.BeaconColor);
                        nObs.Value = configObs.Alarm.Beacon.BeaconColor.ToString();
                        SendBeaconCode(configObs.Alarm.Beacon.BeaconColor, patient);
                    }
                }
            }

            _logger.LogDebug(
                "Insert obs alarm for news at: {DateTimeNow} patientId: {nObsPatientId} color value: {nObsValue}",
                DateTime.Now, nObs.PatientId, nObs.Value);
            await _observationService.Value.InsertObservation(nObs);
        }
        catch (Exception ex)
        {
            _logger.LogError("Exception sending alarm: {exMessage}", ex.Message);
        }
    }

    /// <summary>
    /// Sends a colour command to the light beacon associated with the patient's point-of-care.
    /// Maps <see cref="AlarmEnum.BeaconColor"/> values to <see cref="LightBeaconColor"/> commands
    /// (blue, yellow, red, or off) and logs an error when the patient has no point-of-care identifier.
    /// </summary>
    /// <param name="color">The beacon colour to apply.</param>
    /// <param name="patient">The patient whose associated beacon should be updated.</param>
    private void SendBeaconCode(AlarmEnum.BeaconColor color, Patient patient)
    {
        if (!patient.PointOfCareId.HasValue)
        {
            _logger.LogError("Error sending beacon color on calculateObservations poc id on patient is null {Patient}",
                patient.ToString());
            return;
        }

        switch (color)
        {
            case AlarmEnum.BeaconColor.Blue:
                _lightBeaconService.Value.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Blue);
                break;
            case AlarmEnum.BeaconColor.Yellow:
                _lightBeaconService.Value.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Yellow);
                break;
            case AlarmEnum.BeaconColor.Red:
                _lightBeaconService.Value.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Red);
                break;
            case AlarmEnum.BeaconColor.None:
                _lightBeaconService.Value.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Off);
                break;
        }
    }

    /// <summary>
    /// Computes the IROX index (<c>SpO2 / FiO2 / FR</c>) when <c>SpO2</c>, <c>FiO2</c>, and <c>FR</c>
    /// are all present in the latest observations and were recorded within the last 10 minutes.
    /// Persists the resulting <c>IROX</c> observation. Skips silently when any input is missing,
    /// zero, or unparsable. Errors are logged and swallowed.
    /// </summary>
    /// <param name="obs">The base patient observation that triggered the calculation (<c>SpO2</c>, <c>FiO2</c>, or <c>FR</c>).</param>
    /// <returns>A task that represents the asynchronous calculation operation.</returns>
    private async Task CalculateIrox(BasePatientObservation obs)
    {
        try
        {
            var pob = (PatientObservation)obs;
            var obsToCalc = new List<PatientObservation> { pob };
            switch (pob.Name)
            {
                case "SpO2":

                    var fio2Frvalues = await _observationService.Value.FindLastObservations(obs.PatientId, 1,
                        ["FiO2", "FR"]);
                    if (fio2Frvalues.Count > 0) obsToCalc.AddRange(fio2Frvalues);

                    break;
                case "FiO2":
                    var spo2Frvalues = await _observationService.Value.FindLastObservations(obs.PatientId, 1,
                        ["SpO2", "FR"]);
                    if (spo2Frvalues.Count > 0) obsToCalc.AddRange(spo2Frvalues);

                    break;
                case "FR":
                    var fio2Spo2Values = await _observationService.Value.FindLastObservations(obs.PatientId, 1,
                        ["FiO2", "SpO2"]);

                    if (fio2Spo2Values.Count > 0) obsToCalc.AddRange(fio2Spo2Values);


                    break;
            }

            if (!obsToCalc.All(o => o.Time.CompareTo(DateTime.UtcNow.AddMinutes(10)) <= 0)) return;

            var fio2 = obsToCalc.FirstOrDefault(o => o.Name == "FiO2");
            var fr = obsToCalc.FirstOrDefault(o => o.Name == "FR");
            var spo2 = obsToCalc.FirstOrDefault(o => o.Name == "SpO2");
            if (fio2 == null || fr == null || spo2 == null) return;


            if (!double.TryParse(spo2.Value.ToString(), out var nSpo2) ||
                !double.TryParse(fio2.Value.ToString(), out var nFio2) ||
                !double.TryParse(fr.Value.ToString(), out var nFr))
            {
                _logger.LogWarning("Error parsing values. fio2: {fio2}, fr: {fr}, spo2: {spo2}", fio2, fr, spo2);
                return;
            }

            var spo2Value = nSpo2;
            var fio2Value = nFio2;
            var frValue = nFr;

            if (spo2Value == 0 || fio2Value == 0 || frValue == 0) return;


            var value = nSpo2 / nFio2 / nFr;

            var irox = new PatientObservation
            {
                Name = "IROX",
                CodingSystem = "ADAS",
                PatientId = obs.PatientId,
                Time = obs.Time,
                Value = value
            };
            await _observationService.Value.InsertObservation(irox);
        }
        catch (Exception e)
        {
            _logger.LogError("Exception calculating IROX, error: {eMessage}", e.Message);
        }
    }


    /// <summary>
    /// Persists a <c>Resp_Rate_Calculated</c> observation when the supplied observation's value is
    /// non-zero. For <c>FR</c>, the insert is skipped if a recent (within
    /// <c>CalculateRespRateVentExpires</c> minutes) non-zero <c>Vent_Rate</c> observation exists;
    /// the inserted observation's <c>Time</c> is set to the original <c>Time</c> for <c>FR</c>, or
    /// shifted one second forward for any other name.
    /// </summary>
    /// <param name="obs">The base patient observation that triggered the calculation (<c>FR</c> or <c>Vent_Rate</c>).</param>
    /// <returns>A task that represents the asynchronous insert operation.</returns>
    private async Task CalculateRespRate(BasePatientObservation obs)
    {
        var pobs = (PatientObservation)obs;
        _logger.LogDebug("CalculateRespRate {obsPatientid} wait {obsName} {obsTime} {pobsValue}", obs.PatientId,
            obs.Name, obs.Time, pobs.Value);
        if (Convert.ToDouble(pobs.Value) == 0) return;

        if (Convert.ToDouble(pobs.Value) == 0) return;

        var insert = true;
        if (obs.Name == "FR")
        {
            // Insertamos si no existe una obs Vent_Rate o si es existe tiene más de X" y su valor es > 0
            var lastVentRate =
                (await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["Vent_Rate"]))
                .FirstOrDefault();
            if (lastVentRate == null || (double.TryParse(lastVentRate.Value.ToString(), out var v) ? v : 0) == 0)
            {
                _logger.LogDebug("CalculateRespRate {id} lastVentRate: null", obs.PatientId);
            }
            else
            {
                var diff = DateTime.UtcNow.Subtract(lastVentRate.Time.ToUniversalTime());
                insert =
                    lastVentRate.Time.ToUniversalTime().AddMinutes(_apiSettings.Value.CalculateRespRateVentExpires) <
                    DateTime.UtcNow;
                _logger.LogDebug(
                    "CalculateRespRate {patientid} lastVentRate: {value} - {time} passed {diff} {insert}",
                    obs.PatientId, lastVentRate.Value, lastVentRate.Time, diff, insert ? "Inserted" : "Not inserted");
            }
        }

        if (insert)
        {
            _logger.LogDebug(
                "CalculateRespRate {patientid} Insert Resp_Rate_Calculated with {name} - {time} value {value}",
                obs.PatientId, obs.Name, obs.Time, pobs.Value);
            var calculatedRespRate = new PatientObservation
            {
                Id = new ObjectId(),
                Name = "Resp_Rate_Calculated",
                CodingSystem = "ADAS",
                PatientId = obs.PatientId,
                Time = obs.Name == "FR" ? obs.Time : obs.Time.AddSeconds(1),
                Value = pobs.Value,
                ParentData = new ParentDataClass { Name = obs.Name }
            };
            await _observationService.Value.InsertObservation(calculatedRespRate);
        }
        else
        {
            _logger.LogDebug(
                "CalculateRespRate {patientid} Insert Resp_Rate_Calculated with {name} - {time} value {value}",
                obs.PatientId, obs.Name, obs.Time, pobs.Value);
        }

        _logger.LogDebug("CalculateRespRate {patientid} release {name} {time} {value}", obs.PatientId,
            obs.Name, obs.Time, pobs.Value);
    }

    /// <summary>
    /// Shifts the timestamp of a <c>Hydric_Balance</c> observation forward by one second when another
    /// <c>Hydric_Balance</c> observation was already recorded within the same hour, so the most
    /// recent entry can be identified unambiguously.
    /// </summary>
    /// <param name="obs">The base patient observation expected to be a <see cref="PatientObservation"/> with <c>Name</c> <c>Hydric_Balance</c>.</param>
    /// <returns>A task that resolves to the (possibly time-shifted) base patient observation.</returns>
    private async Task<BasePatientObservation> CalculateHydricBalance(BasePatientObservation obs)
    {
        var pobs = (PatientObservation)obs;

        var lastHydricBalanceFromHour =
            await _observationService.Value.FindLastBeforeDate(pobs.PatientId, pobs.Time.AddMinutes(59),
                "Hydric_Balance");

        if (lastHydricBalanceFromHour != null &&
            DateTime.Compare(new DateTime(lastHydricBalanceFromHour.Time.Year, lastHydricBalanceFromHour.Time.Month,
                    lastHydricBalanceFromHour.Time.Day, lastHydricBalanceFromHour.Time.Hour, 0, 0)
                , new DateTime(pobs.Time.Year, pobs.Time.Month, pobs.Time.Day, pobs.Time.Hour, 0, 0)) == 0)
            pobs.Time = lastHydricBalanceFromHour.Time.AddSeconds(1);

        return obs;
    }

    /// <summary>
    /// Shifts the timestamp of a <c>Hour_Balance</c> observation forward by one second when another
    /// <c>Hour_Balance</c> observation was already recorded within the same hour.
    /// </summary>
    /// <param name="obs">The base patient observation expected to be a <see cref="PatientObservation"/> with <c>Name</c> <c>Hour_Balance</c>.</param>
    /// <returns>A task that resolves to the (possibly time-shifted) base patient observation.</returns>
    private async Task<BasePatientObservation> CalculateHourBalance(BasePatientObservation obs)
    {
        var pobs = (PatientObservation)obs;

        var lastHourBalanceFromHour =
            await _observationService.Value.FindLastBeforeDate(pobs.PatientId, pobs.Time.AddMinutes(59),
                "Hour_Balance");

        if (lastHourBalanceFromHour != null &&
            DateTime.Compare(new DateTime(lastHourBalanceFromHour.Time.Year, lastHourBalanceFromHour.Time.Month,
                    lastHourBalanceFromHour.Time.Day, lastHourBalanceFromHour.Time.Hour, 0, 0)
                , new DateTime(pobs.Time.Year, pobs.Time.Month, pobs.Time.Day, pobs.Time.Hour, 0, 0)) == 0)
            pobs.Time = lastHourBalanceFromHour.Time.AddSeconds(1);

        return obs;
    }

    /// <summary>
    /// Persists a <c>Hydric_Balance_Calculated</c> observation mirroring the supplied
    /// <c>Hydric_Balance</c> value, after enforcing three guards: the observation's hour must not be
    /// in the future, the latest <c>Hydric_Balance_Calculated</c> for the current hour prevents
    /// replaying older hours, and a newer stored <c>Hydric_Balance_Calculated</c> prevents
    /// overwriting it. The stored time is shifted one second forward when an existing calculated
    /// observation in the same hour is detected.
    /// </summary>
    /// <param name="obs">The base patient observation that triggered the calculation, expected to be a <see cref="PatientObservation"/> with <c>Name</c> <c>Hydric_Balance</c>.</param>
    /// <returns>A task that represents the asynchronous insert operation.</returns>
    private async Task CalculateHydricBalanceCalculated(BasePatientObservation obs)
    {
        //Hydric_Balance_Calculated 
        //No guardamos como calculadas nunca horas futuras. En el obx siempre vienen con la hora y los minutos a 00


        var pobs = (PatientObservation)obs;
        var nowUniversalTime = DateTime.Now.ToUniversalTime();
        //La hora de la observación es mayor que la actual. No generamos la calculada
        if (DateTime.Compare(new DateTime(obs.Time.Year, obs.Time.Month, obs.Time.Day, obs.Time.Hour, 0, 0),
                new DateTime(nowUniversalTime.Year, nowUniversalTime.Month, nowUniversalTime.Day, nowUniversalTime.Hour,
                    0, 0)) > 0
           )
        {
            _logger.LogDebug(
                "HYDRIC BALANCE: La hora de la observación es mayor que la actual. No generamos la calculada {obs}",
                obs);
            return;
        }

        //Para la calculada. Si la última que tenemos es de la hora actual y lo que viene es de una hora anterior no entra como calculada.
        var lastHydricBalanceList = await _observationService.Value.FindLastObservations(obs.PatientId, 1,
            ["Hydric_Balance_Calculated"]);
        var lastHydricBalance = lastHydricBalanceList.FirstOrDefault();

        if (lastHydricBalance != null && lastHydricBalance.Time.Hour == DateTime.UtcNow.Hour &&
            obs.Time.Hour != DateTime.UtcNow.Hour)
        {
            _logger.LogDebug(
                "HYDRIC BALANCE: ultima que tenemos es de la hora actual y lo que viene es de una hora anterior no entra como calculada {obs}",
                obs);
            return;
        }

        //Si el último hydric balance es de una hora posterior a la observación que acaba de llegar no entra como calculada.
        if (lastHydricBalance != null &&
            DateTime.Compare(new DateTime(lastHydricBalance.Time.Year, lastHydricBalance.Time.Month,
                    lastHydricBalance.Time.Day, lastHydricBalance.Time.Hour, 0, 0)
                , new DateTime(pobs.Time.Year, pobs.Time.Month, pobs.Time.Day, pobs.Time.Hour, 0, 0)) == 1
           )
        {
            _logger.LogDebug(
                "HYDRIC BALANCE: Si el ultimo hydric balance es de una hora posterior a la observación que acaba de llegar no entra como calculada. {obs}",
                obs);
            return;
        }

        var calculatedHydricBalance = new PatientObservation
        {
            Name = "Hydric_Balance_Calculated",
            CodingSystem = "ADAS",
            PatientId = obs.PatientId,
            Time = obs.Time.AddSeconds(1), //To discriminate updates.
            Value = pobs.Value,
            Units = obs.Units
        };


        if (lastHydricBalance != null && DateTime.Compare(
                new DateTime(lastHydricBalance.Time.Year, lastHydricBalance.Time.Month, lastHydricBalance.Time.Day,
                    lastHydricBalance.Time.Hour, 0, 0)
                , new DateTime(pobs.Time.Year, pobs.Time.Month, pobs.Time.Day, pobs.Time.Hour, 0, 0)) == 0)
            calculatedHydricBalance.Time = lastHydricBalance.Time.AddSeconds(1);

        await _observationService.Value.InsertObservation(calculatedHydricBalance);
    }

    /// <summary>
    /// Maps a <c>Resp_Mode</c> observation to a <c>Resp_Type</c> value: codes listed in
    /// <c>InvasiveVentilation</c> yield <c>Invasive</c>, otherwise the observation's value is matched
    /// against the configured high-frequency / non-invasive catalogues (yielding
    /// <c>HighFrequencyVentilation</c>, <c>NonInvasive</c>, or <c>Invasive</c> as fallback). Values
    /// equal to <c>"EnESPERA"</c> are skipped.
    /// </summary>
    /// <param name="obs">The base patient observation that triggered the calculation, expected to be a <see cref="PatientObservation"/> with <c>Name</c> <c>Resp_Mode</c>.</param>
    /// <returns>A task that represents the asynchronous insert operation.</returns>
    private async Task CalculateVentilationMode(BasePatientObservation obs)
    {
        _logger.LogDebug("CalculateVentilationMode {obs}", obs);
        var pobs = (PatientObservation)obs;

        var respTypeObs = new PatientObservation
        {
            Name = "Resp_Type",
            CodingSystem = "ADAS",
            PatientId = obs.PatientId,
            Time = obs.Time
        };
        //https://epigram.teamwork.com/#/tasks/34765750
        if (obs.Code != null && _codesForInvasiveVentilation.Contains(obs.Code))
        {
            //añadir que sea solo cuando el valor es invasiva
            respTypeObs.Value = nameof(RespirationType.Invasive);
            respTypeObs.Time = obs.Time.AddSeconds(1);
            await _observationService.Value.InsertIfChanged("Resp_Type", respTypeObs);
        }
        else
        {
            var strValue = pobs.Value.ToString();
            if (strValue == null)
                return;

            //si es en espera tendrá que insertar que es en espera 
            if (!"EnESPERA".Equals(strValue))
            {
                respTypeObs.Value = _highFrequencyVentilation.Contains(strValue)
                    ? nameof(RespirationType.HighFrequencyVentilation)
                    : _nonInvasiveVentilation.Contains(strValue)
                        ? respTypeObs.Value = nameof(RespirationType.NonInvasive)
                        : nameof(RespirationType.Invasive);

                await _observationService.Value.InsertIfChanged("Resp_Type", respTypeObs);
            }
        }
    }

    /// <summary>
    /// Computes the <c>Weight_Diff</c> observation (new weight − previous weight, rounded to two
    /// decimals) by comparing the supplied <c>Weight_Current</c> observation against the most
    /// recently stored one for the same patient. Skipped when either value cannot be parsed.
    /// </summary>
    /// <param name="obs">The base patient observation that triggered the calculation, expected to be a <see cref="PatientObservation"/> with <c>Name</c> <c>Weight_Current</c>.</param>
    /// <returns>A task that represents the asynchronous insert operation.</returns>
    private async Task CalculateWeight_DiffObservation(BasePatientObservation obs)
    {
        var toSearchList = new List<string>();

        toSearchList.AddRange(new List<string> { "Weight_Current" });

        var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, toSearchList);

        var pWeight = values.FirstOrDefault(o => o.Name == "Weight_Current");

        var newWeightObs = (PatientObservation)obs;

        if (pWeight?.Value != null && double.TryParse(pWeight.Value.ToString(), out var weight) &&
            double.TryParse(newWeightObs.Value.ToString(), out var newWeight))
        {
            var weightDiffValue = Math.Round(newWeight - weight, 2);

            var weightDiff = new PatientObservation
            {
                Name = "Weight_Diff",
                CodingSystem = "ADAS",
                PatientId = obs.PatientId,
                Time = obs.Time,
                Value = weightDiffValue,
                Units = obs.Units
            };

            await _observationService.Value.InsertObservation(weightDiff);
        }
    }

    /// <summary>
    /// Computes the <c>Diuresis_Weight</c> observation (diuresis / current weight in ml/kg) when
    /// either a <c>Diuresis</c> or a <c>Weight_Current</c> observation is received. When triggered
    /// by <c>Weight_Current</c>, the operation is skipped if the latest <c>Diuresis</c> observation
    /// has expired. Skipped silently when the weight is zero or any value is unparsable.
    /// </summary>
    /// <param name="obs">The base patient observation that triggered the calculation (<c>Diuresis</c> or <c>Weight_Current</c>).</param>
    /// <returns>A task that represents the asynchronous insert operation.</returns>
    private async Task CalculateDiureis_WeightObservation(BasePatientObservation obs)
    {
        var toSearchList = new List<string>();

        PatientObservation? diuresisObs;
        PatientObservation? weightObs;

        toSearchList.AddRange(new List<string> { "Diuresis", "Weight_Current" });

        var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, toSearchList);

        if (obs.Name == "Diuresis")
        {
            diuresisObs = (PatientObservation)obs;
            weightObs = values.FirstOrDefault(o => o.Name == "Weight_Current");
        }
        else
        {
            weightObs = (PatientObservation)obs;
            diuresisObs = values.FirstOrDefault(o => o.Name == "Diuresis");

            if (diuresisObs == null || CheckExpired(diuresisObs)) return;
        }

        if (weightObs?.Value != null)
            if (double.TryParse(weightObs.Value.ToString(), out var nWeight) && nWeight != 0 &&
                double.TryParse(diuresisObs.Value.ToString(), out var diuresis))
            {
                var diruesisWeightValue = Math.Round(diuresis / nWeight, 2);

                var diruesisWeight = new PatientObservation
                {
                    Name = "Diuresis_Weight",
                    CodingSystem = "ADAS",
                    PatientId = obs.PatientId,
                    Time = obs.Time,
                    Value = diruesisWeightValue,
                    Units = "ml/kg"
                };

                await _observationService.Value.InsertObservation(diruesisWeight);
            }
    }

    /// <summary>
    /// Aggregates a <c>AllergiesObs</c> observation into a single <c>Allergies</c> string per type,
    /// special-casing drug allergies ("FÁRMACOS") into a single grouped entry and setting the
    /// <c>Status</c> to <c>Alert</c> when drug allergies are present. <c>InvalidCastException</c>s
    /// are logged and swallowed.
    /// </summary>
    /// <param name="obs">The base patient observation that triggered the calculation, expected to be a <see cref="PatientObservation"/> with <c>Name</c> <c>AllergiesObs</c> and a collection-valued <c>Value</c> of <see cref="PatientAllergiesValue"/>.</param>
    /// <returns>A task that represents the asynchronous insert operation.</returns>
    private async Task CalculateAllergiesObservation(BasePatientObservation obs)
    {
        try
        {
            var pobs = (PatientObservation)obs;

            var allergiesObs = new PatientObservation
            {
                Name = "Allergies",
                CodingSystem = "ADAS",
                PatientId = obs.PatientId,
                Time = obs.Time,
                MessageTime = pobs.MessageTime
            };

            var patientAllergiesValues =
                new List<PatientAllergiesValue>((IEnumerable<PatientAllergiesValue>)pobs.Value);

            var allergiesValues = patientAllergiesValues.GroupBy(o => o.Type)
                .Select(x =>
                    new PatientAllergiesValue
                    {
                        Type = x.Key,
                        Value = string.Join(", ", x.Select(v => v.Value)),
                        Notes = string.Join(", ", x.Select(n => n.Notes))
                    }).ToList();


            var farmacosType = false;
            var farmacosValues = new List<string>();
            var values = new List<string>();

            foreach (var allergies in allergiesValues)
            {
                if (allergies.Value == null)
                    continue;

                var type = allergies.Type?.Replace("Alergia a ", "").Replace("Alergia ", "").ToUpper();

                if (type != null)
                    switch (type)
                    {
                        case "FÁRMACOS":
                            farmacosType = true;
                            farmacosValues.Add(allergies.Value.ToUpper());
                            break;

                        default:
                            values.Add(type);
                            break;
                    }
            }

            if (farmacosType)
            {
                values.Add($"FÁRMACOS ({string.Join(", ", farmacosValues)})");
                allergiesObs.Status = StatusEnum.Type.Alert;
            }

            allergiesObs.Value = string.Join(", ", values);

            await _observationService.Value.InsertObservation(allergiesObs);
        }
        catch (InvalidCastException)
        {
            _logger.LogError("CalculateAllergiesObservation {obs}", obs);
        }
    }

    /// <summary>
    /// For a <c>DrainagesObs</c> observation of type <c>"Drenaje ventricular"</c>, persists the
    /// derived <c>DVE</c> (volume) and <c>Drainage_Height</c> observations using the underlying
    /// <see cref="PatientDrainagesValue"/> properties.
    /// </summary>
    /// <param name="obs">The base patient observation that triggered the calculation, expected to be a <see cref="PatientObservation"/> with <c>Name</c> <c>DrainagesObs</c> and a <see cref="PatientDrainagesValue"/>-typed <c>Value</c>.</param>
    /// <returns>A task that represents the asynchronous insert operation.</returns>
    private async Task CalculateDrainagesObservation(BasePatientObservation obs)
    {
        var pobs = (PatientObservation)obs;

        var patientDraingesValue = (PatientDrainagesValue)pobs.Value;

        if (patientDraingesValue.Type == "Drenaje ventricular")
        {
            var dve = new PatientObservation
            {
                Name = "DVE",
                CodingSystem = "ADAS",
                PatientId = obs.PatientId,
                Time = obs.Time,
                Units = obs.Units
            };

            var drainageHeight = new PatientObservation
            {
                Name = "Drainage_Height",
                CodingSystem = "ADAS",
                PatientId = obs.PatientId,
                Time = obs.Time
            };

            if (patientDraingesValue.Volume != null)
            {
                dve.Value = patientDraingesValue.Volume;
                await _observationService.Value.InsertObservation(dve);
            }

            if (patientDraingesValue.Height != null)
            {
                drainageHeight.Value = patientDraingesValue.Height;
                await _observationService.Value.InsertObservation(drainageHeight);
            }
        }
    }

    /// <summary>
    /// Computes the <c>Delta_Pressure</c> observation (driving pressure = plateau − PEEP) when both
    /// <c>PEEP</c> and <c>Pleateu_Pressure</c> observations are available. The stored <c>Time</c> is
    /// the most recent of the two source observations, so the result is anchored to the latest input.
    /// </summary>
    /// <param name="obs">The base patient observation that triggered the calculation (<c>PEEP</c> or <c>Pleateu_Pressure</c>).</param>
    /// <returns>A task that represents the asynchronous insert operation.</returns>
    private async Task CalculateDelta_PressureObservation(BasePatientObservation obs)
    {
        var toSearchList = new List<string>();

        PatientObservation? peep;
        PatientObservation? pleateuPressure;

        toSearchList.AddRange(new List<string> { "PEEP", "Pleateu_Pressure" });

        var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, toSearchList);
        var time = obs.Time;
        double pleateuPressureValue = 0;

        if (obs.Name == "PEEP")
        {
            peep = (PatientObservation)obs;
            pleateuPressure = values.FirstOrDefault(o => o.Name == "Pleateu_Pressure");

            if (pleateuPressure?.Time != null && time.CompareTo(pleateuPressure.Time) > 0) time = pleateuPressure.Time;
        }
        else
        {
            pleateuPressure = (PatientObservation)obs;
            peep = values.FirstOrDefault(o => o.Name == "PEEP");

            if (peep?.Time != null && time.CompareTo(peep.Time) > 0) time = peep.Time;
        }

        if (peep?.Value != null && pleateuPressure?.Value != null)
        {
            var peepSuccess = double.TryParse(peep.Value.ToString(), out var peepValue);
            if (!peepSuccess) peepValue = 0;

            if (!peepSuccess) pleateuPressureValue = 0;

            var deltaPressure = Math.Round(pleateuPressureValue - peepValue, 2);


            var deltaPressureObs = new PatientObservation
            {
                Name = "Delta_Pressure",
                CodingSystem = "ADAS",
                PatientId = obs.PatientId,
                Time = time,
                Value = deltaPressure
            };

            await _observationService.Value.InsertObservation(deltaPressureObs);
        }
    }

    /// <summary>
    /// Persists a <c>Daily_Balance_Calculated</c> observation mirroring the supplied
    /// <c>Daily_Balance</c> value, but only when the local hour of the observation is 8 (the daily
    /// balance cut-off used by the HRYC customization).
    /// </summary>
    /// <param name="obs">The base patient observation that triggered the calculation, expected to be a <see cref="PatientObservation"/> with <c>Name</c> <c>Daily_Balance</c>.</param>
    /// <returns>A task that represents the asynchronous insert operation.</returns>
    private async Task CalculateDaily_BalanceObservation(BasePatientObservation obs)
    {
        if (obs.Time.ToLocalTime().Hour == 8)
        {
            var dailyBalanceCalculated = new PatientObservation
            {
                Name = "Daily_Balance_Calculated",
                CodingSystem = "ADAS",
                PatientId = obs.PatientId,
                Time = DateTime.Now,
                Units = obs.Units,
                Value = ((PatientObservation)obs).Value
            };

            await _observationService.Value.InsertObservation(dailyBalanceCalculated);
        }
    }

    /// <summary>
    /// Determines whether a patient observation has expired by comparing the current time against
    /// <c>Time + Expires</c> seconds, when <c>Expires</c> is set.
    /// </summary>
    /// <param name="obs">The patient observation to evaluate.</param>
    /// <returns>
    /// <see langword="true"/> when the observation has an <c>Expires</c> value and the current time
    /// is past the expiration instant; otherwise, <see langword="false"/>.
    /// </returns>
    private static bool CheckExpired(PatientObservation obs)
    {
        if (obs.Expires == null) return false;

        var timeExpire = obs.Time.ToLocalTime().AddSeconds(Convert.ToDouble(obs.Expires));

        return DateTime.Now.CompareTo(timeExpire) > 0;
    }
}