using adas_core.Application.Repositories.Interfaces;
using adas_core.Application.Services.Interfaces;
using adas_core.Domain.Enums;
using adas_core.Domain.Models;
using adas_core.Domain.Models.DTO;
using adas_core.Domain.Models.MongoModels;
using Microsoft.Extensions.Logging;
using MongoDB.Bson;

namespace adas_core.Application.Services;

/// <summary>
/// Provides the concrete implementation of the <see cref="IDeviceService"/> contract,
/// handling device-related service operations defined by the interface.
/// </summary>
public class DeviceService : IDeviceService
{
    private readonly IDeviceRepository _deviceRepository;
    private readonly IObservationService _observationService;
    private readonly IAlarmService _alarmService;
    private readonly IConfigObservationService _configObservationService;
    private readonly IPointOfCareService _pointOfCareService;
    private readonly ILogger<DeviceService> _logger;
    
    public DeviceService(
        IDeviceRepository deviceRepository,
        IPointOfCareService pointOfCareService,
        ILogger<DeviceService> logger,
        IObservationService observationService,
        IConfigObservationService configObservationService,
        IAlarmService alarmService)
    {
        _deviceRepository = deviceRepository;
        _pointOfCareService = pointOfCareService;
        _logger = logger;
        _observationService = observationService;
        _configObservationService = configObservationService;
        _alarmService = alarmService;
    }

    /// <summary>
    /// Converts a <see cref="DeviceDto"/> into a <see cref="Device"/> entity by mapping its properties. Applies a fallback to an empty list when <c>PointOfCareIds</c> is null, and to a new <see cref="DeviceSettings"/> instance when <c>Settings</c> is null.
    /// </summary>
    /// <param name="dto">The data transfer object containing the device information to convert.</param>
    /// <returns>A new <see cref="Device"/> entity populated with the values from the supplied DTO.</returns>
    public Device ToEntity(DeviceDto dto)
        {
            return new Device()
            {
                DeviceType = dto.DeviceType,
                MacAddr = dto.MacAddr,
                SerialNumber = dto.SerialNumber,
                Name = dto.Name,
                Battery = dto.Battery,
                Color = dto.Color,
                Connected = dto.Connected,
                Ready = dto.Ready,
                Uuid = dto.Uuid,
                Key = dto.Key,
                CreatedAt =  dto.CreatedAt,
                UpdatedAt = dto.UpdatedAt,
                PointOfCareIds = dto.PointOfCareIds ?? new List<ObjectId>(),
                Settings = dto.Settings ?? new DeviceSettings()
            };
        } 
    /// <summary>
    /// Creates a new device entity from the provided DTO, sets the creation and update timestamps to the current UTC time, and persists it via the device repository.
    /// </summary>
    /// <param name="deviceDto">The data transfer object containing the device information to be mapped and stored.</param>
    /// <returns>The created <see cref="Device"/> entity after successful insertion, or <see langword="null"/> if the device could not be created.</returns>
    public async Task<Device?> Create(DeviceDto deviceDto)
        {
            var device = ToEntity(deviceDto);
            device.CreatedAt = DateTime.UtcNow;
            device.UpdatedAt = DateTime.UtcNow;
            await _deviceRepository.InsertOneAsync(device);
            return device;
        }

    /// <summary>
    /// Deletes an object identified by the specified <paramref name="objectId"/> by delegating to the device repository.
    /// Returns <c>true</c> when the repository's delete operation yields a non-null result, and <c>false</c> when the result is <c>null</c> (e.g., the object was not found or could not be deleted).
    /// </summary>
    /// <param name="objectId">The unique identifier of the object to delete.</param>
    /// <returns>A task that resolves to <c>true</c> if the object was deleted; otherwise, <c>false</c>.</returns>
    public async Task<bool> Delete(ObjectId objectId)
        {
            return await _deviceRepository.DeleteAsync(objectId) != null;
        }

    /// <summary>
    /// Updates an existing device by mapping the provided DTO to a device entity, setting the update timestamp, and persisting the changes through the repository.
    /// </summary>
    /// <param name="deviceDto">The data transfer object containing the updated device information.</param>
    /// <returns>The updated <see cref="Device"/> entity, or <c>null</c> if no device is returned.</returns>
    public async Task<Device?> Update(DeviceDto deviceDto)
        {
            var device = ToEntity(deviceDto);
            device.UpdatedAt = DateTime.UtcNow;
            await _deviceRepository.UpdateOneAsync(device.Id, device);
            return device;
        }

