using System.Collections.Generic;

using Hims.Shared.UserModels.Menu;

namespace Hims.Api.Controllers
{
    using System;
    using System.Globalization;
    using System.Linq;
    using System.Threading.Tasks;

    using Domain.Configurations;
    using Domain.Helpers;
    using Domain.Services;
    using FireSharp;
    using FireSharp.Config;
    using FireSharp.Interfaces;
    using Hangfire;
    using Hims.Api.Hubs;
    using Hims.Api.Models;
    using Hims.Api.Models.PushNotifications.Service;
    using Hims.Api.Models.RealTime;
    using Hims.Domain.Entities;
    using Hims.Shared.UserModels.Common;
    using Hims.Shared.UserModels.Queue;
    using Hims.Shared.UserModels.Queue.HelperModels;
    using IdentityModel.Client;

    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.SignalR;
    using Models.Account;

    using Senders;

    using Shared.DataFilters;
    using Shared.EntityModels;
    using Shared.Library;
    using Shared.Library.Enums;
    using Shared.UserModels.Filters;

    using Utilities;

    using AppointmentCheck = Shared.UserModels.AppointmentCheck;

    // ReSharper disable StyleCop.SA1126

    /// <inheritdoc />
    /// <summary>
    /// The account controller.
    /// </summary>
    [Route("api/queue")]
    [AllowAnonymous]
    [Consumes("application/json")]
    [Produces("application/json")]
    public class QueueController : BaseController
    {
        /// <summary>
        /// The account services.
        /// </summary>
        private readonly IQueueService service;

        private readonly IAppointmentCheckService checkService;

        /// <summary>
        /// The communication.
        /// </summary>
        private readonly IHubContext<CommunicationHub> communication;

        private readonly INotificationService notificationService;

        private readonly IFirebaseClient client;

        /// <summary>
        /// The application configuration.
        /// </summary>
        private readonly IApplicationConfiguration applicationConfiguration;

        /// <inheritdoc />
        public QueueController(
            IQueueService service,
            IAppointmentCheckService checkService,
            IApplicationConfiguration applicationConfiguration,
            IHubContext<CommunicationHub> communication,
            INotificationService notificationService
        )
        {
            this.applicationConfiguration = applicationConfiguration;
            this.service = service;
            this.checkService = checkService;
            this.communication = communication;
            this.notificationService = notificationService;

            this.client = client = new FirebaseClient(new FirebaseConfig
            {
                AuthSecret = this.applicationConfiguration.FirebaseAuthKey,
                BasePath = this.applicationConfiguration.FirebaseDatabase,
            });
        }

        /// <summary>
        /// To logout from application.
        /// </summary>
        /// <param name="model"></param>
        /// <param name="header"></param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        /// <remarks>
        /// ### REMARKS ###
        /// The following codes are returned
        /// - 200 - Logged out successfully.
        /// - 500 - Problem with Server side code.
        /// </remarks>
        [HttpPost]
        [Route("fetch")]
        [ProducesResponseType(typeof(string), 200)]
        [ProducesResponseType(500)]
        public async Task<ActionResult> FetchQueueAsync([FromBody] RequestFetchModel model, [FromHeader] LocationHeader header)
        {
            var response = await this.FetchQueueHelper(model, header);
            return Ok(response);
        }

        private async Task<GenericResponse> FetchQueueHelper(RequestFetchModel model, LocationHeader header)
        {
            try
            {
                var locationId = Convert.ToInt32(header.LocationId);
                var providers = await this.service.GetDoctorsAsync(locationId, model.ProviderId, model.AppointmentIds, model.IsMetaInfoOnly ?? false).ConfigureAwait(false);
                var providerIds = providers.Select(x => x.ProviderId).ToList();

                var mins30 = new TimeSpan(0, 30, 0);
                var extraParams = new QueueReceiveModel
                {
                    StartTime = DateTime.Now.TimeOfDay.Subtract(mins30),
                    EndTime = DateTime.Now.TimeOfDay.Add(mins30),
                    IsMetaInfoOnly = model.IsMetaInfoOnly // true from appointments, false from queue page
                };

                //if (providerIds.Count > 0)
                //{
                //    extraParams.StartTime = DateTime.Now.TimeOfDay.Subtract(new TimeSpan(3, 0, 0)); // 3 Hours
                //    await this.service.UpdateExpiredPatientsAsync(locationId, providerIds, extraParams);
                //}

                var patients = await this.service.GetPatientsAsync(locationId, providerIds, extraParams).ConfigureAwait(false);
                foreach (var provider in providers)
                {
                    provider.Patients = patients.Where(x => x.ProviderId == provider.ProviderId).OrderBy(x => x.AppointmentTime).ToList();
                    foreach (var patient in provider.Patients)
                    {
                        patient.AppointmentTimeString = Convert.ToDateTime(DateTime.Now.ToString("yyyy-MM-dd")).Add(patient.AppointmentTime).ToString("hh:mm tt");
                    }

                    if (provider.Patients.Count > 0)
                    {
                        var firstAppointmentTime = Convert.ToDateTime(DateTime.Now.ToString("yyyy-MM-dd")).Add(provider.Patients.First().AppointmentTime).ToString("HH:mm");

                        // If Waiting priority reached (star count)
                        provider.Patients = provider.Patients.OrderBy(x => x.WaitingCount).ThenBy(x => x.TokenNumber).ToList();

                        var waitingPatient = provider.Patients.Where(x => x.QueueStatusId == 4).FirstOrDefault(x => x.WaitingPriority == 0);
                        int escapeAppointmentId = 0;
                        Shared.UserModels.Queue.PatientModel currentPatient = null;
                        if (waitingPatient != null)
                        {
                            waitingPatient.QueueStatusId = 3;
                            waitingPatient.QueueStatus = "Next";
                            escapeAppointmentId = waitingPatient.AppointmentId;
                            currentPatient = waitingPatient;
                        }
                        else
                        {
                            var firstPatient = provider.Patients.FirstOrDefault(x => x.QueueStatusId == 4);
                            if (firstPatient != null)
                            {
                                firstPatient.QueueStatusId = 3;
                                firstPatient.QueueStatus = "Next";
                                escapeAppointmentId = firstPatient.AppointmentId;
                                currentPatient = firstPatient;
                            }
                        }

                        var remaningPatients = provider.Patients.Where(x => x.AppointmentId != escapeAppointmentId);
                        await this.RealTimeSynctFireAndForget(new RealTimeModel
                        {
                            AppointmentId = escapeAppointmentId,
                            StatusId = 3,
                            CurrentPatient = currentPatient,
                            FirstAppointmentTime = firstAppointmentTime,
                            Patients = remaningPatients.ToList(),
                            Ip = model.Ip,
                        });
                    }
                }

                if (model.IsMetaInfoOnly == null || model.IsMetaInfoOnly == false)
                {
                    return new GenericResponse
                    {
                        Status = GenericStatus.Success,
                        Data = providers
                    };
                }
                else
                {
                    var data = new List<MetaInfoModel>();
                    foreach (var provider in providers)
                    {
                        foreach (var patient in provider.Patients)
                        {
                            data.Add(new MetaInfoModel {
                                AppointmentId = patient.AppointmentId,
                                QueueStatus = patient.QueueStatus,
                                TokenNumber = patient.TokenNumber,
                                PatientName = patient.FullName
                            });
                        }
                    }
                    return new GenericResponse
                    {
                        Status = GenericStatus.Success,
                        Data = data
                    };
                }
            }
            catch (Exception ex)
            {
                return new GenericResponse
                {
                    Status = GenericStatus.Error,
                    Message = ex.Message,
                    Data = new List<DoctorModel>()
                };
            }
        }

