using System.Collections;
using adas_core.Application.Services.Interfaces;
using adas_core.Domain.Enums;
using adas_core.Domain.Models;
using adas_core.Domain.Models.GroupedObservations;
using adas_core.Domain.Models.MongoModels;
using adas_core.Domain.Utils;

namespace adas_core.Application.Services;

public class ObservationDemoService(
    IConfigObservationService configObservationService,
    Lazy<IAlarmService> alarmService)
    : IObservationDemoService
{
    /// <summary>
    /// Generates a list of patient observation alarms based on the provided alarm fields, randomly skipping fields and skipping those with no matching configuration.
    /// </summary>
    /// <param name="patient">The patient to associate with the generated alarms.</param>
    /// <param name="dataAlarmfields">The list of fields used to look up alarm configurations and produce alarms.</param>
    /// <returns>A task that represents the asynchronous operation, containing the list of mapped <see cref="PatientObservationAlarm"/> instances that were successfully resolved.</returns>
    public async Task<List<PatientObservationAlarm>> GenerateAlarmByField(Patient patient, List<Field> dataAlarmfields)
    {
        var listToReturn = new List<PatientObservationAlarm>();
        foreach (var field in dataAlarmfields)
        {
            var sendingRandom = new Random();

            if (sendingRandom.Next(0, 2) == 0 || string.IsNullOrEmpty(field.Name))
                continue;

            var firstCof = await configObservationService.GetConfigObservationItemsByName(field.Name);
            var conf = firstCof.FirstOrDefault();
            if (conf == null)
                continue;

            var alarmObs = new PatientObservationAlarm
            {
                Patient = patient,
                PatientId = patient.Id,
                AlarmConfig = conf.Alarm,
                InactivationState = new InactivationState
                {
                    Audio = AlarmEnum.AudioVideoState.Enabled,
                    Acknowledge = true,
                    Visual = AlarmEnum.AudioVideoState.Enabled
                },
                EventPhase = AlarmEnum.EventPhase.Continue,
                Type = AlarmEnum.ObservationAlarmType.Sp,
                Expires = conf.Expires,
                State = AlarmEnum.ObservationAlarmState.Active,
                Time = DateTime.UtcNow,
                Persist = false,
                MessageTime = DateTime.UtcNow,
                Code = conf.Code,
                PriorityLevel = conf.DemoConfig?.PriorityAlarm,
                Name = conf.Name,
                ParentData = new ParentDataClass
                {
                    Code = conf.ParentCode,
                    Name = conf.ParentName,
                    CodingSystem = conf.CodingSystem
                },
                Units = conf.Units,
                Value = GenerateValueRandom(conf, null) ?? 0
            };
            var obs = await alarmService.Value.MapObservationsByName(alarmObs);


            if (obs != null)
                listToReturn.Add(obs);
        }

        return listToReturn;
    }

    /// <summary>
    /// Generates a list of patient observations for the specified patient based on the provided data fields, using the corresponding configuration items to populate observation values, thresholds, and metadata. Fields without a name or without a matching configuration are skipped. When a configuration includes demo settings, the resulting observation is adjusted with a randomized initial date and, if required, an end time.
    /// </summary>
    /// <param name="patient">The patient to associate the generated observations with.</param>
    /// <param name="dataFields">The list of fields used to look up observation configurations and drive observation generation.</param>
    /// <returns>A task that returns the list of generated <see cref="PatientObservation"/> instances; entries are omitted when no configuration is found for a field.</returns>
    public async Task<List<PatientObservation>> GenerateObservationByField(Patient patient, List<Field> dataFields)
    {
        var listToReturn = new List<PatientObservation>();
        var rnd = new Random();
        foreach (var field in dataFields)
        {
            if (string.IsNullOrEmpty(field.Name))
                continue;

            var conf = await configObservationService.GetConfigObservationItemsByName(field.Name);
            var firstCof = conf.FirstOrDefault();
            if (firstCof == null)
                continue;

            var patientObs = new PatientObservation
            {
                Patient = patient,
                PatientId = patient.Id,
                Max = firstCof.MaxAlert,
                Min = firstCof.MinAlert,
                MaxWarn = firstCof.MaxWarn,
                MinWarn = firstCof.MinWarn,
                ShowOnExpired = firstCof.ShowOnExpired,
                Expires = firstCof.Expires,
                UiConfiguration = firstCof.UiConfiguration,
                MessageTime = DateTime.UtcNow,
                Code = firstCof.Code,
                ColorOnExpired = firstCof.ColorOnExpired,
                AlertColor = firstCof.AlertColor,
                WarnColor = firstCof.WarnColor,
                CodingSystem = firstCof.CodingSystem,
                Alarm = firstCof.Alarm,
                InsertMode = firstCof.InsertMode,
                Name = field.Name,
                Time = DateTime.UtcNow,
                ParentData = new ParentDataClass
                {
                    Code = firstCof.ParentCode,
                    Name = firstCof.ParentName,
                    CodingSystem = firstCof.CodingSystem
                },
                Units = firstCof.Units,
                Value = GenerateValueRandom(firstCof, null) ?? 0
            };
            var result = await configObservationService.Map(patientObs, true);
            if (firstCof.DemoConfig?.RequireInitDate == true && result != null)
            {
                var rndRestNmbHours = rnd.Next(3, 23);
                var initDate = DateTime.UtcNow.AddHours(-rndRestNmbHours);

                // Si se requiere fecha de fin, devolvemos un objeto con ambas
                if (firstCof.DemoConfig?.RequireEndDate == true)
                {
                    result.Time = initDate;
                    result.EndTime = initDate.AddHours(1);
                }

                // Si no, solo devolvemos la fecha inicial
                result.Time = initDate;
            }

            if (result != null)
                listToReturn.Add(result);
        }

        return listToReturn;
    }

    /// <summary>
    /// Generates a <see cref="GroupedObservation"/> for the given patient and grouped field, producing time-spaced observations for each configured name and result type based on the field's regularity.
    /// </summary>
    /// <param name="patient">The patient to associate the generated grouped observation with.</param>
    /// <param name="groupedField">The grouped field configuration that defines the names, results, regularity, and maximum number of observations to generate.</param>
    /// <returns>A task that returns the populated <see cref="GroupedObservation"/> containing the generated observations.</returns>
    public async Task<GroupedObservation> GenerateGroupedObservation(Patient patient, GroupedField groupedField)
    {
        if (groupedField.Names.IsNullOrEmpty() && groupedField.Name != null) groupedField.Names.Add(groupedField.Name);
        var rnd = new Random();
        var startTime = DateTime.UtcNow;
        var go = new GroupedObservation
        {
            Name = groupedField.Name,
            PatientId = patient.Id,
            Group = groupedField.Group,
            Observations = []
        };
        foreach (var groupedFieldName in groupedField.Names)
        {
            var conf = await configObservationService.GetConfigObservationItemsByName(groupedFieldName);
            var firstCof = conf.FirstOrDefault();
            if (firstCof == null)
                continue;

            foreach (var result in groupedField.Result)
            {
                object? prevValue = null;
                for (var i = 0; i <= groupedField.Max; i++)
                {
                    var time = startTime;
                    time = groupedField.Regularity switch
                    {
                        GroupedObservationEnum.Regularity.Day => time.AddDays(-i),
                        GroupedObservationEnum.Regularity.Minute => time.AddMinutes(-i),
                        GroupedObservationEnum.Regularity.Second => time.AddSeconds(-i),
                        _ => time.AddHours(-i)
                    };
                    var val = new GroupedObservation.GroupedObservationObs
                    {
                        Name = groupedFieldName,
                        MaxAlert = firstCof?.MaxAlert,
                        MinAlert = firstCof?.MinAlert,
                        Time = time,
                        IsFilled = false
                    };
                    var value = new GroupedObservation.GroupedObservationObsValue(
                        GenerateValueRandom(firstCof, prevValue) ?? rnd.Next(0, 30), time);
                    prevValue = value.Value;
                    var propertyInfo = val.GetType().GetProperty(result.ToString());

                    if (propertyInfo != null && propertyInfo.CanWrite) propertyInfo.SetValue(val, value);
                    var valueType = await configObservationService.GroupedObservationStatus(
                        groupedField,
                        result,
                        groupedFieldName,
                        val,
                        firstCof?.MaxAlert,
                        firstCof?.MinAlert
                    );
                    value.Type = valueType;
                    go.Observations.Add(
                        val
                    );
                }

                if (firstCof?.DemoConfig?.RequireInitDate.HasValue == true && firstCof.DemoConfig.RequireInitDate.Value)
                    break;
            }
        }

        return go;
    }

    /// <summary>
    /// Generates a random value (or list of values) for a given configuration, falling back to "-" when no configuration is available. When the configuration specifies an array, produces multiple random values using the configured count; otherwise returns a single random value.
    /// </summary>
    /// <param name="firstCof">The configuration observation whose <c>DemoConfig</c> drives the value generation. If null or its <c>DemoConfig</c> is null, the method returns "-".</param>
    /// <param name="prevValue">The previous value, passed through to <c>PickSingleValue</c> as context for the new random value generation.</param>
    /// <returns>A list of generated values when the configuration indicates an array, or a single generated value otherwise. Null is returned when no value could be picked.</returns>
    private static object? GenerateValueRandom(ConfigObservation? firstCof, object? prevValue)
    {
        if (firstCof?.DemoConfig == null)
            return "-";

        // 1. Determinar cuántos valores necesitamos
        var count = firstCof.DemoConfig?.ValueIsArray == true ? firstCof.DemoConfig?.RandomCount ?? 1 : 1;
        var results = new List<object>();

        for (var i = 0; i < count; i++)
        {
            var singleValue = PickSingleValue(firstCof.DemoConfig, prevValue);
            if (singleValue != null)
                results.Add(singleValue);
        }

        // 2. Si no es un array, devolvemos solo el primer elemento
        return firstCof.DemoConfig?.ValueIsArray == true ? results : results.FirstOrDefault();
    }

    /// <summary>
    /// Picks a single value from a <see cref="DemoConfig"/> following a defined priority: first selects randomly from a predefined list of options when <c>SetInValue</c> is enabled, otherwise generates a random numeric value within the configured range, optionally applying a percentage-based variation to a previous value. Returns <c>null</c> if the configuration is null, if the options list is empty, or if random value generation is disabled.
    /// </summary>
    /// <param name="config">The configuration that defines how the value should be selected, including the list of options, the numeric range, and the selection mode.</param>
    /// <param name="prevValue">The previously selected value, used as the base for percentage-based variation when generating a new numeric value; ignored when no previous integer value is available.</param>
    /// <returns>A randomly selected value from the configured options or a generated numeric value clamped within the defined range; <c>null</c> when the configuration does not allow a value to be produced.</returns>
    private static object? PickSingleValue(DemoConfig? config, object? prevValue)
    {
        if (config == null) return null;

        var rnd = new Random();
        // Prioridad 1: Selección de una lista de opciones (SetInValue)
        if (config is { SetInValue: true, ValueOption: not null })
        {
            if (config.ValueOption is List<object> { Count: > 0 } options)
            {
                var index = rnd.Next(options.Count);
                return options.ElementAt(index);
            }

            // Caso especial si ValueOption llega como un JArray o lista genérica
            if (config.ValueOption is IEnumerable list)
            {
                var tempList = list.Cast<object>().ToList();
                return tempList[rnd.Next(tempList.Count)];
            }
        }

        // Prioridad 2: Generación numérica por rango (SetRandomValue)
        if (config.SetRandomValue == false)
            return null;

        // 1. Extraemos los límites primero para usarlos en cualquier caso
        var minLimit = config.MinValue ?? 0;
        var maxLimit = config.MaxValue ?? 100;

        if (prevValue is not int values)
            // Caso base: Si no hay valor previo, generamos uno aleatorio dentro del rango permitido
            return rnd.Next(minLimit, maxLimit + 1);

        if (values == 0) values = 1;
        // 2. Calculamos la variación (entre 5% y 10%)
        var porcentaje = rnd.Next(5, 10) / 100.0;
        var direccion = rnd.Next(0, 2) == 0 ? -1 : 1;

        // 3. Calculamos el valor base redondeado
        var calculado = (int)Math.Round(values + values * porcentaje * direccion);

        // 4. Forzamos que esté dentro del rango [minLimit, maxLimit]
        return Math.Clamp(calculado, minLimit, maxLimit);
    }
}