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;

    /// <summary>
    /// Saves an API request by validating the patient, resolving or creating the patient record, processing the request, and handling associated observations and diagnoses.
    /// </summary>
    /// <param name="apiRequest">The API request containing the patient number, location, type, observations, diagnosis, and related data to be processed.</param>
    /// <exception cref="ApiRequestException">Thrown when the <paramref name="apiRequest"/> has a null or empty patient number.</exception>
    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);
        }
    }

    /// <summary>
    /// Processes an incoming API request for a patient, handling HL7 SIU message types to create, update, or cancel appointments.
    /// Validates the patient and auto-ADT configuration, then routes the request to the appropriate handler based on message type: SIU_S12-S14 and SIU_S18-S22 (booking/rescheduling/modification), SIU_S15-S17 (cancellation), and other types (blocked slots / no-show), persisting changes, refreshing cache, creating audit logs, and emitting broadcasts.
    /// </summary>
    /// <param name="apiRequest">The API request containing the HL7 message type, timestamp, and appointment payload to process.</param>
    /// <param name="patient">The patient associated with the request; if null, the method returns without processing.</param>
    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);
        }
    }

    /// <summary>
    /// Asynchronously saves the provided API request by running the save operation on a background task.
    /// </summary>
    /// <param name="apiRequest">The API request to be saved.</param>
    /// <returns>A task that represents the asynchronous save operation.</returns>
    public Task SaveRequestAsync(ApiRequest apiRequest)
    {
        return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest); }));
    }

    /// <summary>
    /// Archives the specified patient by delegating the operation to <see cref="ArchiveByPatientId"/> using the patient's identifier.
    /// </summary>
    /// <param name="patient">The patient to be archived.</param>
    public async Task Archive(Patient patient)
    {
        await ArchiveByPatientId(patient.Id);
    }

    /// <summary>
    /// Archives all appointments associated with the specified patient by copying them to the appointment archive repository and then deleting them from the source collection.
    /// </summary>
    /// <param name="id">The unique identifier of the patient whose appointments should be archived.</param>
    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);
    }

    /// <summary>
    /// Retrieves a list of patient appointments associated with the specified patient identifier by delegating to the appointment repository.
    /// </summary>
    /// <param name="patientId">The unique identifier of the patient whose appointments are being requested.</param>
    /// <returns>A task that represents the asynchronous operation, containing a list of <see cref="PatientAppointment"/> records for the given patient.</returns>
    public async Task<List<PatientAppointment>> GetByPatient(ObjectId patientId)
    {
        return await appointmentRepository.GetByPatient(patientId);
    }

    /// <summary>
    /// Retrieves the list of appointments scheduled for today (UTC) for the specified patient, using a cache-aside pattern to avoid repeated database queries.
    /// The full list of patient appointments is fetched from cache (or loaded from the repository on a cache miss) and then filtered locally to include only those whose start time falls on the current UTC date.
    /// </summary>
    /// <param name="patientId">The unique identifier of the patient whose appointments are being queried.</param>
    /// <param name="ct">Cancellation token used to cancel the asynchronous operation.</param>
    /// <returns>A task that resolves to a list of <see cref="PatientAppointment"/> instances scheduled for today; an empty list is returned when no appointments match.</returns>
    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;
    }


    /// <summary>
    /// Retrieves the patient appointments scheduled for today at the specified point of care. Uses a cache to store the full appointment list for the point of care and filters it by today's date; returns an empty list if the point of care is not found.
    /// </summary>
    /// <param name="pocId">The identifier of the point of care whose appointments should be retrieved.</param>
    /// <param name="ct">A cancellation token to cancel the asynchronous operation.</param>
    /// <returns>A task that represents the asynchronous operation, containing the list of patient appointments scheduled for today at the specified point of care.</returns>
    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;
    }

    /// <summary>
    /// Asynchronously retrieves all patient appointments associated with the specified patient identifier by delegating to the appointment repository.
    /// </summary>
    /// <param name="patientId">The unique identifier of the patient whose appointments are to be retrieved.</param>
    /// <returns>A task that represents the asynchronous operation, containing an async cursor over the matching <see cref="PatientAppointment"/> documents.</returns>
    public Task<IAsyncCursor<PatientAppointment>> FindByPatientIdAsync(ObjectId patientId)
    {
        return appointmentRepository.FindByPatientIdAsync(patientId);
    }

    /// <summary>
    /// Retrieves all patient appointments associated with the specified location.
    /// </summary>
    /// <param name="location">The location used to filter the patient appointments.</param>
    /// <returns>A task that represents the asynchronous operation, containing a list of patient appointments for the specified location.</returns>
    public async Task<List<PatientAppointment>> FindByLocation(PatientLocation location)
    {
        return await appointmentRepository.FindByLocation(location);
    }


    /// <summary>
    /// Deletes all appointments associated with the specified patient identifier, invalidates the appointments cache, and records the action in the audit log.
    /// </summary>
    /// <param name="id">The unique identifier of the patient whose appointments will be deleted.</param>
    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);
    }

    /// <summary>
    /// Updates multiple appointment records by replacing the specified <paramref name="oldId"/> with the new <paramref name="id"/>, scoped by the given <paramref name="nameId"/>. Delegates the operation to the underlying appointment repository.
    /// </summary>
    /// <param name="nameId">The identifier used to scope which appointment records are affected by the update.</param>
    /// <param name="id">The new ObjectId that will replace the existing one in the matching records.</param>
    /// <param name="oldId">The current ObjectId to be replaced in the matching records.</param>
    public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId)
    {
        await appointmentRepository.UpdateManyObjectId(nameId, id, oldId);
    }

    /// <summary>
    /// Broadcasts a patient appointment operation to all subscribers associated with the appointment's locations. Iterates through each resource group and location, resolving the unit and point of care, and dispatches a fire-and-forget message to every matching subscriber. Skips locations with missing unit/bed data and silently ignores unresolved unit or point-of-care lookups.
    /// </summary>
    /// <param name="appointment">The patient appointment whose resource groups and locations will be broadcast to subscribers.</param>
    /// <param name="operationType">The optional operation type describing the change performed on the appointment; passed along to the subscriber message.</param>
    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);
                    }
    }
}