        /// <summary>
        /// To logout from application.
        /// </summary>
        /// <param name="model"></param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        /// <remarks>
        /// ### REMARKS ###
        /// The following codes are returned
        /// - 200 - Logged out successfully.
        /// - 500 - Problem with Server side code.
        /// </remarks>
        [HttpPost]
        [Route("mobile-fetch")]
        [ProducesResponseType(typeof(string), 200)]
        [ProducesResponseType(500)]
        public async Task<ActionResult> MobileFetchQueueAsync([FromBody] MobileRequestFetchModel model)
        {
            try
            {
                var data = await this.service.MobileFetchAsync(model.PatientId).ConfigureAwait(false);
                data.AppointmentTimeString = Convert.ToDateTime(DateTime.Now.ToString("yyyy-MM-dd")).Add(data.AppointmentTime).ToString("hh:mm tt");

                if (data.LocationId > 0)
                {
                    var patients = await this.service.GetPatientsForQueueAsync(data.ProviderId, data.LocationId ?? 0).ConfigureAwait(false);

                    if (patients.Count() > 0)
                    {
                        patients = patients.OrderBy(x => x.AppointmentTime).ToList();
                        patients = patients.OrderBy(x => x.WaitingCount).ThenBy(x => x.TokenNumber);

                        var baseWaiting = 0D;
                        var baseCount = 0;
                        foreach (var x in patients)
                        {
                            x.TotalWaitingTime = baseWaiting;
                            x.TotalWaitingCount = baseCount;
                            baseWaiting += x.TotalWaitingTime ?? 0;
                            baseCount += x.TotalWaitingCount ?? 0;
                            ++baseCount;
                        }
                        var firstPatient = patients.FirstOrDefault(x => x.QueueStatusId == 4);
                        if (firstPatient != null && firstPatient.PatientId == model.PatientId)
                        {
                            data.QueueStatusId = 3;
                            data.QueueStatus = "Next";
                            data.TotalWaitingTime = firstPatient.TotalWaitingTime;
                            data.TotalWaitingCount = firstPatient.TotalWaitingCount;
                            await this.RealTimeSynctFireAndForget(
                                new RealTimeModel
                                {
                                    AppointmentId = firstPatient.AppointmentId,
                                    StatusId = 3
                                });
                        }
                        else
                        {
                            firstPatient = patients.FirstOrDefault(x => x.PatientId == model.PatientId);
                            if (firstPatient != null)
                            {
                                data.TotalWaitingTime = firstPatient.TotalWaitingTime;
                                data.TotalWaitingCount = firstPatient.TotalWaitingCount;
                            }
                        }
                    }
                }

                return Ok(new GenericResponse
                {
                    Status = GenericStatus.Success,
                    Data = data
                });
            }
            catch (Exception)
            {
                return Ok(new GenericResponse
                {
                    Status = GenericStatus.Error,
                    Message = "No Appointment is available for current date"
                });
            }
        }

