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

namespace adas_core.Application.Customizations.HUVH.UCIN;

public class CalculatedObservations : ICalculatedObservations
{
    private const string Spo2PreObservationName = "SpO2";
    private const string Spo2PostObservationName = "SpO2_Post";

    private const string Spo2PrePostObservationName = "SpO2_Pre_Post";
    private const string CodingSystem = "ADAS";

    private readonly List<string> _complexityObservations =
    [
        "Respiratory_Device_Multivalue", "Intravenous_Routes_Multivalue", "Medication_Multivalue",
        "System_Multivalue", "Newborn_Weight", "Surgery_Multivalue"
    ];

    private readonly ILogger<CalculatedObservations> _logger;
    private readonly IMappingUtils _mappingUtils;
    private readonly Lazy<IMedicineService> _medicineService;
    private readonly Lazy<IObservationService> _observationService;

    private readonly Lazy<ITreatmentService> _treatmentService;


    public CalculatedObservations(IServiceProvider serviceProvider)
    {
        _treatmentService = serviceProvider.GetRequiredService<Lazy<ITreatmentService>>();
        _medicineService = serviceProvider.GetRequiredService<Lazy<IMedicineService>>();
        _observationService = serviceProvider.GetRequiredService<Lazy<IObservationService>>();
        _logger = serviceProvider.GetRequiredService<ILogger<CalculatedObservations>>();

        var apiSettings = serviceProvider.GetRequiredService<IOptions<ApiSettings>>(); //apiSettings;
        _mappingUtils = new MappingUtils(apiSettings);
    }


    public Task CalculateActiveBolus(ObjectId patientId)
    {
        throw new NotImplementedException();
    }


    public async Task<T?> Map<T>(T obs, bool onlyByName = false) where T : BasePatientObservation
    {
        if (obs.Name is "Intervention_In" or "Intervention_Out") await CalculateInterventionMultiValueObservation(obs);
        if (obs.Name is "Surgery") await CalculateMultiValueObservation(obs);
        if (obs.Name != null && _complexityObservations.Contains(obs.Name))
        {
            _logger.LogDebug("Mapping observation Complexity {obs}", obs);
            //TODO ver si hay un máximo de puntuación por grupo de observaciones
            _ = CalculateComplexity(obs);
        }

        if (obs.Name is Spo2PreObservationName or Spo2PostObservationName)
        {
            _logger.LogDebug("Pre-post saturation observation {obs}", obs);
            _ = CalculateSaturation_DiffObservation(obs);
        }

        if (obs.Name is "ALPS" or "NPASS_Sedation" or "NPASS_Analgesia") _ = CalculatePainScale(obs);


        if (obs.Name is "Last_Defecation") obs = (T)CalculateLastDefecationValue(obs);

        return obs;
    }


    public async Task<PatientTreatment> Map(PatientTreatment treatment)
    {
        var order = treatment.PlacerOrder?.EntityIdentifier; //aquí almacenamos el número de orden
        if (string.IsNullOrEmpty(order)) return treatment;

        //comprobamos el estado de los tratamientos anteriores
        var oldTreatments = await CheckExpiredPatientTreatments(treatment);

        treatment.OrderControl = oldTreatments.Any() ? OrderControlType.Xo : OrderControlType.Nw;

        if (treatment.EndTime != null)
            //el tratamiento ha expirado
            treatment.OrderControl = OrderControlType.Dc;

        await CheckTreatmentMedicines(treatment);

        return treatment;
    }

    public async Task<PumpObservation> Map(PumpObservation pumpObservation)
    {
        return await Task.FromResult(pumpObservation);

    }

    public async Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
    {
        var medicineObs = new PatientObservation
        {
            PatientId = patientId,
            Name = "Medication_Multivalue",
            CodingSystem = CodingSystem,
            Value = activeMedicines.Select(m => m.Name ?? string.Empty).Distinct().ToArray(),
            Time = DateTime.Now
        };

        var lastMedicationsObs =
            await _observationService.Value.FindLastObservations(patientId, 1, ["Medication_Multivalue"]);
        var lastMedicationObs = lastMedicationsObs.FirstOrDefault();
        if (lastMedicationObs != null)
        {
            //expiramos la anterior
            lastMedicationObs.Expired = true;
            _ = _observationService.Value.UpdateObservation(lastMedicationObs);
        }

        await _observationService.Value
            .InsertObservation(
                medicineObs); //mapObs:volvemos a mapear la observación para que pase por el cálculo de la complejidad
    }