    /// <summary>
    /// Processes an incoming device event by locating an existing device or creating a new one, then handling device-type-specific logic. Looks up the device using the MAC address, serial number, UUID, or key in that order, falling back to creating a new record when no match is found. When the device is a button, additional button management logic is invoked.
    /// </summary>
    /// <param name="deviceDto">The data transfer object containing the device information from the event, used for lookup and creation.</param>
    /// <returns>The existing or newly created <see cref="Device"/> associated with the event.</returns>
    public async Task<Device?> ReceiveEvent(DeviceDto deviceDto)
        {
            Device? deviceExist = null;
            if (!string.IsNullOrEmpty(deviceDto.MacAddr))
            {
                deviceExist = await _deviceRepository.FindByMacAddr(deviceDto.MacAddr);
            }
    
            if (deviceExist == null && deviceDto.SerialNumber != null)
            {
                deviceExist = await _deviceRepository.FindBySerialNumber(deviceDto.SerialNumber);
            }
    
            if (deviceExist == null && deviceDto.Uuid != null)
            {
                deviceExist = await _deviceRepository.FindByUuid(deviceDto.Uuid);
            }
    
            if (deviceExist == null && deviceDto.Key != null)
            {
                deviceExist = await _deviceRepository.FindByKey(deviceDto.Key);
            }
    
            if (deviceExist == null)
            {
                deviceExist = ToEntity(deviceDto);
                deviceExist.CreatedAt = DateTime.UtcNow;
                deviceExist.UpdatedAt = DateTime.UtcNow;
                await _deviceRepository.InsertOneAsync(deviceExist);
            }
            else
            {
                await _deviceRepository.UpdateDeviceStats(deviceExist.Id, deviceDto);
            }
    
            switch (deviceDto.DeviceType)
            {
                case DeviceType.Unknown:
                    break;
                case DeviceType.Button:
                    await ManageDeviceButton(deviceExist, deviceDto);
                    break;
            }
            
            return deviceExist;
        }

    /// <summary>
    /// Handles device button actions by dispatching the configured action type (sending an observation or alarm) when both the device's configured action and the received click event are present.
    /// </summary>
    /// <param name="deviceExist">The existing device whose configured action settings determine which action to execute.</param>
    /// <param name="deviceDto">The incoming device event payload providing the click type that triggers the action.</param>
    private async Task ManageDeviceButton(Device deviceExist, DeviceDto deviceDto)
        {
            if (deviceExist.Settings?.Action?.Type != null && deviceDto.Event?.ClickType != null)
            {
                switch (deviceExist.Settings.Action.Type)
                {
                    case DeviceActionType.SendObs:
                        await SendObservationOnAction(deviceExist.Settings.Action, deviceDto.Event.ClickType.Value, deviceExist.PointOfCareIds);
                        break;
                    case DeviceActionType.SendAlarm:
                        await SendAlarmOnAction(deviceExist.Settings.Action, deviceDto.Event.ClickType.Value, deviceExist.PointOfCareIds);
                        break;
                }
            }
        }