        /// <summary>
        /// To logout from application.
        /// </summary>
        /// <param name="model"></param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        /// <remarks>
        /// ### REMARKS ###
        /// The following codes are returned
        /// - 200 - Logged out successfully.
        /// - 500 - Problem with Server side code.
        /// </remarks>
        [HttpPost]
        [Route("appointments")]
        [ProducesResponseType(typeof(string), 200)]
        [ProducesResponseType(500)]
        public async Task<ActionResult> AppointmentsStatusAsync([FromBody] AppointmentActionModel model)
        {
            var appointments = new List<AppointmentStatusModel>();
            try
            {
                appointments = (await this.service.GetAppointmentsAsync(model.ProviderId, model.LocationId).ConfigureAwait(false)).ToList();

                if (appointments.Count() <= 0) return Ok(appointments);

                // If Waiting priority reached (star count)
                var waitingPatient = appointments.Where(x => x.QueueStatusId == 4).FirstOrDefault(x => x.WaitingPriority == 0);
                if (waitingPatient != null)
                {
                    waitingPatient.QueueStatusId = 3;
                    waitingPatient.QueueStatus = "Next";
                    await this.RealTimeSynctFireAndForget(new RealTimeModel
                    {
                        AppointmentId = waitingPatient.AppointmentId,
                        StatusId = 3
                    });
                }
                else
                {
                    var firstPatient = appointments.OrderBy(x => x.WaitingCount).ThenBy(x => x.TokenNumber).FirstOrDefault(x => x.QueueStatusId == 4);
                    if (firstPatient != null)
                    {
                        firstPatient.QueueStatusId = 3;
                        firstPatient.QueueStatus = "Next";
                        await this.RealTimeSynctFireAndForget(new RealTimeModel
                        {
                            AppointmentId = firstPatient.AppointmentId,
                            StatusId = 3
                        });
                    }
                }
            }
            catch (Exception ex)
            {
                // ignore
                return Ok(appointments);
            }
            return Ok(appointments);
        }

        /// <summary>
        /// To logout from application.
        /// </summary>
        /// <param name="model"></param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        /// <remarks>
        /// ### REMARKS ###
        /// The following codes are returned
        /// - 200 - Logged out successfully.
        /// - 500 - Problem with Server side code.
        /// </remarks>
        [HttpPost]
        [Route("call-next-anonmous")]
        [ProducesResponseType(typeof(string), 200)]
        [ProducesResponseType(500)]
        public async Task<ActionResult> CallNextAnonmousAsync([FromBody] CallNextAnonmousModel model, [FromHeader] LocationHeader header)
        {
            var createdBy = Convert.ToInt32(header.CreatedBy);
            var requestModel = new RequestFetchModel
            {
                ProviderId = model.ProviderId,
                IsMetaInfoOnly = true,
                Ip = model.Ip
            };
            var appointmentResponse = await this.FetchQueueHelper(requestModel, header);
            if (appointmentResponse.Status == GenericStatus.Success)
            {
                var nextAppointment = ((List<MetaInfoModel>)appointmentResponse.Data).Where(x => x.QueueStatus == "Next");
                if(nextAppointment.Any())
                {
                    var next = nextAppointment.First();
                    var actionModel = new ActionModel
                    {
                        AppointmentId = next.AppointmentId,
                        CreatedBy = createdBy,
                        UserId = createdBy,
                        PatientName = next.PatientName,
                        TokenNumber = next.TokenNumber
                    };
                    var response = await this.CallPatientHelper(actionModel);
                    return Ok(response);
                } else
                {
                    return Ok(new GenericResponse
                    {
                        Status = GenericStatus.Warning,
                    });
                }
            }

            return Ok(new GenericResponse
            {
                Status = GenericStatus.Error,
            });
        }

        /// <summary>
        /// To logout from application.
        /// </summary>
        /// <param name="model"></param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        /// <remarks>
        /// ### REMARKS ###
        /// The following codes are returned
        /// - 200 - Logged out successfully.
        /// - 500 - Problem with Server side code.
        /// </remarks>
        [HttpPost]
        [Route("callout")]
        [ProducesResponseType(typeof(string), 200)]
        [ProducesResponseType(500)]
        public async Task<ActionResult> CallPatientAsync([FromBody] ActionModel model)
        {
            var response = await this.CallPatientHelper(model);
            return Ok(response);
        }