    public async Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
    {
        var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(id);
        return activeTreatments;
    }


    public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
    {
        throw new NotImplementedException();
    }

   
    public async Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
    {
        if (string.IsNullOrEmpty(newObservation.Name))
        {
            _logger.LogError(
                "Error FixTimeInconsistencyWithLast. Observation name is 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;
    }

    public Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToInsert)
    {
        return Task.FromResult(listToInsert);
    }

    public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
    {
        return Task.FromResult(obs);
    }

    public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
    {
        throw new NotImplementedException();
    }

    private async Task CalculatePainScale(BasePatientObservation obs)
    {
        if (obs is not PatientObservation pobs) return;


        object valueObs;

        switch (pobs.Name)
        {
            case "ALPS":
                valueObs = pobs.Value;
                break;
            case "NPASS_Sedation":
            {
                //Buscamos la complementaria "NPASS Analgesia"
                var analgesiaObs =
                    await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["NPASS_Analgesia"]);
                var analgesiaValue = analgesiaObs.FirstOrDefault()?.Value.ToString() ?? "0";

                valueObs = $"{pobs.Value}/{analgesiaValue}";
            }
                break;
            case "NPASS_Analgesia":
            {
                //Buscamos la complementaria "NPASS Sedation"
                var sedationObs =
                    await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["NPASS_Sedation"]);
                var sedationValue = sedationObs.FirstOrDefault()?.Value.ToString() ?? "0";

                valueObs = $"{sedationValue}/{pobs.Value}";
            }
                break;
            default:
                return;
        }

        var painScaleObs = new PatientObservation
        {
            PatientId = obs.PatientId,
            Time = DateTime.UtcNow,
            CodingSystem = "ADAS",
            Name = "Pain_Scale",
            Value = valueObs
        };

