using adas_core.Application.Exceptions;
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.AppSettings;
using adas_core.Domain.Models.Filter;
using adas_core.Domain.Models.Masters;
using adas_core.Domain.Models.MongoModels;
using adas_core.Domain.Models.Responses;
using adas_core.Domain.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;

namespace adas_core.Application.Services;

public class PointOfCareService(
    ILogger<PointOfCareService> logger,
    IPointOfCareRepository pointOfCareRepository,
    Lazy<IPatientService> patientService,
    Lazy<IUnitService> unitService,
    ISubscribersService subscribersService,
    Lazy<IClientMessageService> clientMessageService,
    Lazy<IAdmissionService> admissionService,
    IHttpContextAccessor httpContextAccessor,
    ILocalAuditService auditService,
    ICacheService cacheService,
    IOptions<CacheSettings> cacheSettings)
    : IPointOfCareService
{
    private readonly CacheSettings?  _cacheSettings =  cacheSettings.Value ;
    
    public async Task<PointOfCare?> InsertPointOfCare(PointOfCare pointOfCare)
    {
        try
        {
            var unit = await unitService.Value.FindById(pointOfCare.UnitId);
            if (unit == null)
            {
                logger.LogError("Unit id: {unitId} Not Found. PointOfCare not inserted", pointOfCare.UnitId);
                return null;
            }

            pointOfCare.UnitId = unit.Id;

            await pointOfCareRepository.InsertOneAsync(pointOfCare);
            await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, pointOfCare);
            return await FindById(pointOfCare.Id);
        }
        catch (Exception ex)
        {
            logger.LogError("Exception inserting pointOfCare: {pointOfCare}, Exception: {ex}", pointOfCare, ex);
            return null;
        }
    }

    public async Task Delete(ObjectId id)
    {
        var poc = await FindById(id);
        if (poc is not { AdmissionId: null }) return;
        
        await pointOfCareRepository.Delete(id);
        
        await cacheService.DeleteByPatternAsync(CacheKeys.PointOfCareBase(id));
        
        await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, poc, null);
        
    }

    public async Task DeletePoCsByUnitId(ObjectId unitId)
    {
        await pointOfCareRepository.DeleteManyByUnitId(unitId);
        
        await cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.PontOfCare));
        
    }

    public async Task<HashSet<ObjectId>> FindAllIdCamerasInUse()
    {
        return await pointOfCareRepository.FindAllIdCamerasInUse();
    }
    public async Task<HashSet<ObjectId>> FindAllIdRelaysInUse()
    {
        return await pointOfCareRepository.FindAllIdRelaysInUse();
    }
    public async Task<HashSet<ObjectId>> FindAllIdBeaconsInUse()
    {
        return await pointOfCareRepository.FindAllIdBeaconsInUse();
    }

    public async Task<IEnumerable<PointOfCare>?> FindAllByUnitIdWithDevices(ObjectId unitId)
    {
        var result = await pointOfCareRepository.FindAllByUnitIdWithDevices(unitId);

        return result ?? [];
    }


    public async Task<PointOfCare?> Update(PointOfCare pointOfCare)
    {
        var oldPoc = await pointOfCareRepository.FindById(pointOfCare.Id);
        await pointOfCareRepository.Update(pointOfCare);
        var updatedPoc = await GetInfo(pointOfCare.Id);
        await cacheService.DeleteByPatternAsync(CacheKeys.PointOfCareBase(pointOfCare.Id));
        await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldPoc, updatedPoc);
        if (updatedPoc == null) return null;
        SendPointOfCareBroadcast(updatedPoc, OperationType.UpdatedPointOfCare);
        return updatedPoc;

    }

    public async Task UpdateUnit(ObjectId id, Unit unit)
    {
        var poc = await FindById(id);
        if (poc == null)
            return;
        await pointOfCareRepository.UpdateUnitId(id, unit);
        
        await cacheService.DeleteByPatternAsync(CacheKeys.PointOfCareBase(id));
        
        var newPoc = await FindById(id);
        await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, poc, newPoc!);
        SendPointOfCareBroadcast(poc, OperationType.UpdatedPointOfCare);
    }

    public async Task<List<PointOfCare>> GetAll()
    {
        var c = await pointOfCareRepository.GetAll();
        return c ?? [];
    }

    public async Task<List<PointOfCare>> GetAllConfigs()
    {
        var c = await pointOfCareRepository.GetAllConfigs();
        return c ?? [];
    }

    public async Task<List<PointOfCare>> GetAllLocationInfo()
    {
        var c = await pointOfCareRepository.GetAllLocationInfo();
        return c ?? [];
    }

    public async Task<PaginationResponse<PointOfCare>> GetPaginatedPoCs(PaginationFilter filter)
    {
        var result = pointOfCareRepository.GetPaginatedPoCs(filter);
        var count = await result.CountDocumentsAsync();
        var data = await result.Skip((filter.PageNumber - 1) * filter.PageSize)
            .Limit(filter.PageSize)
            .ToCursorAsync();


        var dataList = await data.ToListAsync();
        return new PaginationResponse<PointOfCare>(dataList, filter.PageNumber, filter.PageSize, count);
    }

    public async Task UpdateConfiguration(ObjectId id, PointOfCareConfiguration configuration)
    {
        var old = await pointOfCareRepository.GetPoCConfiguration(id) ??
                  throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed);
        await pointOfCareRepository.UpdateConfiguration(id, configuration);
        
        await cacheService.DeleteByPatternAsync(CacheKeys.PointOfCareBase(id));
        
        await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, old.Configuration,
            configuration);
    }

    public async Task<PointOfCare?> FindById(ObjectId id)
    {
        return await pointOfCareRepository.FindByIdAllConfig(id);
    }
    public async Task<PointOfCare?> FindByIdAllConfig(ObjectId id)
    {
        return await pointOfCareRepository.FindByIdAllConfig(id);
    }

    public async Task<IEnumerable<PointOfCare>?> FindAllByUnitId(ObjectId unit)
    {
        var result = await pointOfCareRepository.FindAllByUnitId(unit);

        return result ?? [];
    }

    public async Task<IEnumerable<PointOfCare>> FindByUnitAndStatus(ObjectId unitId, StatusEnum.PointOfCare poc,
        bool excludeVirtual = false)
    {
        return await pointOfCareRepository.FindByUnitAndStatus(unitId, poc, excludeVirtual);
    }

    /// <summary>
    ///     Check nex admission when patient has exit from poc
    /// </summary>
    /// <param name="patientLocation">PoC id</param>
    public async void CheckNextAdmission(ObjectId? patientLocation)
    {
        try
        {
            if (patientLocation == null) return;
            var pocToCheck = await GetInfo(patientLocation.Value,null);
            if (pocToCheck != null && pocToCheck.Status != StatusEnum.PointOfCare.Locked)
            {
                if (pocToCheck.AdmissionId != null)
                {
                    pocToCheck.Status = StatusEnum.PointOfCare.Reserved;
                }
                else
                {
                    // Sabemos que puede ser una lista, pero limitamos directamente a 1 elemento
                    var next = await admissionService.Value.GetAdmissionByPointOfCareId(pocToCheck.Id);
                    var adm = next.FirstOrDefault();
                    if (adm != null)
                    {
                        pocToCheck.Admission = adm;
                        pocToCheck.AdmissionId = adm.Id;
                        pocToCheck.Status = StatusEnum.PointOfCare.Reserved;
                    }
                    else
                    {
                        pocToCheck.Status = StatusEnum.PointOfCare.Available;
                    }
                }

                await Update(pocToCheck);
            }
        }
        catch (Exception e)
        {
            logger.LogError("Error checking next admission for PointOfCare {pocId}: {message}", patientLocation,
                e.Message);
        }
    }

    public async Task<PointOfCare?> FindByBedAndUnitId(string? bed, ObjectId? unitId)
    {
        if (unitId == null || string.IsNullOrEmpty(bed)) return null;
        return await pointOfCareRepository.FindByBedAndUnitId(bed, unitId.Value);
    }

    public async Task UpdateRelayConfig(PointOfCare poc)
    {
        var oldPoc = await pointOfCareRepository.GetPoCConfiguration(poc.Id);
        if (poc.Configuration?.RelayIdList != null)
        {
            await pointOfCareRepository.UpdateRelayConfig(poc.Id, poc.Configuration.RelayIdList);
            await cacheService.DeleteByPatternAsync(CacheKeys.PointOfCareBase(poc.Id));
            await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldPoc, poc);
        }
    }

    public async Task<IEnumerable<PointOfCare>?> FindByRoom(string room)
    {
        return await pointOfCareRepository.FindByRoom(room);
    }

    public async Task<IEnumerable<PointOfCare>?> FindByBed(string bed)
    {
        return await pointOfCareRepository.FindByBed(bed);
    }

    public async Task<List<PointOfCare>> FindAllByUnitIds(List<ObjectId> unitIds)
    {
        var filter = Builders<PointOfCare>.Filter.In(p => p.UnitId, unitIds);
        return await pointOfCareRepository.FindByFilter(filter);
    }
    
    public async Task<PointOfCare?> GetInfo(ObjectId id, bool fillPatientData = true)
    {
        var poc = await FindById(id);
        if (poc == null) return null;
        var unit = await unitService.Value.FindById(poc.UnitId);
        if (unit is { Name: not null })
            poc.UnitName = unit.Name;
        if (fillPatientData)
        {
            var patient = await patientService.Value.GetByPointOfCare(poc);
            if (patient != null)
            {
                poc.Patientid = patient.Id;
                poc.Patient = patient;
            }

            if (poc.AdmissionId != null)
            {
                var admission = await admissionService.Value.GetAdmissionByIdAsync(poc.AdmissionId.Value);
                if (admission != null)
                    poc.Admission = admission;
            }
        }

        return poc;
    }
    
    public async Task<PointOfCare?> GetInfo(ObjectId id, LocaleEnum? locale, bool fillPatientData = true, 
        CancellationToken ct = default)
    {
        var (key, ttl) = CacheKeys.PointOfCareBaseKeyWithTtl(_cacheSettings, id);
        var poc = await cacheService.GetOrSetObjectAsync(key,
            () => pointOfCareRepository.FindById(id),
            ttl, ct);

        if (poc == null || !fillPatientData) 
            return poc;
        
        var unit = await unitService.Value.FindById(poc.UnitId);
        if (unit?.Name != null)
            poc.UnitName = unit.Name;
        
        var patient = locale != null
            ? await patientService.Value.GetByPointOfCareAndLocale(poc, unit, locale)
            : await patientService.Value.GetByPointOfCare(poc);
            
        if (patient != null)
        {
            poc.Patientid = patient.Id;
            poc.Patient = patient;
        }

        if (poc.AdmissionId == null) return poc;
            
        var admission = await admissionService.Value.GetAdmissionByIdAsync(poc.AdmissionId.Value);
        if (admission != null)
            poc.Admission = admission;
        return poc;
    }
   
  

    public async Task<PointOfCare?> FindPoCByPatientId(ObjectId patientId)
    {
        var patient = await patientService.Value.FindById(patientId);
        if (patient is { PointOfCareId: not null })
            return await FindById(patient.PointOfCareId.Value);
        return null;
    }

    public async Task<PointOfCare?> FindPoCByPatientNumber(string patientNumber)
    {
        var patient = await patientService.Value.FindByPatientNumber(patientNumber);
        if (patient is { PointOfCareId: not null })
            return await FindById(patient.PointOfCareId.Value);
        return null;
    }

    public async Task<long> CountPoCsByUnitId(ObjectId unitId)
    {
        return await pointOfCareRepository.CountByUnitId(unitId);
    }

    public async Task<long> CountVirtualPoCsByUnitId(ObjectId unitId)
    {
        return await pointOfCareRepository.CountVirtualsByUnitId(unitId);
    }

    public async Task SetPointOfCareStatus(ObjectId id, StatusEnum.PointOfCare status)
    {
        var pointOfCare = await GetInfo(id,null);

        if (pointOfCare == null) return;

        pointOfCare.Status = status;

        await pointOfCareRepository.Update(pointOfCare);
        
        await cacheService.DeleteByPatternAsync(CacheKeys.PointOfCareBase(id));
        
        var oldPoc = await auditService.DeepCopyAsync(pointOfCare);
        if (oldPoc != null)
            await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldPoc, pointOfCare);
        // await _patientService.Value.UpdatePatientAltable()
        SendPointOfCareBroadcast(pointOfCare, OperationType.UpdatedPointOfCare);
    }

    public async Task<PointOfCare?> FindPoCByPatientLocation(PatientLocation patientLocation)
    {
        return await pointOfCareRepository.FindByPatientLocation(patientLocation);
    }

    private void SendPointOfCareBroadcast(PointOfCare pointOfCare, OperationType operation)
    {
        try
        {
            var subscribers = subscribersService.GetSubscribers()
                .Where(s => s.LocationIds.Any(c => c == pointOfCare.Id)).ToList();

            foreach (var subscriber in subscribers)
                _ = clientMessageService.Value.SendAsync(subscriber.Id, operation, pointOfCare);
        }
        catch (Exception ex)
        {
            logger.LogError("Exception sending PoC broadcast. Operation type: {op}. Exception: {ex}",
                operation.ToString(), ex);
        }
    }
}