        private async Task<GenericResponse> CallPatientHelper(ActionModel model)
        {
            try
            {
                var appointment = await this.service.GetAppointmentAsync(model.AppointmentId).ConfigureAwait(false);
                if (model.TokenNumber == null)
                {
                    model.TokenNumber = appointment.TokenNumber;
                    model.PatientName = appointment.FullName;
                }
                // var providerLocationId = await this.service.GetProviderLocationIdByAppointmentId(model.AppointmentId).ConfigureAwait(false);
                // var locationId = await this.providerService.FindLocationIdByProviderLocationId(providerLocationId).ConfigureAwait(false);
                var userCubicle = await this.service.GetUserCubicle(model.CreatedBy ?? 0);

                //var availableCubicles = await this.service.GetFreeCubicleId(appointment.LocationId ?? 0, model.AppointmentId);
                //var emptyCubicle = availableCubicles.Where(x => x.QueueStatusId == null).Count();
                //if (availableCubicles.Count() <= 0 || (model.ActionType == "StartEncounter" && emptyCubicle <= 0))
                //{
                //    return Ok(new GenericResponse
                //    {
                //        Status = GenericStatus.Warning
                //    });
                //}

                await this.service.ReducePriorityAsync(appointment.ProviderId, appointment.LocationId ?? 0);

                // int? callingCubicleId = 0;
                if (model.CallingAppointmentId > 0)
                {
                    var waitResponse = await this.service.WaitPatientAsync(model.CallingAppointmentId ?? 0).ConfigureAwait(false);
                    // callingCubicleId = await this.service.GetCallingCubicleId(model.CallingAppointmentId ?? 0);
                    // Send notification to calling patient since doctor has click on next to call another patient
                    var callingtBody = "You are pushed back to waiting state";
                    var callingReason = "Patient did not respond to calling";
                    BackgroundJob.Enqueue(() => this.NotificationsToAppointmentFireAndForget(model.CallingAppointmentId ?? 0, callingtBody, callingReason));
                    if (waitResponse > 0)
                    {
                        await this.RealTimeSynctFireAndForget(new RealTimeModel
                        {
                            AppointmentId = model.CallingAppointmentId ?? 0,
                            StatusId = 4
                        });
                    }
                }

                //var availableCubicle = availableCubicles.FirstOrDefault(x => x.QueueStatusId == null);
                //if (availableCubicle == null)
                //{
                //    availableCubicle = availableCubicles.OrderBy(x => x.CubicleId).FirstOrDefault(x => x.QueueStatusId == 1);
                //    if (callingCubicleId != 0 && callingCubicleId != null)
                //    {
                //        availableCubicle.CubicleId = callingCubicleId ?? 0;
                //    }
                //}

                if (appointment.QueueStatusId == 5)
                {
                    return new GenericResponse
                    {
                        Status = GenericStatus.Warning,
                        Message = "Queue is already completed for this appointment"
                    };
                }
                var response = await this.service.CallPatientAsync(model.AppointmentId, userCubicle.CubicleId ?? 0).ConfigureAwait(false);
                if (response > 0)
                {
                    await this.checkService.InsertAsync(new AppointmentCheck.InsertModel
                    {
                        AppointmentId = model.AppointmentId,
                        Action = "doctor_calling",
                        CreatedBy = model.CreatedBy,
                        UserId = model.UserId
                    });
                    await this.RealTimeSynctFireAndForget(
                        new RealTimeModel
                        {
                            AppointmentId = model.AppointmentId,
                            StatusId = 1,
                            CubicleName = userCubicle.CubicleName
                        });
                }
                // send notification to calling patient
                var appointmentBody = "Doctor is calling you to" + userCubicle.CubicleName;
                var appointmentReason = "Current Queue Token";
                BackgroundJob.Enqueue(() => this.NotificationsToAppointmentFireAndForget(model.AppointmentId, appointmentBody, appointmentReason));

                var providerId = await this.service.GetProviderIdByAppointmentId(model.AppointmentId).ConfigureAwait(false);
                var reason = "Some Patient called inside doctor room, Queue Status has been changed";
                BackgroundJob.Enqueue(() => this.NotificationsToPatientsFireAndForget(providerId, appointment.LocationId ?? 0, reason));

                var communicationModel = new CommunicationMessageAlt
                {
                    Type = 2,
                    GroupName = "QueueManagement",
                    PatientName = model.PatientName,
                    TokenNumber = model.TokenNumber ?? 0,
                    CubicleName = userCubicle.CubicleName
                };
                var appointmentIds = new List<int> { model.AppointmentId };
                if (model.CallingAppointmentId != null)
                {
                    appointmentIds.Add(model.CallingAppointmentId ?? 0);
                }
                // this.WebNotificationsFireAndForget(appointmentIds, communicationModel);
                await this.communication.Clients.Group("QueueManagement").SendAsync("CommunicationGroup", communicationModel).ConfigureAwait(false);
                return new GenericResponse
                {
                    Status = response > 0 ? GenericStatus.Success : GenericStatus.Error
                };
            }
            catch (Exception ex)
            {
                return new GenericResponse
                {
                    Status = GenericStatus.Error,
                    Message = ex.Message
                };
            }
        }

        /// <summary>
        /// To logout from application.
        /// </summary>
        /// <param name="model"></param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        /// <remarks>
        /// ### REMARKS ###
        /// The following codes are returned
        /// - 200 - Logged out successfully.
        /// - 500 - Problem with Server side code.
        /// </remarks>
        [HttpPost]
        [Route("start")]
        [ProducesResponseType(typeof(string), 200)]
        [ProducesResponseType(500)]
        public async Task<ActionResult> StartEncounterAsync([FromBody] ActionModel model)
        {
            try
            {
                var response = await this.service.CheckInAsync(model.AppointmentId).ConfigureAwait(false);
                if (response == -1)
                {
                    return Ok(new GenericResponse
                    {
                        Status = GenericStatus.Warning,
                        Message = "Patient is not checked in"
                    });
                }
                if (response > 0)
                {
                    await this.checkService.InsertAsync(new AppointmentCheck.InsertModel
                    {
                        AppointmentId = model.AppointmentId,
                        Action = "in_room",
                        CreatedBy = model.CreatedBy,
                        UserId = model.UserId
                    });
                    await this.RealTimeSynctFireAndForget(new RealTimeModel
                    {
                        AppointmentId = model.AppointmentId,
                        StatusId = 2
                    });
                }
                var communicationModel = new CommunicationMessageAlt
                {
                    Type = 1,
                    GroupName = "QueueManagement"
                };
                var appointmentIds = new List<int> { model.AppointmentId };
                // this.WebNotificationsFireAndForget(appointmentIds, communicationModel);

                await this.communication.Clients.Group("QueueManagement").SendAsync("CommunicationGroup", communicationModel).ConfigureAwait(false);

                return Ok(new GenericResponse
                {
                    Status = response > 0 ? GenericStatus.Success : GenericStatus.Error
                });
            }
            catch (Exception ex)
            {
                return Ok(new GenericResponse
                {
                    Status = GenericStatus.Error,
                    Message = ex.Message
                });
            }
        }

