/*
 * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;

import java.security.Principal;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.function.Predicate;

import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;

import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamType;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SEBClientConfigDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.EventHandlingStrategy;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.PingHandlingStrategy;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientNotificationService;
import ch.ethz.seb.sebserver.webservice.weblayer.api.APIConstraintViolationException;

@Lazy
@Service
@WebServiceProfile
public class SEBClientConnectionServiceImpl implements SEBClientConnectionService {

    private static final Logger log = LoggerFactory.getLogger(SEBClientConnectionServiceImpl.class);

    private static final Predicate<ClientConnection> DISABLE_STATE_PREDICATE = ClientConnection
            .getStatusPredicate(
                    ConnectionStatus.UNDEFINED,
                    ConnectionStatus.CONNECTION_REQUESTED,
                    ConnectionStatus.AUTHENTICATED,
                    ConnectionStatus.CLOSED);

    private final ExamSessionService examSessionService;
    private final ExamSessionCacheService examSessionCacheService;
    private final CacheManager cacheManager;
    private final EventHandlingStrategy eventHandlingStrategy;
    private final ClientConnectionDAO clientConnectionDAO;
    private final PingHandlingStrategy pingHandlingStrategy;
    private final SEBClientConfigDAO sebClientConfigDAO;
    private final SEBClientInstructionService sebInstructionService;
    private final SEBClientNotificationService sebClientNotificationService;
    private final WebserviceInfo webserviceInfo;
    private final ExamAdminService examAdminService;

    protected SEBClientConnectionServiceImpl(
            final ExamSessionService examSessionService,
            final EventHandlingStrategyFactory eventHandlingStrategyFactory,
            final PingHandlingStrategyFactory pingHandlingStrategyFactory,
            final SEBClientConfigDAO sebClientConfigDAO,
            final SEBClientInstructionService sebInstructionService,
            final SEBClientNotificationService sebClientNotificationService,
            final ExamAdminService examAdminService) {

        this.examSessionService = examSessionService;
        this.examSessionCacheService = examSessionService.getExamSessionCacheService();
        this.cacheManager = examSessionService.getCacheManager();
        this.clientConnectionDAO = examSessionService.getClientConnectionDAO();
        this.pingHandlingStrategy = pingHandlingStrategyFactory.get();
        this.eventHandlingStrategy = eventHandlingStrategyFactory.get();
        this.sebClientConfigDAO = sebClientConfigDAO;
        this.sebInstructionService = sebInstructionService;
        this.sebClientNotificationService = sebClientNotificationService;
        this.webserviceInfo = sebInstructionService.getWebserviceInfo();
        this.examAdminService = examAdminService;
    }

    @Override
    public ExamSessionService getExamSessionService() {
        return this.examSessionService;
    }

    @Override
    public Result<ClientConnection> createClientConnection(
            final Principal principal,
            final Long institutionId,
            final String clientAddress,
            final Long examId) {

        return Result.tryCatch(() -> {

            final Long clientsInstitution = getInstitutionId(principal);
            if (!clientsInstitution.equals(institutionId)) {
                log.error("Institutional integrity violation: requested institution: {} authenticated institution: {}",
                        institutionId,
                        clientsInstitution);
                throw new APIConstraintViolationException("Institutional integrity violation");
            }

            if (log.isDebugEnabled()) {
                log.debug("Request received on Exam Client Connection create endpoint: "
                        + "institution: {} "
                        + "exam: {} "
                        + "client-address: {}",
                        institutionId,
                        examId,
                        clientAddress);
            }

            if (log.isDebugEnabled()) {
                log.debug("SEB client connection attempt, create ClientConnection for "
                        + "institution {} "
                        + "exam: {} "
                        + "client address: {}",
                        institutionId,
                        examId,
                        clientAddress);
            }

            if (examId != null) {
                checkExamIntegrity(examId);
            }

            // Create ClientConnection in status CONNECTION_REQUESTED for further processing
            final String connectionToken = createToken();
            final ClientConnection clientConnection = this.clientConnectionDAO.createNew(new ClientConnection(
                    null,
                    institutionId,
                    examId,
                    ConnectionStatus.CONNECTION_REQUESTED,
                    connectionToken,
                    null,
                    clientAddress,
                    null,
                    null,
                    null,
                    null,
                    null))
                    .getOrThrow();

            // load client connection data into cache
            final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService
                    .getClientConnection(connectionToken);

            if (activeClientConnection == null) {
                log.warn("Failed to load ClientConnectionDataInternal into cache on update");
            } else {
                if (log.isDebugEnabled()) {
                    log.debug("New ClientConnection created: {}", clientConnection);
                }
            }

            return clientConnection;
        });
    }

    @Override
    public Result<ClientConnection> updateClientConnection(
            final String connectionToken,
            final Long institutionId,
            final Long examId,
            final String clientAddress,
            final String userSessionId) {

        return Result.tryCatch(() -> {
            if (log.isDebugEnabled()) {
                log.debug(
                        "SEB client connection, update ClientConnection for "
                                + "connectionToken {} "
                                + "institutionId: {}"
                                + "exam: {} "
                                + "client address: {} "
                                + "userSessionId: {}",
                        connectionToken,
                        institutionId,
                        examId,
                        clientAddress,
                        userSessionId);
            }

            final ClientConnection clientConnection = getClientConnection(connectionToken);

            checkInstitutionalIntegrity(institutionId, clientConnection);
            checkExamIdIntegrity(examId, clientConnection);

            // If we have an active connection, update the connection data if requested
            if (clientConnection.status == ConnectionStatus.ACTIVE) {
                return updateActiveConnection(clientConnection, clientAddress, userSessionId);
            }

            // connection integrity check
            if (clientConnection.status != ConnectionStatus.CONNECTION_REQUESTED) {
                log.error("ClientConnection integrity violation: client connection is not in expected state: {}",
                        clientConnection);
                throw new IllegalArgumentException(
                        "ClientConnection integrity violation: client connection is not in expected state");
            }

            if (examId != null) {
                checkExamIntegrity(examId);
            }

            // userSessionId integrity check
            if (userSessionId != null &&
                    clientConnection.userSessionId != null &&
                    !userSessionId.equals(clientConnection.userSessionId)) {

                log.error(
                        "User session identifier integrity violation: another User session identifier is already set for the connection: {}",
                        clientConnection);
                throw new IllegalArgumentException(
                        "User session identifier integrity violation: another User session identifier is already set for the connection");
            }

            final String virtualClientAddress = getVirtualClientAddress(
                    (examId != null) ? examId : clientConnection.examId,
                    clientAddress,
                    clientConnection.clientAddress);

            final ClientConnection updatedClientConnection = this.clientConnectionDAO
                    .save(new ClientConnection(
                            clientConnection.id,
                            null,
                            examId,
                            (userSessionId != null) ? ConnectionStatus.AUTHENTICATED : null,
                            null,
                            userSessionId,
                            null,
                            virtualClientAddress,
                            null,
                            null,
                            null,
                            null))
                    .getOrThrow();

            final ClientConnectionDataInternal activeClientConnection =
                    reloadConnectionCache(connectionToken);

            if (activeClientConnection == null) {
                log.warn("Failed to load ClientConnectionDataInternal into cache on update");
            } else if (log.isDebugEnabled()) {
                log.debug("SEB client connection, successfully updated ClientConnection: {}",
                        updatedClientConnection);
            }

            return updatedClientConnection;
        });
    }

    private ClientConnection updateActiveConnection(
            final ClientConnection clientConnection,
            final String clientAddress,
            final String userSessionId) {

        final String virtualClientAddress = getVirtualClientAddress(
                clientConnection.examId,
                clientAddress,
                clientConnection.clientAddress);

        final ClientConnection updatedClientConnection = this.clientConnectionDAO
                .save(new ClientConnection(
                        clientConnection.id,
                        null,
                        null,
                        null,
                        null,
                        (StringUtils.isNotBlank(userSessionId)) ? userSessionId : null,
                        (StringUtils.isNotBlank(clientAddress)) ? clientAddress : null,
                        virtualClientAddress,
                        null,
                        null,
                        null,
                        null))
                .getOrThrow();

        final ClientConnectionDataInternal activeClientConnection =
                reloadConnectionCache(clientConnection.connectionToken);

        if (activeClientConnection == null) {
            log.warn("Failed to load ClientConnectionDataInternal into cache on update");
        } else if (log.isDebugEnabled()) {
            log.debug("SEB client connection, successfully updated ClientConnection: {}",
                    updatedClientConnection);
        }

        return updatedClientConnection;
    }

    @Override
    public Result<ClientConnection> establishClientConnection(
            final String connectionToken,
            final Long institutionId,
            final Long examId,
            final String clientAddress,
            final String userSessionId) {

        return Result.tryCatch(() -> {

            if (log.isDebugEnabled()) {
                log.debug(
                        "SEB client connection, establish ClientConnection for "
                                + "connectionToken {} "
                                + "institutionId: {}"
                                + "exam: {} "
                                + "client address: {} "
                                + "userSessionId: {}",
                        connectionToken,
                        institutionId,
                        examId,
                        clientAddress,
                        userSessionId);
            }

            ClientConnection clientConnection = getClientConnection(connectionToken);
            checkInstitutionalIntegrity(institutionId, clientConnection);
            checkExamIdIntegrity(examId, clientConnection);
            clientConnection = updateUserSessionId(userSessionId, clientConnection, examId);

            // connection integrity check
            if (clientConnection.status == ConnectionStatus.CONNECTION_REQUESTED) {
                log.warn("ClientConnection integrity warning: client connection is not authenticated: {}",
                        clientConnection);
            } else if (clientConnection.status != ConnectionStatus.AUTHENTICATED) {
                log.error("ClientConnection integrity violation: client connection is not in expected state: {}",
                        clientConnection);
                throw new IllegalArgumentException(
                        "ClientConnection integrity violation: client connection is not in expected state");
            }

            final String virtualClientAddress = getVirtualClientAddress(
                    (examId != null) ? examId : clientConnection.examId,
                    clientAddress,
                    clientConnection.clientAddress);

            final Boolean proctoringEnabled = this.examAdminService
                    .isExamProctoringEnabled(clientConnection.examId)
                    .getOr(false);
            final Long currentExamId = (examId != null) ? examId : clientConnection.examId;

            // create new ClientConnection for update
            final ClientConnection establishedClientConnection = new ClientConnection(
                    clientConnection.id,
                    null,
                    currentExamId,
                    ConnectionStatus.ACTIVE,
                    null,
                    clientConnection.userSessionId,
                    null,
                    virtualClientAddress,
                    null,
                    null,
                    null,
                    proctoringEnabled);

            // ClientConnection integrity
            if (clientConnection.institutionId == null ||
                    clientConnection.connectionToken == null ||
                    establishedClientConnection.examId == null ||
                    clientConnection.clientAddress == null ||
                    establishedClientConnection.status != ConnectionStatus.ACTIVE) {

                log.error("ClientConnection integrity violation, clientConnection: {}, establishedClientConnection: {}",
                        clientConnection,
                        establishedClientConnection);
                throw new IllegalStateException("ClientConnection integrity violation");
            }

            final ClientConnection updatedClientConnection = this.clientConnectionDAO
                    .save(establishedClientConnection)
                    .getOrThrow();

            checkExamIntegrity(updatedClientConnection.examId);

            final ClientConnectionDataInternal activeClientConnection =
                    reloadConnectionCache(connectionToken);

            if (activeClientConnection == null) {
                log.warn("Failed to load ClientConnectionDataInternal into cache on update");
            } else if (log.isDebugEnabled()) {
                log.debug("SEB client connection, successfully established ClientConnection: {}",
                        updatedClientConnection);
            }

            // notify ping handler about established connection
            this.pingHandlingStrategy.initForConnection(
                    updatedClientConnection.id,
                    connectionToken);

            return updatedClientConnection;
        });
    }

    @Override
    public Result<ClientConnection> closeConnection(
            final String connectionToken,
            final Long institutionId,
            final String clientAddress) {

        return Result.tryCatch(() -> {

            if (log.isDebugEnabled()) {
                log.debug("SEB client connection: regular close attempt for "
                        + "institution {} "
                        + "client address: {} "
                        + "connectionToken {} ",
                        institutionId,
                        clientAddress,
                        connectionToken);
            }

            final ClientConnection clientConnection = this.clientConnectionDAO
                    .byConnectionToken(connectionToken)
                    .getOrThrow();

            ClientConnection updatedClientConnection;
            if (clientConnection.status != ConnectionStatus.CLOSED) {
                updatedClientConnection = saveInState(
                        clientConnection,
                        ConnectionStatus.CLOSED);

                if (log.isDebugEnabled()) {
                    log.debug("SEB client connection: successfully closed ClientConnection: {}",
                            clientConnection);
                }
            } else {
                log.warn("SEB client connection is already closed: {}", clientConnection);
                updatedClientConnection = clientConnection;
            }

            reloadConnectionCache(connectionToken);
            return updatedClientConnection;
        });
    }

    @Override
    public Result<ClientConnection> disableConnection(final String connectionToken, final Long institutionId) {
        return Result.tryCatch(() -> {
            final ClientConnectionData connectionData = getExamSessionService()
                    .getConnectionData(connectionToken)
                    .getOrThrow();

            // An active connection can only be disabled if we have a missing ping
            if (connectionData.clientConnection.status == ConnectionStatus.ACTIVE &&
                    !BooleanUtils.isTrue(connectionData.getMissingPing())) {

                return connectionData.clientConnection;
            }

            if (log.isDebugEnabled()) {
                log.debug("SEB client connection: SEB Server disable attempt for "
                        + "institution {} "
                        + "connectionToken {} ",
                        institutionId,
                        connectionToken);
            }

            final ClientConnection clientConnection = this.clientConnectionDAO
                    .byConnectionToken(connectionToken)
                    .getOrThrow();

            ClientConnection updatedClientConnection;
            if (DISABLE_STATE_PREDICATE.test(clientConnection)) {

                updatedClientConnection = saveInState(
                        clientConnection,
                        ConnectionStatus.DISABLED);

                if (log.isDebugEnabled()) {
                    log.debug("SEB client connection: successfully disabled ClientConnection: {}",
                            clientConnection);
                }

                final Boolean proctoringEnabled = this.examAdminService
                        .isExamProctoringEnabled(clientConnection.examId)
                        .getOr(false);
                if (proctoringEnabled) {
                    final ClientConnection updateClientConnection = new ClientConnection(
                            clientConnection.id,
                            null,
                            null,
                            null,
                            null,
                            null,
                            null,
                            null,
                            null,
                            null,
                            null,
                            true);
                    this.clientConnectionDAO
                            .save(updateClientConnection)
                            .getOrThrow();
                }
            } else {
                log.warn("SEB client connection in invalid state for disabling: {}", clientConnection);
                updatedClientConnection = clientConnection;
            }

            reloadConnectionCache(connectionToken);
            return updatedClientConnection;
        });
    }

    @Override
    public void updatePingEvents() {
        try {

            final Cache cache = this.cacheManager.getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION);
            final long now = DateTime.now(DateTimeZone.UTC).getMillis();
            this.examSessionService
                    .getExamDAO()
                    .allRunningExamIds()
                    .getOrThrow()
                    .stream()
                    .flatMap(examId -> (this.webserviceInfo.isDistributed())
                            ? this.clientConnectionDAO
                                    .getConnectionTokensNoCache(examId)
                                    .getOrThrow()
                                    .stream()
                            : this.clientConnectionDAO
                                    .getConnectionTokens(examId)
                                    .getOrThrow()
                                    .stream())
                    .map(token -> cache.get(token, ClientConnectionDataInternal.class))
                    .filter(Objects::nonNull)
                    .filter(connection -> connection.pingIndicator != null &&
                            connection.clientConnection.status.establishedStatus)
                    .forEach(missingPingUpdate(now));

        } catch (final Exception e) {
            log.error("Failed to update ping events: ", e);
        }
    }

    @Override
    public String notifyPing(
            final String connectionToken,
            final long timestamp,
            final int pingNumber) {

        this.pingHandlingStrategy.notifyPing(connectionToken, timestamp, pingNumber);
        return this.sebInstructionService.getInstructionJSON(connectionToken);
    }

    @Override
    public void notifyClientEvent(
            final String connectionToken,
            final ClientEvent event) {

        try {
            final ClientConnectionDataInternal activeClientConnection =
                    this.examSessionCacheService.getClientConnection(connectionToken);

            if (activeClientConnection != null) {

                // store event
                this.eventHandlingStrategy.accept(ClientEvent.toRecord(
                        event,
                        activeClientConnection.getConnectionId()));

                switch (event.eventType) {
                    case NOTIFICATION: {
                        this.sebClientNotificationService
                                .notifyNewNotification(activeClientConnection.getConnectionId());
                        break;
                    }
                    case NOTIFICATION_CONFIRMED: {
                        this.sebClientNotificationService.confirmPendingNotification(event, connectionToken);
                        break;
                    }
                    default: {
                        // update indicators
                        activeClientConnection.getIndicatorMapping(event.eventType)
                                .forEach(indicator -> indicator.notifyValueChange(event));
                    }
                }

            } else {
                log.warn("No active ClientConnection found for connectionToken: {}", connectionToken);
            }
        } catch (final Exception e) {
            log.error("Failed to process SEB client event: ", e);
        }
    }

    @Override
    public void confirmInstructionDone(final String connectionToken, final String instructionConfirm) {
        this.sebInstructionService.confirmInstructionDone(connectionToken, instructionConfirm);
    }

    private void checkExamRunning(final Long examId) {
        if (examId != null && !this.examSessionService.isExamRunning(examId)) {
            examNotRunningException(examId);
        }
    }

    private ClientConnection getClientConnection(final String connectionToken) {
        return this.clientConnectionDAO
                .byConnectionToken(connectionToken)
                .getOrThrow();
    }

    private void checkInstitutionalIntegrity(
            final Long institutionId,
            final ClientConnection clientConnection) throws IllegalAccessError {

        if (!institutionId.equals(clientConnection.institutionId)) {
            log.error("Institution integrity violation with institution: {} on clientConnection: {}",
                    institutionId,
                    clientConnection);
            throw new IllegalAccessError("Institution integrity violation");
        }
    }

    private String createToken() {
        return UUID.randomUUID().toString();
    }

    private void examNotRunningException(final Long examId) {
        log.error("The exam {} is not running", examId);
        throw new IllegalStateException("The exam " + examId + " is not running");
    }

    private String getVirtualClientAddress(
            final Long examId,
            final String requestClientAddress,
            final String existingClientAddress) {

        if (examId == null ||
                requestClientAddress == null ||
                requestClientAddress.equals(existingClientAddress) ||
                !isVDI(examId)) {
            return null;
        }

        return requestClientAddress;
    }

    private boolean isVDI(final Long examId) {
        return this.examSessionService.getRunningExam(examId)
                .getOrThrow()
                .getType() == ExamType.VDI;
    }

    private Long getInstitutionId(final Principal principal) {
        final String clientId = principal.getName();
        return this.sebClientConfigDAO.byClientName(clientId)
                .getOrThrow().institutionId;
    }

    private void checkExamIdIntegrity(final Long examId, final ClientConnection clientConnection) {
        if (examId != null &&
                clientConnection.examId != null &&
                !examId.equals(clientConnection.examId)) {

            log.error("Exam integrity violation: another examId is already set for the connection: {}",
                    clientConnection);
            throw new IllegalArgumentException(
                    "Exam integrity violation: another examId is already set for the connection");
        }
        checkExamRunning(examId);
    }

    private ClientConnection updateUserSessionId(
            final String userSessionId,
            ClientConnection clientConnection,
            final Long examId) {

        if (StringUtils.isNoneBlank(userSessionId)) {
            if (StringUtils.isNoneBlank(clientConnection.userSessionId)) {
                log.error(
                        "ClientConnection integrity violation: clientConnection has already a userSessionId: {} : {}",
                        userSessionId, clientConnection);
                throw new IllegalArgumentException(
                        "ClientConnection integrity violation: clientConnection has already a userSessionId");
            }

            // try to get user account display name
            String accountId = userSessionId;
            try {
                accountId = this.examSessionService
                        .getRunningExam((clientConnection.examId != null)
                                ? clientConnection.examId
                                : examId)
                        .flatMap(exam -> this.examSessionService.getLmsAPIService().getLmsAPITemplate(exam.lmsSetupId))
                        .map(template -> template.getExamineeName(userSessionId))
                        .getOr(userSessionId);
            } catch (final Exception e) {
                log.warn("Unexpected error while trying to get user account display name: {}", e.getMessage());
            }

            // create new ClientConnection for update
            final ClientConnection authenticatedClientConnection = new ClientConnection(
                    clientConnection.id,
                    null,
                    null,
                    ConnectionStatus.AUTHENTICATED,
                    null,
                    accountId,
                    null,
                    null,
                    null,
                    null,
                    null,
                    null);

            clientConnection = this.clientConnectionDAO
                    .save(authenticatedClientConnection)
                    .getOrThrow();
        }
        return clientConnection;
    }

    private void checkExamIntegrity(final Long examId) {
        if (this.webserviceInfo.isDistributed()) {
            // if the cached Exam is not up to date anymore, we have to update the cache first
            final Result<Exam> updateExamCache = this.examSessionService.updateExamCache(examId);
            if (updateExamCache.hasError()) {
                log.warn("Failed to update Exam-Cache for Exam: {}", examId);
            }
        }

        // check Exam is running and not locked
        checkExamRunning(examId);
        if (this.examSessionService.isExamLocked(examId)) {
            throw new APIConstraintViolationException(
                    "Exam is currently on update and locked for new SEB Client connections");
        }

        // check Exam has a default SEB Exam configuration attached
        if (!this.examSessionService.hasDefaultConfigurationAttached(examId)) {
            throw new APIConstraintViolationException(
                    "Exam is currently running but has no default SEB Exam configuration attached");
        }
    }

    private ClientConnection saveInState(final ClientConnection clientConnection, final ConnectionStatus status) {
        return this.clientConnectionDAO.save(new ClientConnection(
                clientConnection.id, null, null,
                status, null, null, null, null, null, null, null,
                true))
                .getOrThrow();
    }

    private ClientConnectionDataInternal reloadConnectionCache(final String connectionToken) {
        // evict cached ClientConnection
        this.examSessionCacheService.evictClientConnection(connectionToken);
        // evict also cached ping record
        this.examSessionCacheService.evictPingRecord(connectionToken);
        // and load updated ClientConnection into cache
        return this.examSessionCacheService.getClientConnection(connectionToken);
    }

    private Consumer<ClientConnectionDataInternal> missingPingUpdate(final long now) {
        return connection -> {
            final ClientEventRecord clientEventRecord = connection.pingIndicator.updateLogEvent(now);
            if (clientEventRecord != null) {
                // store event and and flush cache
                this.eventHandlingStrategy.accept(clientEventRecord);
                if (this.webserviceInfo.isDistributed()) {
                    // mark for update and flush the cache
                    this.clientConnectionDAO.save(connection.clientConnection);
                    this.examSessionCacheService.evictClientConnection(
                            connection.clientConnection.connectionToken);
                } else {
                    // update indicators
                    if (clientEventRecord.getType() != null && EventType.ERROR_LOG.id == clientEventRecord.getType()) {
                        connection.getIndicatorMapping(EventType.ERROR_LOG)
                                .forEach(indicator -> indicator.notifyValueChange(clientEventRecord));
                    }
                }
            }
        };
    }

}
