internal async Task ProcessJoinRequestAsync(LoRaRequest request)
        {
            LoRaDevice loRaDevice = null;
            var        loraRegion = request.Region;

            try
            {
                var timeWatcher       = request.GetTimeWatcher();
                var processingTimeout = timeWatcher.GetRemainingTimeToJoinAcceptSecondWindow() - TimeSpan.FromMilliseconds(100);
                using var joinAcceptCancellationToken = new CancellationTokenSource(processingTimeout > TimeSpan.Zero ? processingTimeout : TimeSpan.Zero);

                var joinReq = (LoRaPayloadJoinRequest)request.Payload;

                var devEui = joinReq.DevEUI;

                using var scope = this.logger.BeginDeviceScope(devEui);

                this.logger.LogInformation("join request received");

                if (this.concentratorDeduplication.CheckDuplicateJoin(request) is ConcentratorDeduplicationResult.Duplicate)
                {
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.DeduplicationDrop);
                    // we do not log here as the concentratorDeduplication service already does more detailed logging
                    return;
                }

                loRaDevice = await this.deviceRegistry.GetDeviceForJoinRequestAsync(devEui, joinReq.DevNonce);

                if (loRaDevice == null)
                {
                    request.NotifyFailed(devEui.ToString(), LoRaDeviceRequestFailedReason.UnknownDevice);
                    // we do not log here as we assume that the deviceRegistry does a more informed logging if returning null
                    return;
                }

                if (loRaDevice.AppKey is null)
                {
                    this.logger.LogError("join refused: missing AppKey for OTAA device");
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.InvalidJoinRequest);
                    return;
                }

                var appKey = loRaDevice.AppKey.Value;

                this.joinRequestCounter?.Add(1);

                if (loRaDevice.AppEui != joinReq.AppEui)
                {
                    this.logger.LogError("join refused: AppEUI for OTAA does not match device");
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.InvalidJoinRequest);
                    return;
                }

                if (!joinReq.CheckMic(appKey))
                {
                    this.logger.LogError("join refused: invalid MIC");
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.JoinMicCheckFailed);
                    return;
                }

                // Make sure that is a new request and not a replay
                if (loRaDevice.DevNonce is { } devNonce&& devNonce == joinReq.DevNonce)
                {
                    if (string.IsNullOrEmpty(loRaDevice.GatewayID))
                    {
                        this.logger.LogInformation("join refused: join already processed by another gateway");
                    }
                    else
                    {
                        this.logger.LogError("join refused: DevNonce already used by this device");
                    }

                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.JoinDevNonceAlreadyUsed);
                    return;
                }

                // Check that the device is joining through the linked gateway and not another
                if (!loRaDevice.IsOurDevice)
                {
                    this.logger.LogInformation("join refused: trying to join not through its linked gateway, ignoring join request");
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.HandledByAnotherGateway);
                    return;
                }

                var netId    = this.configuration.NetId;
                var appNonce = new AppNonce(RandomNumberGenerator.GetInt32(toExclusive: AppNonce.MaxValue + 1));
                var appSKey  = OTAAKeysGenerator.CalculateAppSessionKey(appNonce, netId, joinReq.DevNonce, appKey);
                var nwkSKey  = OTAAKeysGenerator.CalculateNetworkSessionKey(appNonce, netId, joinReq.DevNonce, appKey);
                var address  = RandomNumberGenerator.GetInt32(toExclusive: DevAddr.MaxNetworkAddress + 1);
                // The 7 LBS of the NetID become the NwkID of a DevAddr:
                var devAddr = new DevAddr(unchecked ((byte)netId.NetworkId), address);

                var oldDevAddr = loRaDevice.DevAddr;

                if (!timeWatcher.InTimeForJoinAccept())
                {
                    this.receiveWindowMisses?.Add(1);
                    // in this case it's too late, we need to break and avoid saving twins
                    this.logger.LogInformation("join refused: processing of the join request took too long, sending no message");
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.ReceiveWindowMissed);
                    return;
                }

                var updatedProperties = new LoRaDeviceJoinUpdateProperties
                {
                    DevAddr            = devAddr,
                    NwkSKey            = nwkSKey,
                    AppSKey            = appSKey,
                    AppNonce           = appNonce,
                    DevNonce           = joinReq.DevNonce,
                    NetId              = netId,
                    Region             = request.Region.LoRaRegion,
                    PreferredGatewayID = this.configuration.GatewayID,
                };

                if (loRaDevice.ClassType == LoRaDeviceClassType.C)
                {
                    updatedProperties.SavePreferredGateway = true;
                    updatedProperties.SaveRegion           = true;
                    updatedProperties.StationEui           = request.StationEui;
                }

                DeviceJoinInfo deviceJoinInfo = null;
                if (request.Region.LoRaRegion == LoRaRegionType.CN470RP2)
                {
                    if (request.Region.TryGetJoinChannelIndex(request.RadioMetadata.Frequency, out var channelIndex))
                    {
                        updatedProperties.CN470JoinChannel = channelIndex;
                        deviceJoinInfo = new DeviceJoinInfo(channelIndex);
                    }
                    else
                    {
                        this.logger.LogError("failed to retrieve the join channel index for device");
                    }
                }

                var deviceUpdateSucceeded = await loRaDevice.UpdateAfterJoinAsync(updatedProperties, joinAcceptCancellationToken.Token);

                if (!deviceUpdateSucceeded)
                {
                    this.logger.LogError("join refused: join request could not save twin");
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.IoTHubProblem);
                    return;
                }

                var windowToUse = timeWatcher.ResolveJoinAcceptWindowToUse();
                if (windowToUse is null)
                {
                    this.receiveWindowMisses?.Add(1);
                    this.logger.LogInformation("join refused: processing of the join request took too long, sending no message");
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.ReceiveWindowMissed);
                    return;
                }

                this.deviceRegistry.UpdateDeviceAfterJoin(loRaDevice, oldDevAddr);

                // Build join accept downlink message

                // Build the DlSettings fields that is a superposition of RX2DR and RX1DROffset field
                var dlSettings = new byte[1];

                if (loRaDevice.DesiredRX2DataRate.HasValue)
                {
                    if (request.Region.DRtoConfiguration.ContainsKey(loRaDevice.DesiredRX2DataRate.Value))
                    {
                        dlSettings[0] = (byte)((byte)loRaDevice.DesiredRX2DataRate & 0b00001111);
                    }
                    else
                    {
                        this.logger.LogError("twin RX2 DR value is not within acceptable values");
                    }
                }

                if (request.Region.IsValidRX1DROffset(loRaDevice.DesiredRX1DROffset))
                {
                    var rx1droffset = (byte)(loRaDevice.DesiredRX1DROffset << 4);
                    dlSettings[0] = (byte)(dlSettings[0] + rx1droffset);
                }
                else
                {
                    this.logger.LogError("twin RX1 offset DR value is not within acceptable values");
                }

                // The following DesiredRxDelay is different than the RxDelay to be passed to Serialize function
                // This one is a delay between TX and RX for any message to be processed by joining device
                // The field accepted by Serialize method is an indication of the delay (compared to receive time of join request)
                // of when the message Join Accept message should be sent
                var loraSpecDesiredRxDelay = RxDelay.RxDelay0;
                if (Enum.IsDefined(loRaDevice.DesiredRXDelay))
                {
                    loraSpecDesiredRxDelay = loRaDevice.DesiredRXDelay;
                }
                else
                {
                    this.logger.LogError("twin RX delay value is not within acceptable values");
                }

                var loRaPayloadJoinAccept = new LoRaPayloadJoinAccept(
                    netId,   // NETID 0 / 1 is default test
                    devAddr, // todo add device address management
                    appNonce,
                    dlSettings,
                    loraSpecDesiredRxDelay,
                    null);

                if (!loraRegion.TryGetDownstreamChannelFrequency(request.RadioMetadata.Frequency, upstreamDataRate: request.RadioMetadata.DataRate, deviceJoinInfo: deviceJoinInfo, downstreamFrequency: out var freq))
                {
                    this.logger.LogError("could not resolve DR and/or frequency for downstream");
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.InvalidUpstreamMessage);
                    return;
                }

                var joinAcceptBytes = loRaPayloadJoinAccept.Serialize(appKey);

                var rx1 = windowToUse is not ReceiveWindow2
                        ? new ReceiveWindow(loraRegion.GetDownstreamDataRate(request.RadioMetadata.DataRate, loRaDevice.ReportedRX1DROffset), freq)
                        : (ReceiveWindow?)null;

                var rx2 = new ReceiveWindow(loraRegion.GetDownstreamRX2DataRate(this.configuration.Rx2DataRate, null, deviceJoinInfo, this.logger),
                                            loraRegion.GetDownstreamRX2Freq(this.configuration.Rx2Frequency, deviceJoinInfo, this.logger));

                var downlinkMessage = new DownlinkMessage(joinAcceptBytes,
                                                          request.RadioMetadata.UpInfo.Xtime,
                                                          rx1,
                                                          rx2,
                                                          loRaDevice.DevEUI,
                                                          loraRegion.JoinAcceptDelay1,
                                                          loRaDevice.ClassType,
                                                          request.StationEui,
                                                          request.RadioMetadata.UpInfo.AntennaPreference);

                this.receiveWindowHits?.Add(1, KeyValuePair.Create(MetricRegistry.ReceiveWindowTagName, (object)windowToUse));
                _ = request.DownstreamMessageSender.SendDownstreamAsync(downlinkMessage);
                request.NotifySucceeded(loRaDevice, downlinkMessage);

                if (this.logger.IsEnabled(LogLevel.Debug))
                {
                    var jsonMsg = JsonConvert.SerializeObject(downlinkMessage);
                    this.logger.LogDebug($"{MacMessageType.JoinAccept} {jsonMsg}");
                }
                else
                {
                    this.logger.LogInformation("join accepted");
                }
            }
            catch (Exception ex) when
                (ExceptionFilterUtility.True(() => this.logger.LogError(ex, $"failed to handle join request. {ex.Message}", LogLevel.Error),
                                             () => this.unhandledExceptionCount?.Add(1)))
            {
                request.NotifyFailed(loRaDevice, ex);
                throw;
            }
        }
        public async Task <LoRaDeviceRequestProcessResult> ProcessRequestAsync(LoRaRequest request, LoRaDevice loRaDevice)
        {
            var timeWatcher = request.GetTimeWatcher();

            using (var deviceConnectionActivity = loRaDevice.BeginDeviceClientConnectionActivity())
            {
                if (deviceConnectionActivity == null)
                {
                    return(new LoRaDeviceRequestProcessResult(loRaDevice, request, LoRaDeviceRequestFailedReason.DeviceClientConnectionFailed));
                }

                var loraPayload = (LoRaPayloadData)request.Payload;

                var payloadFcnt = loraPayload.GetFcnt();

                uint payloadFcntAdjusted = LoRaPayload.InferUpper32BitsForClientFcnt(payloadFcnt, loRaDevice.FCntUp);
                Logger.Log(loRaDevice.DevEUI, $"converted 16bit FCnt {payloadFcnt} to 32bit FCnt {payloadFcntAdjusted}", LogLevel.Debug);

                var payloadPort          = loraPayload.GetFPort();
                var requiresConfirmation = loraPayload.IsConfirmed || loraPayload.IsMacAnswerRequired;

                LoRaADRResult loRaADRResult = null;

                var frameCounterStrategy = this.frameCounterUpdateStrategyProvider.GetStrategy(loRaDevice.GatewayID);
                if (frameCounterStrategy == null)
                {
                    Logger.Log(loRaDevice.DevEUI, $"failed to resolve frame count update strategy, device gateway: {loRaDevice.GatewayID}, message ignored", LogLevel.Error);
                    return(new LoRaDeviceRequestProcessResult(loRaDevice, request, LoRaDeviceRequestFailedReason.ApplicationError));
                }

                // Contains the Cloud to message we need to send
                IReceivedLoRaCloudToDeviceMessage cloudToDeviceMessage = null;

                // Leaf devices that restart lose the counter. In relax mode we accept the incoming frame counter
                // ABP device does not reset the Fcnt so in relax mode we should reset for 0 (LMIC based) or 1
                bool isFrameCounterFromNewlyStartedDevice = await this.DetermineIfFramecounterIsFromNewlyStartedDeviceAsync(loRaDevice, payloadFcntAdjusted, frameCounterStrategy);

                // Reply attack or confirmed reply
                // Confirmed resubmit: A confirmed message that was received previously but we did not answer in time
                // Device will send it again and we just need to return an ack (but also check for C2D to send it over)
                if (!ValidateRequest(request, isFrameCounterFromNewlyStartedDevice, payloadFcntAdjusted, loRaDevice, requiresConfirmation, out bool isConfirmedResubmit, out LoRaDeviceRequestProcessResult result))
                {
                    return(result);
                }

                var useMultipleGateways = string.IsNullOrEmpty(loRaDevice.GatewayID);

                try
                {
                    var bundlerResult = await this.TryUseBundler(request, loRaDevice, loraPayload, useMultipleGateways);

                    loRaADRResult = bundlerResult?.AdrResult;

                    if (bundlerResult?.PreferredGatewayResult != null)
                    {
                        this.HandlePreferredGatewayChanges(request, loRaDevice, bundlerResult);
                    }

                    if (loraPayload.IsAdrReq)
                    {
                        Logger.Log(loRaDevice.DevEUI, $"ADR ack request received", LogLevel.Debug);
                    }

                    // ADR should be performed before the deduplication
                    // as we still want to collect the signal info, even if we drop
                    // it in the next step
                    if (loRaADRResult == null && loraPayload.IsAdrEnabled)
                    {
                        loRaADRResult = await this.PerformADR(request, loRaDevice, loraPayload, payloadFcntAdjusted, loRaADRResult, frameCounterStrategy);
                    }

                    if (loRaADRResult?.CanConfirmToDevice == true || loraPayload.IsAdrReq)
                    {
                        // if we got an ADR result or request, we have to send the update to the device
                        requiresConfirmation = true;
                    }

                    if (useMultipleGateways)
                    {
                        // applying the correct deduplication
                        if (bundlerResult?.DeduplicationResult != null && !bundlerResult.DeduplicationResult.CanProcess)
                        {
                            // duplication strategy is indicating that we do not need to continue processing this message
                            Logger.Log(loRaDevice.DevEUI, $"duplication strategy indicated to not process message: {payloadFcnt}", LogLevel.Debug);
                            return(new LoRaDeviceRequestProcessResult(loRaDevice, request, LoRaDeviceRequestFailedReason.DeduplicationDrop));
                        }
                    }
                    else
                    {
                        // we must save class C devices regions in order to send c2d messages
                        if (loRaDevice.ClassType == LoRaDeviceClassType.C && request.Region.LoRaRegion != loRaDevice.LoRaRegion)
                        {
                            loRaDevice.UpdateRegion(request.Region.LoRaRegion, acceptChanges: false);
                        }
                    }

                    // if deduplication already processed the next framecounter down, use that
                    uint?fcntDown = loRaADRResult?.FCntDown != null ? loRaADRResult.FCntDown : bundlerResult?.NextFCntDown;

                    if (fcntDown.HasValue)
                    {
                        LogFrameCounterDownState(loRaDevice, fcntDown.Value);
                    }

                    // If it is confirmed it require us to update the frame counter down
                    // Multiple gateways: in redis, otherwise in device twin
                    if (requiresConfirmation)
                    {
                        fcntDown = await this.EnsureHasFcntDownAsync(loRaDevice, fcntDown, payloadFcntAdjusted, frameCounterStrategy);

                        // Failed to update the fcnt down
                        // In multi gateway scenarios it means the another gateway was faster than using, can stop now
                        if (fcntDown <= 0)
                        {
                            return(new LoRaDeviceRequestProcessResult(loRaDevice, request, LoRaDeviceRequestFailedReason.HandledByAnotherGateway));
                        }
                    }

                    var validFcntUp = isFrameCounterFromNewlyStartedDevice || (payloadFcntAdjusted > loRaDevice.FCntUp);
                    if (validFcntUp || isConfirmedResubmit)
                    {
                        if (!isConfirmedResubmit)
                        {
                            Logger.Log(loRaDevice.DevEUI, $"valid frame counter, msg: {payloadFcntAdjusted} server: {loRaDevice.FCntUp}", LogLevel.Debug);
                        }

                        object payloadData          = null;
                        byte[] decryptedPayloadData = null;

                        if (loraPayload.Frmpayload.Length > 0)
                        {
                            try
                            {
                                decryptedPayloadData = loraPayload.GetDecryptedPayload(loRaDevice.AppSKey);
                            }
                            catch (Exception ex)
                            {
                                Logger.Log(loRaDevice.DevEUI, $"failed to decrypt message: {ex.Message}", LogLevel.Error);
                            }
                        }

                        if (payloadPort == LoRaFPort.MacCommand)
                        {
                            if (decryptedPayloadData?.Length > 0)
                            {
                                loraPayload.MacCommands = MacCommand.CreateMacCommandFromBytes(loRaDevice.DevEUI, decryptedPayloadData);
                            }

                            if (loraPayload.IsMacAnswerRequired)
                            {
                                fcntDown = await this.EnsureHasFcntDownAsync(loRaDevice, fcntDown, payloadFcntAdjusted, frameCounterStrategy);

                                if (!fcntDown.HasValue || fcntDown <= 0)
                                {
                                    return(new LoRaDeviceRequestProcessResult(loRaDevice, request, LoRaDeviceRequestFailedReason.HandledByAnotherGateway));
                                }

                                requiresConfirmation = true;
                            }
                        }
                        else
                        {
                            if (string.IsNullOrEmpty(loRaDevice.SensorDecoder))
                            {
                                Logger.Log(loRaDevice.DevEUI, $"no decoder set in device twin. port: {payloadPort}", LogLevel.Debug);
                                payloadData = new UndecodedPayload(decryptedPayloadData);
                            }
                            else
                            {
                                Logger.Log(loRaDevice.DevEUI, $"decoding with: {loRaDevice.SensorDecoder} port: {payloadPort}", LogLevel.Debug);
                                var decodePayloadResult = await this.payloadDecoder.DecodeMessageAsync(loRaDevice.DevEUI, decryptedPayloadData, payloadPort, loRaDevice.SensorDecoder);

                                payloadData = decodePayloadResult.GetDecodedPayload();

                                if (decodePayloadResult.CloudToDeviceMessage != null)
                                {
                                    if (string.IsNullOrEmpty(decodePayloadResult.CloudToDeviceMessage.DevEUI) || string.Equals(loRaDevice.DevEUI, decodePayloadResult.CloudToDeviceMessage.DevEUI, StringComparison.InvariantCultureIgnoreCase))
                                    {
                                        // sending c2d to same device
                                        cloudToDeviceMessage = decodePayloadResult.CloudToDeviceMessage;
                                        fcntDown             = await this.EnsureHasFcntDownAsync(loRaDevice, fcntDown, payloadFcntAdjusted, frameCounterStrategy);

                                        if (!fcntDown.HasValue || fcntDown <= 0)
                                        {
                                            // We did not get a valid frame count down, therefore we should not process the message
                                            _ = cloudToDeviceMessage.AbandonAsync();

                                            cloudToDeviceMessage = null;
                                        }
                                        else
                                        {
                                            requiresConfirmation = true;
                                        }
                                    }
                                    else
                                    {
                                        this.SendClassCDeviceMessage(decodePayloadResult.CloudToDeviceMessage);
                                    }
                                }
                            }
                        }

                        if (!isConfirmedResubmit)
                        {
                            // In case it is a Mac Command only we don't want to send it to the IoT Hub
                            if (payloadPort != LoRaFPort.MacCommand)
                            {
                                if (!await this.SendDeviceEventAsync(request, loRaDevice, timeWatcher, payloadData, bundlerResult?.DeduplicationResult, decryptedPayloadData))
                                {
                                    // failed to send event to IoT Hub, stop now
                                    return(new LoRaDeviceRequestProcessResult(loRaDevice, request, LoRaDeviceRequestFailedReason.IoTHubProblem));
                                }
                            }

                            loRaDevice.SetFcntUp(payloadFcntAdjusted);
                        }
                    }

                    // We check if we have time to futher progress or not
                    // C2D checks are quite expensive so if we are really late we just stop here
                    var timeToSecondWindow = timeWatcher.GetRemainingTimeToReceiveSecondWindow(loRaDevice);
                    if (timeToSecondWindow < LoRaOperationTimeWatcher.ExpectedTimeToPackageAndSendMessage)
                    {
                        if (requiresConfirmation)
                        {
                            Logger.Log(loRaDevice.DevEUI, $"too late for down message ({timeWatcher.GetElapsedTime()})", LogLevel.Information);
                        }

                        return(new LoRaDeviceRequestProcessResult(loRaDevice, request));
                    }

                    // If it is confirmed and
                    // - Downlink is disabled for the device or
                    // - we don't have time to check c2d and send to device we return now
                    if (requiresConfirmation && (!loRaDevice.DownlinkEnabled || timeToSecondWindow.Subtract(LoRaOperationTimeWatcher.ExpectedTimeToPackageAndSendMessage) <= LoRaOperationTimeWatcher.MinimumAvailableTimeToCheckForCloudMessage))
                    {
                        var downlinkMessageBuilderResp = DownlinkMessageBuilder.CreateDownlinkMessage(
                            this.configuration,
                            loRaDevice,
                            request,
                            timeWatcher,
                            cloudToDeviceMessage,
                            false, // fpending
                            fcntDown.GetValueOrDefault(),
                            loRaADRResult);

                        if (downlinkMessageBuilderResp.DownlinkPktFwdMessage != null)
                        {
                            _ = request.PacketForwarder.SendDownstreamAsync(downlinkMessageBuilderResp.DownlinkPktFwdMessage);

                            if (cloudToDeviceMessage != null)
                            {
                                if (downlinkMessageBuilderResp.IsMessageTooLong)
                                {
                                    await cloudToDeviceMessage.AbandonAsync();
                                }
                                else
                                {
                                    await cloudToDeviceMessage.CompleteAsync();
                                }
                            }
                        }

                        return(new LoRaDeviceRequestProcessResult(loRaDevice, request, downlinkMessageBuilderResp.DownlinkPktFwdMessage));
                    }

                    // Flag indicating if there is another C2D message waiting
                    var fpending = false;

                    // If downlink is enabled and we did not get a cloud to device message from decoder
                    // try to get one from IoT Hub C2D
                    if (loRaDevice.DownlinkEnabled && cloudToDeviceMessage == null)
                    {
                        // ReceiveAsync has a longer timeout
                        // But we wait less that the timeout (available time before 2nd window)
                        // if message is received after timeout, keep it in loraDeviceInfo and return the next call
                        var timeAvailableToCheckCloudToDeviceMessages = timeWatcher.GetAvailableTimeToCheckCloudToDeviceMessage(loRaDevice);
                        if (timeAvailableToCheckCloudToDeviceMessages >= LoRaOperationTimeWatcher.MinimumAvailableTimeToCheckForCloudMessage)
                        {
                            cloudToDeviceMessage = await this.ReceiveCloudToDeviceAsync(loRaDevice, timeAvailableToCheckCloudToDeviceMessages);

                            if (cloudToDeviceMessage != null && !this.ValidateCloudToDeviceMessage(loRaDevice, request, cloudToDeviceMessage))
                            {
                                // Reject cloud to device message based on result from ValidateCloudToDeviceMessage
                                _ = cloudToDeviceMessage.RejectAsync();
                                cloudToDeviceMessage = null;
                            }

                            if (cloudToDeviceMessage != null)
                            {
                                if (!requiresConfirmation)
                                {
                                    // The message coming from the device was not confirmed, therefore we did not computed the frame count down
                                    // Now we need to increment because there is a C2D message to be sent
                                    fcntDown = await this.EnsureHasFcntDownAsync(loRaDevice, fcntDown, payloadFcntAdjusted, frameCounterStrategy);

                                    if (!fcntDown.HasValue || fcntDown <= 0)
                                    {
                                        // We did not get a valid frame count down, therefore we should not process the message
                                        _ = cloudToDeviceMessage.AbandonAsync();

                                        cloudToDeviceMessage = null;
                                    }
                                    else
                                    {
                                        requiresConfirmation = true;
                                    }
                                }

                                // Checking again if cloudToDeviceMessage is valid because the fcntDown resolution could have failed,
                                // causing us to drop the message
                                if (cloudToDeviceMessage != null)
                                {
                                    var remainingTimeForFPendingCheck = timeWatcher.GetRemainingTimeToReceiveSecondWindow(loRaDevice) - (LoRaOperationTimeWatcher.CheckForCloudMessageCallEstimatedOverhead + LoRaOperationTimeWatcher.MinimumAvailableTimeToCheckForCloudMessage);
                                    if (remainingTimeForFPendingCheck >= LoRaOperationTimeWatcher.MinimumAvailableTimeToCheckForCloudMessage)
                                    {
                                        var additionalMsg = await this.ReceiveCloudToDeviceAsync(loRaDevice, LoRaOperationTimeWatcher.MinimumAvailableTimeToCheckForCloudMessage);

                                        if (additionalMsg != null)
                                        {
                                            fpending = true;
                                            Logger.Log(loRaDevice.DevEUI, $"found cloud to device message, setting fpending flag, message id: {additionalMsg.MessageId ?? "undefined"}", LogLevel.Information);
                                            _ = additionalMsg.AbandonAsync();
                                        }
                                    }
                                }
                            }
                        }
                    }

                    // No C2D message and request was not confirmed, return nothing
                    if (!requiresConfirmation)
                    {
                        return(new LoRaDeviceRequestProcessResult(loRaDevice, request));
                    }

                    var confirmDownlinkMessageBuilderResp = DownlinkMessageBuilder.CreateDownlinkMessage(
                        this.configuration,
                        loRaDevice,
                        request,
                        timeWatcher,
                        cloudToDeviceMessage,
                        fpending,
                        fcntDown.GetValueOrDefault(),
                        loRaADRResult);

                    if (cloudToDeviceMessage != null)
                    {
                        if (confirmDownlinkMessageBuilderResp.DownlinkPktFwdMessage == null)
                        {
                            Logger.Log(loRaDevice.DevEUI, $"out of time for downstream message, will abandon cloud to device message id: {cloudToDeviceMessage.MessageId ?? "undefined"}", LogLevel.Information);
                            _ = cloudToDeviceMessage.AbandonAsync();
                        }
                        else if (confirmDownlinkMessageBuilderResp.IsMessageTooLong)
                        {
                            Logger.Log(loRaDevice.DevEUI, $"payload will not fit in current receive window, will abandon cloud to device message id: {cloudToDeviceMessage.MessageId ?? "undefined"}", LogLevel.Error);
                            _ = cloudToDeviceMessage.AbandonAsync();
                        }
                        else
                        {
                            _ = cloudToDeviceMessage.CompleteAsync();
                        }
                    }

                    if (confirmDownlinkMessageBuilderResp.DownlinkPktFwdMessage != null)
                    {
                        _ = request.PacketForwarder.SendDownstreamAsync(confirmDownlinkMessageBuilderResp.DownlinkPktFwdMessage);
                    }

                    return(new LoRaDeviceRequestProcessResult(loRaDevice, request, confirmDownlinkMessageBuilderResp.DownlinkPktFwdMessage));
                }
                finally
                {
                    try
                    {
                        await loRaDevice.SaveChangesAsync();
                    }
                    catch (Exception saveChangesException)
                    {
                        Logger.Log(loRaDevice.DevEUI, $"error updating reported properties. {saveChangesException.Message}", LogLevel.Error);
                    }
                }
            }
        }