        /// <summary>
        /// To logout from application.
        /// </summary>
        /// <param name="model"></param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        /// <remarks>
        /// ### REMARKS ###
        /// The following codes are returned
        /// - 200 - Logged out successfully.
        /// - 500 - Problem with Server side code.
        /// </remarks>
        [HttpPost]
        [Route("check-in")]
        [ProducesResponseType(typeof(string), 200)]
        [ProducesResponseType(500)]
        public async Task<ActionResult> CheckInPatientAsync([FromBody] ActionModel model)
        {
            try
            {
                var response = await this.service.CheckInAsync(model.AppointmentId).ConfigureAwait(true);
                if (response == -1)
                {
                    return Ok(new GenericResponse
                    {
                        Status = GenericStatus.Error,
                        Message = "Record not found"
                    });
                }

                if (response > 0)
                {
                    await this.checkService.InsertAsync(new AppointmentCheck.InsertModel
                    {
                        AppointmentId = model.AppointmentId,
                        Action = "start_encounter",
                        CreatedBy = model.CreatedBy,
                        UserId = model.UserId
                    });

                    await this.RealTimeSynctFireAndForget(new RealTimeModel
                    {
                        AppointmentId = model.AppointmentId,
                        StatusId = 2
                    });
                }
                var communicationModel = new CommunicationMessageAlt
                {
                    Type = 1,
                    GroupName = "QueueManagement"
                };
                var appointmentIds = new List<int> { model.AppointmentId };
                // this.WebNotificationsFireAndForget(appointmentIds, communicationModel);

                await this.communication.Clients.Group("QueueManagement").SendAsync("CommunicationGroup", communicationModel).ConfigureAwait(false);

                return Ok(new GenericResponse
                {
                    Status = response > 0 ? GenericStatus.Success : GenericStatus.Error
                });
            }
            catch (Exception ex)
            {
                return Ok(new GenericResponse
                {
                    Status = GenericStatus.Error,
                    Message = ex.Message
                });
            }
        }

        /// <summary>
        /// Basically a checkin
        /// </summary>
        /// <param name="model"></param>
        /// <param name="header"></param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        /// <remarks>
        /// ### REMARKS ###
        /// The following codes are returned
        /// - 200 - Logged out successfully.
        /// - 500 - Problem with Server side code.
        /// </remarks>
        [HttpPost]
        [Route("accept")]
        [ProducesResponseType(typeof(string), 200)]
        [ProducesResponseType(500)]
        public async Task<ActionResult> AcceptPatientAsync([FromBody] ActionModel model, [FromHeader] LocationHeader header)
        {
            try
            {
                var response = await this.service.AcceptAsync(model.AppointmentId).ConfigureAwait(false);
                if (response == -1)
                {
                    return Ok(new GenericResponse
                    {
                        Status = GenericStatus.Error,
                        Message = "Record not found"
                    });
                }
                if (response > 0)
                {
                    await this.checkService.InsertAsync(new AppointmentCheck.InsertModel
                    {
                        AppointmentId = model.AppointmentId,
                        Action = "check_in",
                        CreatedBy = model.CreatedBy,
                        UserId = model.UserId
                    });
                    await this.RealTimeSynctFireAndForget(new RealTimeModel
                    {
                        AppointmentId = model.AppointmentId,
                        StatusId = 4
                    });
                }

                var providerId = await this.service.GetProviderIdByAppointmentId(model.AppointmentId).ConfigureAwait(false);
                var locationId = Convert.ToInt32(header.LocationId);
                var reason = "Some Patient has been consultion with doctor, Queue Status has been changed";
                BackgroundJob.Enqueue(() => this.NotificationsToPatientsFireAndForget(providerId, locationId, reason));

                var communicationModel = new CommunicationMessageAlt
                {
                    Type = 1,
                    GroupName = "QueueManagement"
                };
                var appointmentIds = new List<int> { model.AppointmentId };
                // this.WebNotificationsFireAndForget(appointmentIds, communicationModel);

                await this.communication.Clients.Group("QueueManagement").SendAsync("CommunicationGroup", communicationModel).ConfigureAwait(false);
                return Ok(new GenericResponse
                {
                    Status = response > 0 ? GenericStatus.Success : GenericStatus.Error
                });
            }
            catch (Exception ex)
            {
                return Ok(new GenericResponse
                {
                    Status = GenericStatus.Error,
                    Message = ex.Message
                });
            }
        }

        /// <summary>
        /// To logout from application.
        /// </summary>
        /// <param name="model"></param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        /// <remarks>
        /// ### REMARKS ###
        /// The following codes are returned
        /// - 200 - Logged out successfully.
        /// - 500 - Problem with Server side code.
        /// </remarks>
        [HttpPost]
        [Route("cancel")]
        [ProducesResponseType(typeof(string), 200)]
        [ProducesResponseType(500)]
        public async Task<ActionResult> CancelPatientAsync([FromBody] ActionModel model)
        {
            try
            {
                var response = await this.service.CancelAsync(model.AppointmentId).ConfigureAwait(false);
                if (response > 0)
                {
                    await this.checkService.InsertAsync(new AppointmentCheck.InsertModel
                    {
                        AppointmentId = model.AppointmentId,
                        Action = "check_in_cancelled",
                        CreatedBy = model.CreatedBy,
                        UserId = model.UserId
                    });
                    await this.RealTimeSynctFireAndForget(new RealTimeModel
                    {
                        AppointmentId = model.AppointmentId,
                        StatusId = 7
                    });
                }
                var communicationModel = new CommunicationMessageAlt
                {
                    Type = 1,
                    GroupName = "QueueManagement"
                };
                var appointmentIds = new List<int> { model.AppointmentId };
                // this.WebNotificationsFireAndForget(appointmentIds, communicationModel);
                var callingtBody = "You are pushed back to waiting state";
                var callingReason = "Patient did not respond to calling";
                BackgroundJob.Enqueue(() => this.NotificationsToAppointmentFireAndForget(model.AppointmentId, callingtBody, callingReason));

                await this.communication.Clients.Group("QueueManagement").SendAsync("CommunicationGroup", communicationModel).ConfigureAwait(false);
                return Ok(new GenericResponse
                {
                    Status = response > 0 ? GenericStatus.Success : GenericStatus.Error
                });
            }
            catch (Exception ex)
            {
                return Ok(new GenericResponse
                {
                    Status = GenericStatus.Error,
                    Message = ex.Message
                });
            }
        }