        _ = _observationService.Value.InsertObservation(painScaleObs, mapObs: false);
    }

    private static BasePatientObservation CalculateLastDefecationValue(BasePatientObservation obs)
    {
        if (obs is not PatientObservation pobs) return obs;
        //Assign obs time to value
        //Convert the UTC DateTime to local time.
        var localTime = pobs.Time.ToLocalTime();

        //Format the local time into a short date/time string.
        pobs.Value = localTime.ToString("dd/MM/yyyy HH:mm");

        return pobs;
    }


    private async Task CalculateMultiValueObservation(BasePatientObservation obs)
    {
        //El valor de la observación es un array de strings
        if (obs is not PatientObservation pobs || string.IsNullOrEmpty(obs.Name)) return;

        var newObsName = $"{obs.Name}_Multivalue";
        var actualObsInBd =
            await _observationService.Value.FindLastObservations(obs.PatientId, 1, [newObsName]);


        var newValue = new List<string>();

        if (pobs.Value is not string stringValueName) return;


        if (actualObsInBd.Count > 0)
            if (actualObsInBd.First().Value is string[] oldValue)
            {
                oldValue = oldValue.Where(x => x != stringValueName).ToArray();
                newValue.AddRange(oldValue);
            }

        newValue.Add(stringValueName);

        if (newValue.Count == 0) return;

        //Expiramos la anterior
        var actualObs = actualObsInBd.FirstOrDefault();
        if (actualObs != null)
        {
            actualObs.Expired = true;

            await _observationService.Value.UpdateObservation(actualObs);
        }

        var newObservation = new PatientObservation
        {
            PatientId = obs.PatientId,
            Time = DateTime.UtcNow,
            CodingSystem = CodingSystem,
            Name = newObsName,
            Value = newValue.ToArray()
        };

        _logger.LogDebug(
            "Calculate {name}_Multivalue {outpatient} Insert Calculated {time} value {value}",
            newObsName, newObservation.PatientId, newObservation.Time,
            newObservation.Value.ToJson());

        await _observationService.Value.InsertObservation(newObservation);
    }

    private async Task CalculateInterventionMultiValueObservation(BasePatientObservation obs)
    {
        if (obs is not PatientObservation pobs || string.IsNullOrEmpty(obs.Name) ||
            pobs.Value is not string valueObs) return;

        if (!GetInterventionValue(valueObs, out var r) || r == null) return;

        var obsType = r.Value.type; //"Intravenous_Routes";
        var obsGroup = r.Value.Name; // "Vía arterial";
        var newObsName = $"{obsType}_Multivalue";

        // Obtener la última observación multivalue
        var result = await _observationService.Value.FindLastObservations(obs.PatientId, 1, [newObsName]);
        var actualMultiValueObsInDb = result.FirstOrDefault(o => !o.Expired);

        var oldValue = actualMultiValueObsInDb?.Value as string[] ?? [];
        var newValue = new List<string>(oldValue);

        if (obs.Name.Contains("_In"))
        {
            //Por seguridad solo tomamos el último valor que nos llega
            //Puede ser que no se acuerden de cancelar el anterior antes de añadir un nuevo dispositivo y no puede haber dos dispositivos del mismo tipo activos al mismo tiempo
            if (obsType == "Respiratory_Device")
                newValue.Clear();

            // Inserción dispositivo
            if (!oldValue.Contains(obsGroup))
                newValue.Add(obsGroup);
        }
        else if (obs.Name.Contains("_Out"))
        {
            // Retirada del dispositivo
            //Buscamos la inserción
            var name = $"{obs.Name.Split('_')[0]}_In";
            var inObs = await _observationService.Value.FindLastNotExpiredObservatonsByPatient(obs.PatientId, name);

            var sameValueObs = inObs.Where(o => o.Value == pobs.Value).ToList();

            if (sameValueObs.Any())
                // Si hay más de una observación del mismo tipo, no eliminar del multivalue
                if (sameValueObs.Count > 1)
                    return;

            // Actualizar valor eliminando el grupo
            newValue.Remove(obsGroup);
        }
        else
        {
            return;
        }

        //Expiramos la anterior
        if (actualMultiValueObsInDb is { Expired: false })
        {
            actualMultiValueObsInDb.Expired = true;
            await _observationService.Value.UpdateObservation(actualMultiValueObsInDb);
        }

        // Si no hay valores nuevos, no se crea una nueva observación
        if (!newValue.Any())
            return;


        // Crear y registrar nueva observación
        var newObservation = new PatientObservation
        {
            PatientId = obs.PatientId,
            Time = DateTime.UtcNow,
            CodingSystem = CodingSystem,
            Name = newObsName,
            Value = newValue.ToArray()
        };

        _logger.LogDebug(
            "Calculate {name}_Multivalue {patientid} Insert Calculated {time} value {value}",
            newObsName, newObservation.PatientId, newObservation.Time,
            newObservation.Value.ToJson()
        );

        await _observationService.Value.InsertObservation(newObservation);
    }

    private bool GetInterventionValue(string valueObs, out (string type, string Name, string Group)? r)
    {
        var code = valueObs.Split(" ")[0]; //
        if (code.EndsWith(".")) // Verifica si termina con un punto
            code = code.Substring(0, code.Length - 1).Replace('.', ','); // Remueve el último carácter

        if (!double.TryParse(code, out var codeParsed))
        {
            r = null;
            return false;
        }


        r = _mappingUtils.SearchByCode(codeParsed, "Intervention"); //TODO validar este parámetro con Lola

        return r != null;
    }


    /**
     * Calculamos la complejidad basándonos en el valor de Respiratorio/Vías/Medicación/Sistemas/Peso nacimiento/cirugía
     * cuando entra una nueva observación que afecte a complejidad, se recalcula de 0 para no tener que estar pendiente de qué puntos corresponden a qué observación
     * Valor máximo 46 Guardamos la observación que nos llega y recalculamos complejidad.
     */
    private async Task CalculateComplexity(BasePatientObservation obs)
    {
        var pobs = obs as PatientObservation;
        if (pobs == null) return;

        var complexity = new PatientObservation
        {
            PatientId = obs.PatientId,
            Time = DateTime.UtcNow,
            CodingSystem = CodingSystem,
            Name = "Complexity",
            Value = 0
        };

        var complexityValue = 0;

        // Diccionario para manejar las operaciones basadas en el nombre de la observación
        var operations = new Dictionary<string, Func<ObjectId, PatientObservation?, Task<int>>>
        {
            {
                "Respiratory_Device_Multivalue",
                (patientId, observation) =>
                    CalculateMultivalueObservationComplexityValue(patientId, "Respiratory_Device_Multivalue",
                        observation)
            },
            {
                "Intravenous_Routes_Multivalue",
                (patientId, observation) =>
                    CalculateMultivalueObservationComplexityValue(patientId, "Intravenous_Routes_Multivalue",
                        observation)
            },
            {
                "Medication_Multivalue",
                (patientId, observation) =>
                    CalculateMultivalueObservationComplexityValue(patientId, "Medication_Multivalue", observation)
            },
            {
                "System_Multivalue",
                (patientId, observation) =>
                    CalculateMultivalueObservationComplexityValue(patientId, "System_Multivalue", observation)
            },
            { "Newborn_Weight", CalculateWeightNewBornValue },
            {
                "Surgery_Multivalue",
                (patientId, observation) =>
                    CalculateMultivalueObservationComplexityValue(patientId, "Surgery_Multivalue", observation)
            }
        };

        // Realizar cálculos iterando sobre las operaciones
        foreach (var operation in operations)
            complexityValue += await operation.Value(pobs.PatientId, operation.Key.Equals(pobs.Name) ? pobs : null);

        complexity.Value = Math.Min(complexityValue, 46); // Limitar el valor máximo a 46

        _logger.LogDebug(
            "complexity for patient {patientid} => inserting complexity value: {complexityValue} for patient: {complexityPatientid} at time {fixedTimeComplexityTime}",
            complexity.PatientId, complexity.Value, complexity.PatientId, complexity.Time);

        await _observationService.Value.InsertObservation(complexity, mapObs: false);
    }

    private async Task<int> CalculateMultivalueObservationComplexityValue(ObjectId patientId, string observationName,
        PatientObservation? observation = null)
    {
        var result =
            await _observationService.Value.FindLastObservations(patientId, 1, [observationName]);
        var actualObsInBd = result.FirstOrDefault();

        var valueObj = observation == null ? actualObsInBd?.Value : observation.Value;
        var valueList = valueObj as string[];

        var valueToReturn = valueList?.Sum(v => _mappingUtils.GetComplexityValue(v)) ?? 0;

        _logger.LogDebug(
            "complexity for patient {patientid} => observation: {observationName}  plus complexity: {Value}", patientId,
            observationName, valueToReturn);

        return valueToReturn;
    }


    private async Task<int> CalculateWeightNewBornValue(ObjectId patientId, PatientObservation? pobs = null)
    {
        var result =
            await _observationService.Value.FindLastObservations(patientId, 1, ["Newborn_Weight"]);
        var actualObsInBd = result.FirstOrDefault();

        string? strValue;
        string? obsName;
        if (pobs == null)
        {
            if (actualObsInBd?.Value == null) return 0;

            strValue = actualObsInBd.Value.ToString();
            obsName = actualObsInBd.Name;
        }
        else
        {
            if (pobs.Units == "kg") pobs = ParseWeight(pobs);

            strValue = pobs.Value.ToString();
            obsName = pobs.Name;
        }

        if (!double.TryParse(strValue, out var valueParsed) || obsName == null)
            return 0;

        var weightNewBornValue = _mappingUtils.GetComplexityValue(obsName, valueParsed);


        _logger.LogDebug(
            "complexity for patient {patientid} =>  Weight_Newborn plus complexity: {Weight_NewbornValue}",
            patientId, weightNewBornValue);

        return weightNewBornValue;
    }

    private PatientObservation ParseWeight(PatientObservation obs)
    {
        if (!double.TryParse(obs.Value.ToString(), out var dValue))
        {
            _logger.LogError("Error casting weight Observation {obs}:", obs);
            return obs;
        }

        obs.Units = "gr";
        obs.Value = dValue * 1000;
        return obs;
    }


    private async Task CalculateSaturation_DiffObservation(BasePatientObservation obs)
    {
        var toSearchList = new List<string>();
        PatientObservation? pre;
        PatientObservation? post;

        toSearchList.AddRange(new List<string> { Spo2PreObservationName, Spo2PostObservationName });

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

        if (obs.Name == Spo2PreObservationName)
        {
            pre = (PatientObservation)obs;
            post = values.FirstOrDefault(o => o.Name == Spo2PostObservationName);

            if (post?.Time != null && time.CompareTo(post.Time) > 0) time = post.Time;
        }
        else
        {
            post = (PatientObservation)obs;
            pre = values.FirstOrDefault(o => o.Name == Spo2PreObservationName);

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

        if (pre?.Value != null && post?.Value != null)
        {
            var preSuccess = double.TryParse(pre.Value.ToString(), out var preValue);
            if (!preSuccess) preValue = 0;
            var postSuccess = double.TryParse(post.Value.ToString(), out var postValue);
            if (!postSuccess) postValue = 0;

            var saturationDiff = Math.Round(preValue - postValue, 2);

            var saturationDiffObs = new PatientObservation
            {
                Name = "SpO2_Diff",
                CodingSystem = CodingSystem,
                PatientId = obs.PatientId,
                Time = time,
                Value = saturationDiff
            };
            var saturationPrePost = new PatientObservation
            {
                Name = Spo2PrePostObservationName,
                CodingSystem = CodingSystem,
                PatientId = obs.PatientId,
                Time = time,
                Value = pre.Value + "/" + post.Value
            };

            await _observationService.Value.InsertObservation(saturationDiffObs);
            await _observationService.Value.InsertObservation(saturationPrePost);
        }
    }

    private async Task<List<PatientTreatment>> CheckExpiredPatientTreatments(PatientTreatment treatment)
    {
        //si el tratamiento ha expirado actualizamos el OrderControl a DC y devolvemos las que siguen activas
        var result = new List<PatientTreatment>();
        var oldTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(treatment.PatientId);

        foreach (var t in oldTreatments)
        {
            if (t == null) continue;

            if (t.PlacerOrder?.EntityIdentifier == treatment.PlacerOrder?.EntityIdentifier)
            {
                //Es el mismo tratamiento actualizamos

                if (t.EndTime != null && t.EndTime > DateTime.UtcNow) treatment.OrderControl = OrderControlType.Dc;

                treatment.Id = t.Id; //Asignamos el ID del tratamiento existente para actualizarlo
                _ = _treatmentService.Value.UpdateTreatment(treatment);
            }
            else
            {
                result.Add(t);
            }
        }


        return result;
    }

    private async Task CheckTreatmentMedicines(PatientTreatment treatment)
    {
        var newMedicines = new List<Medicine>();
        var hasVasoactives = false;
        foreach (var code in treatment.RequestedGiveCodes)
        {
            var m = await _medicineService.Value.GetByCode(code.Identifier);
            if (m == null) continue;

            //Si el código es de tipo nutrición entonces creamos una observación de ese tipo que tendrá el valor mapeado con el nombre correspondiente
            //CreateNutritionObservation y NO agregamos a medicamentos 
            var r = _mappingUtils.SearchByCode(m.Codes.First(), "Treatment");
            if (r is { type: "Nutrition" })
            {
                await CreateNutritionObservation(r.Value.group, treatment.PatientId);
                continue;
            }

            if (r is { type: "Medication", group: "Drogas vasoactivas" })
                hasVasoactives = true;

            newMedicines.Add(m);
        }

        if (hasVasoactives)
        {
            var vObs = new PatientObservation
            {
                Name = "HasVasoactive",
                Value = true,
                Time = DateTime.UtcNow,
                CodingSystem = CodingSystem
            };

            await _observationService.Value.InsertObservation(vObs, mapObs: false);
        }

        if (newMedicines.Count == 0)
            return;

        var _ = await GetActiveTreatmentsByPatient(treatment.PatientId);

        var activeTreatments = _.ToList();

        var __ = await _medicineService.Value.GetMedicinesOfTreatments(activeTreatments); //Todo revisar
        var activeMedicines = __.ToList();

        // Combina ambas listas y selecciona solo elementos únicos con el mismo nombre
        var uniqueMedicines = newMedicines
            .Concat(activeMedicines)
            .GroupBy(m => m.Name)
            .Select(g => g.First())
            .ToList();

        await CalculateMedicineObservation(uniqueMedicines, treatment.PatientId);
    }

    private async Task CreateNutritionObservation(string value, ObjectId patientId)
    {
        var nutritionObs = new PatientObservation
        {
            PatientId = patientId,
            Name = "Feeding_Type",
            CodingSystem = CodingSystem,
            Value = value,
            Time = DateTime.Now
        };

        if (value.ToLower().Contains("parenteral"))
        {
            nutritionObs.Name = "Parenteral";
            nutritionObs.Value = "SI";
        }

        var lastNutritionObsList =
            await _observationService.Value.FindLastObservations(patientId, 1, [nutritionObs.Name]);
        var lastNutritionObs = lastNutritionObsList.FirstOrDefault();
        if (lastNutritionObs is { Expired: false })
        {
            //expiramos la anterior
            lastNutritionObs.Expired = true;
            _ = _observationService.Value.UpdateObservation(lastNutritionObs);
        }

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