    /// <summary>
    /// Sends a patient observation alarm based on a device action and the type of click event that triggered it.
    /// The alarm value is selected from the device action's configuration according to the click type
    /// (SingleClick, DoubleClick, or Hold) and is dispatched for each point of care that has an associated patient.
    /// The method exits early when the configuration observation cannot be found or when the action has no value defined for the current click type.
    /// </summary>
    /// <param name="settingsAction">The device action containing the configuration observation and the per-click-type alarm values to use.</param>
    /// <param name="eventClickType">The click event that triggered the action, which determines which value from the device action is sent.</param>
    /// <param name="deviceExistPointOfCareIds">The list of point of care identifiers whose patients should receive the alarm.</param>
    private async Task SendAlarmOnAction(
            DeviceAction settingsAction,
            ClickType eventClickType,
            List<ObjectId> deviceExistPointOfCareIds)
        {
            var configObs = await _configObservationService.GetConfigById(settingsAction.ConfigObservationId);
            if(configObs == null) return;
            var obsData = new ObservationData
            {
                Code = configObs.Code,
                CodingSystem = configObs.CodingSystem,
                Time = DateTime.UtcNow,
                Text = configObs.Name,
            };
            var obs = new PatientObservationAlarm
            {
                InactivationState = new InactivationState{ Visual = AlarmEnum.AudioVideoState.Enabled, Audio = AlarmEnum.AudioVideoState.Enabled},
                MessageTime = DateTime.UtcNow,
                Persist = true,
                Code = obsData.Code,
                CodingSystem = obsData.CodingSystem,
                Name = configObs.Name,
                Time =  DateTime.UtcNow
            };
            foreach (var pocId in deviceExistPointOfCareIds)
            {
                var data = await _pointOfCareService.GetInfo(pocId, null, true);
                if (data != null && data.Patient?.Id != null)
                {
                    obs.PatientId = data.Patient.Id;
                    obs.Patient = data.Patient;
                    switch (eventClickType)
                    {
                        case ClickType.SingleClick:
                            if(settingsAction.ValueOnSingleClick == null) return;
                            obs.Value = settingsAction.ValueOnSingleClick;
                            break;
                        case ClickType.DoubleClick:
                            if(settingsAction.ValueOnDoubleClick == null) return;
                            obs.Value = settingsAction.ValueOnDoubleClick;
                            break;
                        case ClickType.Hold:
                            if(settingsAction.ValueOnHoldClick == null) return;
                            obs.Value = settingsAction.ValueOnHoldClick;
                            break;
                    }
                    // Process Obs on service
                    await _alarmService.ProcessAlarmObservations(new List<PatientObservationAlarm>{obs},new List<PatientObservation>(){new (){Name = configObs.Name, Value = obs.Value, Code = obsData.Code, CodingSystem = obsData.CodingSystem}}, data.Patient, DateTime.UtcNow, obsData);
                }
    
            }
        }

    /// <summary>
    /// Sends a patient observation derived from the configuration linked to a device action, applying the value
    /// associated with the specified click type (single, double, or hold) for each point of care patient found.
    /// The method short-circuits when the configuration observation is missing or when no value is defined for
    /// the given click type.
    /// </summary>
    /// <param name="settingsAction">The device action whose associated configuration observation and per-click values drive the observation payload.</param>
    /// <param name="eventClickType">The type of click event that triggered the action; selects which value (single, double, or hold) is assigned to the observation.</param>
    /// <param name="deviceExistPointOfCareIds">The list of point of care identifiers whose resolved patients will receive the generated observation.</param>
    /// <returns>A task that represents the asynchronous send operation; no meaningful business result is returned.</returns>
    private async Task SendObservationOnAction(
            DeviceAction settingsAction,
            ClickType eventClickType,
            List<ObjectId> deviceExistPointOfCareIds)
        {
            var configObs = await _configObservationService.GetConfigById(settingsAction.ConfigObservationId);
            if(configObs == null) return;
            var obsData = new ObservationData
            {
                Code = configObs.Code,
                CodingSystem = configObs.CodingSystem,
                Time = DateTime.UtcNow,
                Text = configObs.Name,
            };
            var obs = new PatientObservation
            {
                MessageTime = DateTime.UtcNow,
                Persist = true,
                Code = obsData.Code,
                CodingSystem = obsData.CodingSystem,
                Name = configObs.Name,
                Time =  DateTime.UtcNow
            };
            foreach (var pocId in deviceExistPointOfCareIds)
            {
                var data = await _pointOfCareService.GetInfo(pocId, null, true);
                if (data != null && data.Patient?.Id != null)
                {
                    obs.PatientId = data.Patient.Id;
                    obs.Patient = data.Patient;
                    switch (eventClickType)
                    {
                        case ClickType.SingleClick:
                            if(settingsAction.ValueOnSingleClick == null) return;
                            obs.Value = settingsAction.ValueOnSingleClick;
                            break;
                        case ClickType.DoubleClick:
                            if(settingsAction.ValueOnDoubleClick == null) return;
                            obs.Value = settingsAction.ValueOnDoubleClick;
                            break;
                        case ClickType.Hold:
                            if(settingsAction.ValueOnHoldClick == null) return;
                            obs.Value = settingsAction.ValueOnHoldClick;
                            break;
                    }
                    // Process Obs on service
                    _observationService.ProcessObservations(new List<PatientObservation>{obs}, data.Patient, DateTime.UtcNow, obsData);
                }
    
            }
        }
}