        /// <summary>
        /// To logout from application.
        /// </summary>
        /// <param name="model"></param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        /// <remarks>
        /// ### REMARKS ###
        /// The following codes are returned
        /// - 200 - Logged out successfully.
        /// - 500 - Problem with Server side code.
        /// </remarks>
        [HttpPost]
        [Route("save-web-token")]
        [ProducesResponseType(typeof(string), 200)]
        [ProducesResponseType(500)]
        public async Task<ActionResult> SaveWebTokenAsync([FromBody] WebTokenModel model)
        {
            try
            {
                var response = await this.service.SaveWebTokenAsync(model).ConfigureAwait(false);
                return Ok(new GenericResponse
                {
                    Status = response > 0 ? GenericStatus.Success : GenericStatus.Error
                });
            }
            catch (Exception ex)
            {
                return Ok(new GenericResponse
                {
                    Status = GenericStatus.Error,
                    Message = ex.Message
                });
            }
        }

        /// <summary>
        /// To logout from application.
        /// </summary>
        /// <param name="model"></param>
        /// <param name="header"></param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        /// <remarks>
        /// ### REMARKS ###
        /// The following codes are returned
        /// - 200 - Logged out successfully.
        /// - 500 - Problem with Server side code.
        /// </remarks>
        [HttpPost]
        [Route("complete")]
        [ProducesResponseType(typeof(string), 200)]
        [ProducesResponseType(500)]
        public async Task<ActionResult> CompletePatientAsync([FromBody] ActionModel model, [FromHeader] LocationHeader header)
        {
            try
            {
                var response = await this.service.CompletePatientAsync(model.AppointmentId).ConfigureAwait(false);
                if (response == -1)
                {
                    return Ok(new GenericResponse
                    {
                        Status = GenericStatus.Error,
                        Message = "Record not found"
                    });
                }

                if (response != -1)
                {
                    await this.checkService.InsertAsync(new AppointmentCheck.InsertModel
                    {
                        AppointmentId = model.AppointmentId,
                        Action = "complete_encounter",
                        CreatedBy = model.CreatedBy,
                        UserId = model.UserId,
                        IgnoreForQueue = response == -2
                    });
                    await this.RealTimeSynctFireAndForget(new RealTimeModel
                    {
                        AppointmentId = model.AppointmentId,
                        StatusId = 5
                    });
                }
                var providerId = await this.service.GetProviderIdByAppointmentId(model.AppointmentId).ConfigureAwait(false);
                var locationId = Convert.ToInt32(header.LocationId);
                var reason = "Some Patient has been completed consultion with doctor, Queue Status has been changed";
                BackgroundJob.Enqueue(() => this.NotificationsToPatientsFireAndForget(providerId, locationId, reason));

                var communicationModel = new CommunicationMessageAlt
                {
                    Type = 1,
                    GroupName = "QueueManagement"
                };
                var appointmentIds = new List<int> { model.AppointmentId };
                await this.communication.Clients.Group("QueueManagement").SendAsync("CommunicationGroup", communicationModel).ConfigureAwait(false);
                return Ok(new GenericResponse
                {
                    Status = response > 0 ? GenericStatus.Success : GenericStatus.Error
                });
            }
            catch (Exception ex)
            {
                return Ok(new GenericResponse
                {
                    Status = GenericStatus.Error,
                    Message = ex.Message
                });
            }
        }
        /// <summary>
        /// To logout from application.
        /// </summary>
        /// <param name="model"></param>
        /// <param name="header"></param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        /// <remarks>
        /// ### REMARKS ###
        /// The following codes are returned
        /// - 200 - Logged out successfully.
        /// - 500 - Problem with Server side code.
        /// </remarks>
        [HttpPost]
        [Route("move-to-queue")]
        [ProducesResponseType(typeof(string), 200)]
        [ProducesResponseType(500)]
        public async Task<ActionResult> MoveToQueuePatientAsync([FromBody] ActionModel model, [FromHeader] LocationHeader header)
        {
            try
            {
                var response = await this.service.MoveToQueuePatientAsync(model.AppointmentId).ConfigureAwait(false);
                if (response == -1)
                {
                    return Ok(new GenericResponse
                    {
                        Status = GenericStatus.Error,
                        Message = "Record not found"
                    });
                }

                if (response != -1)
                {
                    await this.checkService.InsertAsync(new AppointmentCheck.InsertModel
                    {
                        AppointmentId = model.AppointmentId,
                        Action = "moved_to_queue",
                        CreatedBy = model.CreatedBy,
                        UserId = model.UserId
                    });
                    await this.RealTimeSynctFireAndForget(new RealTimeModel
                    {
                        AppointmentId = model.AppointmentId,
                        StatusId = 4
                    });
                }
                var communicationModel = new CommunicationMessageAlt
                {
                    Type = 1,
                    GroupName = "QueueManagement"
                };
                var appointmentIds = new List<int> { model.AppointmentId };
                await this.communication.Clients.Group("QueueManagement").SendAsync("CommunicationGroup", communicationModel).ConfigureAwait(false);
                return Ok(new GenericResponse
                {
                    Status = response > 0 ? GenericStatus.Success : GenericStatus.Error
                });
            }
            catch (Exception ex)
            {
                return Ok(new GenericResponse
                {
                    Status = GenericStatus.Error,
                    Message = ex.Message
                });
            }
        }
        public async Task NotificationsToAppointmentFireAndForget(int appointmentId, string body, string reason)
        {
            try
            {
                var deviceTokens = await this.service.GetAndroidDeviceTokenByAppointmentId(appointmentId).ConfigureAwait(false);
                if (deviceTokens.Count() > 0)
                {
                    foreach (var token in deviceTokens)
                    {
                        await this.notificationService.SendNotification(new Models.PushNotifications.NotificationModel
                        {
                            DeviceId = token,
                            IsAndroiodDevice = true,
                            Title = "Queue Management",
                            Body = body
                        },
                        new Models.PushNotifications.DataPayload
                        {
                            Type = "QM",
                            Do = "Queue Calling",
                            Reason = reason
                        }).ConfigureAwait(false);
                    }
                }
            }
            catch (Exception ex)
            {
                /* Ignore */
            }
        }

