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.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using Patient = adas_core.Domain.Models.MongoModels.Patient;

namespace adas_core.Application.Services;

public class AppointmentService(
    IAppointmentRepository appointmentRepository,
    IAppointmentArchiveRepository appointmentArchiveRepository,
    Lazy<IPatientService> patientService,
    Lazy<IObservationService> observationService,
    IDiagnosisService diagnosisService,
    IUnitService unitService,
    IOptions<ApiSettings> apiSettings,
    IOptions<CacheSettings> cacheSettings,
    ILogger<AppointmentService> logger,
    IHttpContextAccessor httpContextAccessor,
    ILocalAuditService auditService,
    IPointOfCareService pointOfCareService,
    ISubscribersService subscribersService,
    IClientMessageService clientMessageService,
    ICacheService cacheService)
    : IAppointmentService
{
    private readonly bool _createPatientWithSiu = apiSettings.Value.CreatePatientWithSiu;
    private readonly CacheSettings?  _cacheSettings =  cacheSettings.Value ;

    public async Task SaveRequest(ApiRequest apiRequest)
    {
        if (string.IsNullOrEmpty(apiRequest.PatientNumber))
        {
            logger.LogDebug("Patient is null");
            throw new ApiRequestException("Patient number is null");
        }

        logger.LogDebug("patientNumber:  {apiRequestPatientNumber} location:  {apiRequestLocation}",
            apiRequest.PatientNumber, apiRequest.Location);
        logger.LogDebug("RequestType:  {apiRequestType}", apiRequest.Type);

        var patient = await patientService.Value.FindByPatientNumber(apiRequest.PatientNumber);
        if (patient == null)
        {
            // PATIENT NOT FOUND
            logger.LogWarning("Patient not Found. {patientNumber}", apiRequest.PatientNumber);
            if (!_createPatientWithSiu) return; // IGNORE
            patient = await patientService.Value.CreatePatientFromRequest(apiRequest, true);
        }

        await ProcessApiRequest(apiRequest, patient);

        if (patient != null)
        {
            if (apiRequest is { Observations: not null, ObservationData: not null })
                observationService.Value.ProcessObservations(apiRequest.Observations, patient,
                    apiRequest.MessageTime, apiRequest.ObservationData);
            else
                logger.LogWarning("Observations not Found. {patientNumber} ", patient.PatientNumber);

            if (apiRequest.Diagnosis != null)
                _ = diagnosisService.ProcessDiagnosis(apiRequest.Diagnosis, patient, apiRequest.MessageTime);
            else
                logger.LogWarning("Diagnosis not Found. {patientNumber}", patient.PatientNumber);
        }
    }

    public async Task ProcessApiRequest(ApiRequest apiRequest, Patient? patient)
    {
        if (patient == null)
            return;

        var unitConfig = await unitService.FindById(patient.UnitId);
        if (!Hl7Utils.ManageAutoAdt(unitConfig, null, logger, "ORU")) return;

        apiRequest.Appointments ??= [];
        if (apiRequest.Appointment != null) apiRequest.Appointments.Add(apiRequest.Appointment);
        //apiRequest.appointments.ForEach(ap =>
        foreach (var ap in apiRequest.Appointments)
        {
            if (ap.VisitNumber == null || patient.Person == null) return;

            ap.PatientId = patient.Id;
            ap.Patient = patient.Person;
            var apdb = ap.VisitNumber != null
                ? await appointmentRepository.FindByPatientAndVisitNumber(ap.PatientId, ap.VisitNumber)
                : null;
            apdb ??= await appointmentRepository.FindByPatientAndReason(ap.PatientId, ap.AppointmentReason);

            if (apdb != null && DateTime.Compare(apdb.UpdateTime, apiRequest.MessageTime) > 0) return;
            // TODO: Aux
            var oldAp = await auditService.DeepCopyAsync(ap);
            switch (apiRequest.Type)
            {
                //* SIU_S12 - Notification of new appointment booking
                //* SIU_S13 - Notification of Appointment Rescheduling
                //* SIU_S14 - Notification of Appointment Modification
                //* SIU_S18 - Notification of Addition of Service/Resource on Appointment
                //* SIU_S19 - Notification of Modification of Service/Resource on Appointment
                //* SIU_S20 - Notification of Cancellation of Service/Resource on Appointment
                //* SIU_S21 - Notification of Discontinuation of Service/Resource on Appointment
                //* SIU_S22 - Notification of Deletion of Service/Resource on Appointment

                case "SIU_S12":
                case "SIU_S13":
                case "SIU_S14":
                case "SIU_S18":
                case "SIU_S19":
                case "SIU_S20":
                case "SIU_S21":
                case "SIU_S22":
                    ap.UpdateTime = apiRequest.MessageTime;
                    if (apdb != null)
                    {
                        ap.Id = apdb.Id;
                        ap.CreateTime = apdb.CreateTime;
                        ap.AppointmentOperationType = OperationType.UpdatedAppointment;
                        ap.ApplyResourceGroups(apdb);
                        await appointmentRepository.Update(ap);
                        // Invalidar CACHE (colección completa)
                        await cacheService.DeleteObjectAsync(CacheKeys.PatientAppointmentsToday(ap.PatientId));
                        //SendBroadcast(ap, OperationType.updatedAppointment);
                    }
                    else
                    {
                        ap.CreateTime = apiRequest.MessageTime;
                        ap.AppointmentOperationType = OperationType.NewAppointment;
                        ap.ApplyResourceGroups();
                        await appointmentRepository.InsertOneAsync(ap);
                        //SendBroadcast(ap, OperationType.newAppointment);
                    }

                    break;


                //* SIU_S15 - Notification of Appointment Cancellation
                //* SIU_S16 - Notification of Appointment Discontinuation
                //* SIU_S17 - Notification of Appointment Deletion

                case "SIU_S15":
                case "SIU_S16":
                case "SIU_S17":
                    ap.UpdateTime = apiRequest.MessageTime;
                    if (apdb != null)
                    {
                        ap.Id = apdb.Id;
                        ap.CreateTime = apdb.CreateTime;
                        ap.AppointmentOperationType = OperationType.CanceledAppointment;
                        ap.ApplyResourceGroups(apdb);
                        await appointmentRepository.Update(ap);
                        await cacheService.DeleteObjectAsync(CacheKeys.PatientAppointmentsToday(ap.PatientId));
                    }
                    else
                    {
                        ap.CreateTime = apiRequest.MessageTime;
                        ap.AppointmentOperationType = OperationType.CanceledAppointment;
                        ap.ApplyResourceGroups();
                        await appointmentRepository.InsertOneAsync(ap);
                    }

                    //SendBroadcast(ap, OperationType.canceledAppointment);
                    break;


                //* SIU_S23 - Notification of Blocked Schedule Time Slot(S)
                //* SIU_S24 - Notification of Opened (un-blocked) Schedule Time Slot(s)
                //* SIU_S26 - Notification That Patient Did Not Show Up for Scheduled Appointment

                default:

                    ap.UpdateTime = apiRequest.MessageTime;
                    if (apdb != null)
                    {
                        ap.Id = apdb.Id;
                        ap.CreateTime = apdb.CreateTime;
                        //ap.appointmentOperationType = OperationType.updatedAppointment;
                        ap.ApplyResourceGroups(apdb);
                        await appointmentRepository.Update(ap);
                        await cacheService.DeleteObjectAsync(CacheKeys.PatientAppointmentsToday(ap.PatientId));
                        //SendBroadcast(ap, ap.appointmentOperationType);
                    }
                    else
                    {
                        ap.CreateTime = apiRequest.MessageTime;
                        //ap.appointmentOperationType = OperationType.newAppointment;
                        ap.ApplyResourceGroups();
                        await appointmentRepository.InsertOneAsync(ap);
                        //SendBroadcast(ap, ap.appointmentOperationType);
                    }

                    break;
            }

            if (apdb != null)
                await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, apdb, ap);

            else await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, ap);

            if (ap.AppointmentOperationType.HasValue)
                await SendBroadcast(ap, ap.AppointmentOperationType);
        }
    }

    public Task SaveRequestAsync(ApiRequest apiRequest)
    {
        return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest); }));
    }

    public async Task Archive(Patient patient)
    {
        await ArchiveByPatientId(patient.Id);
    }

    public async Task ArchiveByPatientId(ObjectId id)
    {
        logger.LogDebug("Archive Appointments by patientId {id}", id);
        using (var cursor = await FindByPatientIdAsync(id))
        {
            while (await cursor.MoveNextAsync())
                foreach (var current in cursor.Current)
                    await appointmentArchiveRepository.InsertOneAsync(current);
        }

        await DeleteByPatientId(id);
    }

    public async Task<List<PatientAppointment>> GetByPatient(ObjectId patientId)
    {
        return await appointmentRepository.GetByPatient(patientId);
    }
    
    public async Task<List<PatientAppointment>> GetTodayByPatient(
        ObjectId patientId,
        CancellationToken ct = default)
    {
        // Obtener clave + TTL según CacheSettings
        var (key, ttl) = CacheKeys.PatientAppointmentsTodayKeyWithTtl(_cacheSettings, patientId);

        // Cachear la lista RAW de citas del paciente (sin filtrar)
        var allAppointments = await cacheService.GetOrSetObjectAsync(
            key,
            async () =>
            {
                // 1 - Consultar todas las citas del paciente
                var list = await appointmentRepository.GetByPatient(patientId);

                // 2 - Devuelve RAW (List<PatientAppointment>), nada filtrado
                return list;
            },
            ttl,
            ct);

        // 3 - Ahora filtramos solo las de "hoy" 
        var today = DateTime.UtcNow.Date;

        var todayAppointments = allAppointments
            .Where(a => a.Timings.Any(t =>
                t.StartTime.HasValue &&
                t.StartTime.Value.Date == today))
            .ToList();

        return todayAppointments;
    }

   
    public async Task<List<PatientAppointment>> GetTodayByPoc(
        ObjectId pocId,
        CancellationToken ct = default)
    {
        var poc = await pointOfCareService.FindById(pocId);
        if(poc == null) return [];
        
        // Obtener clave + TTL según CacheSettings
        var (key, ttl) = CacheKeys.PocAppointmentsTodayKeyWithTtl(_cacheSettings, pocId);

        // Cachear la lista RAW de citas del pointOfCare (sin filtrar)
        var allAppointments = await cacheService.GetOrSetObjectAsync(
            key,
            async () =>
            {
                // 1 - Consultar todas las citas del pointOfCare
                var list = await appointmentRepository.FindByPoC(poc);

                // 2 - Devuelve RAW (List<PatientAppointment>), nada filtrado
                return list;
            },
            ttl,
            ct);

        // 3 - Ahora filtramos solo las de "hoy" 
        var today = DateTime.UtcNow.Date;

        var todayAppointments = allAppointments
            .Where(a => a.Timings.Any(t =>
                t.StartTime.HasValue &&
                t.StartTime.Value.Date == today))
            .ToList();

        return todayAppointments;
    }

    public Task<IAsyncCursor<PatientAppointment>> FindByPatientIdAsync(ObjectId patientId)
    {
        return appointmentRepository.FindByPatientIdAsync(patientId);
    }

    public async Task<List<PatientAppointment>> FindByLocation(PatientLocation location)
    {
        return await appointmentRepository.FindByLocation(location);
    }
    
    
    public async Task DeleteByPatientId(ObjectId id)
    {
        var patientApp = await FindByPatientIdAsync(id);
        logger.LogDebug("Delete Appointments by Patient Id {id}", id);
        await appointmentRepository.DeleteByPatientId(id);
        
        // Invalidar CACHE (colección completa)
        await cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.Appointments));

        await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, patientApp, null);
    }

    public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId)
    {
        await appointmentRepository.UpdateManyObjectId(nameId, id, oldId);
    }

    private async Task SendBroadcast(PatientAppointment appointment, OperationType? operationType)
    {
        //RECORRE LOS DIFERENTES LOCATIONS DE LA CITA
        foreach (var resourceGroup in appointment.ResourceGroups)
            if (resourceGroup.Locations != null)
                foreach (var location in resourceGroup.Locations)
                    // TODO
                    if (!string.IsNullOrEmpty(location.UnitName) && !string.IsNullOrEmpty(location.Bed))
                    {
                        var unit = await unitService.FindByName(location.UnitName);
                        if (unit == null) continue;

                        var poc = await pointOfCareService.FindByBedAndUnitId(location.Bed, unit.Id);
                        if (poc == null) continue;

                        var subscribers = subscribersService.GetSubscribers().Where(s =>
                            !s.LocationIds.IsNullOrEmpty() && s.LocationIds.Any(c =>
                                c == poc.Id
                            )).ToList();

                        foreach (var subscriber in subscribers)
                            _ = clientMessageService.SendAsync(subscriber.Id, operationType, appointment);
                    }
    }
}