        private double GetDelay(List<Shared.UserModels.Queue.PatientModel> patients, Shared.UserModels.Queue.PatientModel currentPatient = null)
        {
            try
            {
                var list = new List<double>();
                var lastIndex = -1;
                var tailTime = 0D;

                for (var i = 0; i < patients.Count; i++)
                {
                    var item = patients.ElementAt(i);
                    if (string.IsNullOrEmpty(item.StartDate))
                    {
                        continue;
                    }

                    var startTime = new DateTime(2023, 1, 1, item.AppointmentTime.Hours, item.AppointmentTime.Minutes, 0);
                    var tokens = item.StartDate.Split(':');
                    var actualStartTime = new DateTime(2023, 1, 1, Convert.ToInt32(tokens[0]), Convert.ToInt32(tokens[1]), 0);
                    var delay = actualStartTime.Subtract(startTime).TotalMinutes;

                    if (!string.IsNullOrEmpty(item.CompleteDate))
                    {
                        tokens = item.CompleteDate.Split(':');
                        var actualCompleteTime = new DateTime(2023, 1, 1, Convert.ToInt32(tokens[0]), Convert.ToInt32(tokens[1]), 0);
                        var appointmentEndTime = new DateTime(2023, 1, 1, item.AppointmentEndTime.Hours, item.AppointmentEndTime.Minutes, 0);
                        var completedDifference = actualCompleteTime.Subtract(appointmentEndTime).TotalMinutes;
                        delay = completedDifference;
                    }

                    list.Add(delay);
                    lastIndex = i;
                }

                if (currentPatient == null)
                {
                    return 0;
                }

                if (lastIndex != 0)
                {
                    var lastElement = patients.ElementAt(lastIndex);
                    if (!string.IsNullOrEmpty(lastElement.CompleteDate))
                    {
                        var lastPatientStartTime = new DateTime(2023, 1, 1, lastElement.AppointmentTime.Hours, lastElement.AppointmentTime.Minutes, 0);
                        var tokens = lastElement.CompleteDate.Split(':');
                        var lastPatientCompleteTime = new DateTime(2023, 1, 1, Convert.ToInt32(tokens[0]), Convert.ToInt32(tokens[1]), 0);
                        if (lastPatientCompleteTime >= lastPatientStartTime)
                        {
                            var currentPatientAppointmentTime = new DateTime(2023, 1, 1, currentPatient.AppointmentTime.Hours, currentPatient.AppointmentTime.Minutes, 0);
                            if (lastPatientCompleteTime > currentPatientAppointmentTime)
                            {
                                var overlappedDifference = lastPatientCompleteTime.Subtract(currentPatientAppointmentTime).TotalMinutes;
                                list.Add(overlappedDifference);
                                var now = new DateTime(2023, 1, 1, DateTime.Now.Hour, DateTime.Now.Minute, 0);
                                var currentPatientStartTime = new DateTime(2023, 1, 1, currentPatient.AppointmentTime.Hours, currentPatient.AppointmentTime.Minutes, 0);
                                var toCompare = currentPatientStartTime > lastPatientCompleteTime ? currentPatientStartTime : lastPatientCompleteTime;
                                tailTime = now.Subtract(toCompare).TotalMinutes;
                            }
                            else
                            {
                                list.Add(0);
                            }
                        }
                    }
                }

                return list.Last() + tailTime;
            }
            catch (Exception)
            {
                return 0;
            }
        }

        private bool IsTimeGap(List<Shared.UserModels.Queue.PatientModel> patients)
        {
            try
            {
                if (patients.Count == 0)
                {
                    return false;
                }
                var first = patients.First().AppointmentTime;
                var last = patients.Last().AppointmentEndTime;
                var startTime = new DateTime(2023, 1, 1, first.Hours, first.Minutes, 0);
                var endTime = new DateTime(2023, 1, 1, last.Hours, last.Minutes, 0);
                var totalMinutes = endTime.Subtract(startTime).TotalMinutes;

                var actualMinutes = 0D;
                foreach (var item in patients)
                {
                    var currentStartTime = new DateTime(2023, 1, 1, item.AppointmentTime.Hours, item.AppointmentTime.Minutes, 0);
                    var currentEndTime = new DateTime(2023, 1, 1, item.AppointmentEndTime.Hours, item.AppointmentEndTime.Minutes, 0);
                    actualMinutes += endTime.Subtract(startTime).TotalMinutes;
                }

                return actualMinutes < totalMinutes;
            }
            catch (Exception ex)
            {
                return true;
            }
        }

        private async Task RealTimeSynctFireAndForget(RealTimeModel model)
        {
            try
            {
                var data = new Dictionary<string, object>();
                if (model.Patients != null)
                {
                    var counter = model.CurrentPatient != null ? 1 : 0;
                    var isQueueStarted = model.Patients.Any(x => !string.IsNullOrEmpty(x.StartDate));

                    var remainingPatients = model.Patients.Where(x => x.QueueStatusId != 5);
                    for (var i = 0; i < remainingPatients.Count(); i++)
                    {
                        var element = remainingPatients.ElementAt(i);
                        var apointmentsCurrentToTillNow = model.Patients.Where(x =>
                            (x.AppointmentTime >= DateTime.Now.TimeOfDay || x.AppointmentEndTime >= DateTime.Now.TimeOfDay)
                            && x.AppointmentTime <= element.AppointmentTime).OrderBy(x => x.AppointmentTime).ToList();
                        var isTimeGap = IsTimeGap(apointmentsCurrentToTillNow);
                        var apointmentsTillNow = model.Patients.Where(x => x.AppointmentTime <= element.AppointmentTime).OrderBy(x => x.AppointmentTime).ToList();
                        var callingPatients = remainingPatients.Where(x => x.QueueStatusId == 1);
                        var currentPatient = callingPatients.Count() > 0 ? callingPatients.First() : model.CurrentPatient != null ? model.CurrentPatient : null;
                        var delay = !isTimeGap ? GetDelay(apointmentsTillNow.ToList(), currentPatient) : 0D;
                        TimeSpan span = TimeSpan.FromMinutes(delay);
                        string delayString = delay > 60 ? string.Format("{0:D1}H:{1:D2}M", span.Hours, span.Minutes) : delay + " minute(s)";
                        var waitStatus = !isQueueStarted
                                    ? "Queue Not Started" : delay <= 0 || isTimeGap
                                        ? "On Time" : delay > 0
                                            ? delayString + " delay" : delayString + " early";
                        data.Add(
                            element.AppointmentId.ToString(),
                            new
                            {
                                statusId = element.QueueStatusId,
                                peopleAhead = counter,
                                waitStatus = waitStatus,
                                tokenNumber = element.TokenNumber,
                                IP = model.Ip,
                                starCount = element.WaitingCount
                            }
                        );
                        if (element.QueueStatusId != 7)
                        {
                            ++counter;
                        }
                    }
                }

                if (model.AppointmentId > 0)
                {
                    if (!string.IsNullOrEmpty(model.CubicleName))
                    {
                        data.Add(model.AppointmentId.ToString(), new
                        {
                            statusId = model.StatusId,
                            cubicleName = model.CubicleName,
                            starCount = model.CurrentPatient != null ? model.CurrentPatient.WaitingCount ?? 0 : 0
                        });
                    }
                    else
                    {
                        data.Add(model.AppointmentId.ToString(), new
                        {
                            statusId = model.StatusId,
                            starCount = model.CurrentPatient != null ? model.CurrentPatient.WaitingCount ?? 0 : 0
                        });
                    }
                }

                await this.client.UpdateAsync("appointments", data);
            }
            catch (Exception ex)
            {
                /* Ignore */
            }
        }

        public async Task NotificationsToPatientsFireAndForget(int providerId, int locationId, string reason)
        {
            try
            {
                var deviceTokens = await this.service.GetPatientsAndroidDeviceTokensByProviderId(providerId, locationId).ConfigureAwait(false);
                if (deviceTokens.Count() > 0)
                {
                    foreach (var token in deviceTokens)
                    {
                        await this.notificationService.SendNotification(new Models.PushNotifications.NotificationModel
                        {
                            DeviceId = token,
                            IsAndroiodDevice = true,
                            Title = "Queue Management",
                            Body = "Your Queue has changed"
                        },
                        new Models.PushNotifications.DataPayload
                        {
                            Type = "QM",
                            Do = "Refresh",
                            Reason = reason,
                        }).ConfigureAwait(false);
                    }
                }
            }
            catch (Exception ex)
            {
                /* Ignore */
            }
        }

        public async Task WebNotificationsFireAndForget(List<int> appointmentIds, CommunicationMessageAlt communicationModel)
        {
            try
            {
                var webTokens = await this.service.GetQueueWebTokensId(appointmentIds).ConfigureAwait(false);
                if (webTokens.Count() > 0)
                {
                    foreach (var token in webTokens)
                    {
                        await this.notificationService.SendWebNotification(new Models.PushNotifications.NotificationModel
                        {
                            IsAndroiodDevice = true,
                            Title = "Queue Management",
                            Body = "Queue Management got some updates",
                            DeviceId = token
                        }, communicationModel).ConfigureAwait(false);
                    }
                }
            }
            catch (Exception ex)
            {
                /* Ignore */
            }
        }

    }
}