/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
package com.scytl.products.ov.channel.cc.choicecodes.verification.service;

import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.api.primitives.PrimitivesServiceAPI;
import com.scytl.cryptolib.elgamal.bean.ElGamalPrivateKey;
import com.scytl.cryptolib.mathematical.groups.impl.Exponent;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;
import com.scytl.cryptolib.proofs.proof.Proof;
import com.scytl.products.oscore.logging.api.domain.Level;
import com.scytl.products.oscore.logging.api.domain.LogContent;
import com.scytl.products.oscore.logging.api.writer.SecureLoggingWriter;
import com.scytl.products.ov.channel.cc.commons.exponentiation.ExponentiationService;
import com.scytl.products.ov.channel.cc.commons.exponentiation.PowersAndProof;
import com.scytl.products.ov.channel.cc.commons.keymanagement.KeyManager;
import com.scytl.products.ov.channel.cc.commons.keymanagement.KeyNotFoundException;
import com.scytl.products.ov.channel.cc.commons.keymanagement.log.ControlComponentsCommonsLogConstants;
import com.scytl.products.ov.channel.cc.commons.keymanagement.log.ControlComponentsCommonsLogEvents;
import com.scytl.products.ov.channel.cc.commons.services.ChoiceCodesKeyRepository;
import com.scytl.products.ov.channel.cc.commons.services.compute.KeyDerivationService;
import com.scytl.products.ov.channel.cc.commons.slogger.ServiceTransactionInfoProvider;
import com.scytl.products.ov.commons.beans.domain.model.confirmation.TraceableConfirmationMessage;
import com.scytl.products.ov.commons.beans.domain.model.messaging.PayloadSignatureException;
import com.scytl.products.ov.commons.beans.domain.model.vote.Vote;
import com.scytl.products.ov.commons.dto.BallotCastingKeyVerificationInput;
import com.scytl.products.ov.commons.dto.ChoiceCodeVerificationDTO;
import com.scytl.products.ov.commons.dto.ChoiceCodesVerificationInput;
import com.scytl.products.ov.commons.dto.ChoiceCodesVerificationResPayload;
import com.scytl.products.ov.commons.dto.PartialCodesInput;
import com.scytl.products.ov.commons.messaging.MessageListener;
import com.scytl.products.ov.commons.messaging.MessagingException;
import com.scytl.products.ov.commons.messaging.MessagingService;
import com.scytl.products.ov.commons.messaging.Queue;
import com.scytl.products.ov.commons.sign.PayloadSigner;
import com.scytl.products.ov.commons.ui.ws.rs.ObjectMappers;

@Service
public class ChoiceCodesComputationServiceImpl implements ChoiceCodesComputationService, MessageListener {
    private Logger LOG = LoggerFactory.getLogger("std");

    private static final String CONFIRM_STRING_PADDING = "confirm";

    public static final int VOTE_CAST_MAX_ATTEMPTS = 5;

    private Queue computationInputQueue;

    private Queue computationOutputQueue;

    @Autowired
    private SecureLoggingWriter secureLoggingWriter;

    @Autowired
    private SecureLoggingWriter auditLoggingWriter;

    @Value("${keys.nodeId:defCcxId}")
    private String controlComponentId;

    @Autowired
    private MessagingService messagingService;

    @Autowired
    private ExponentiationService exponentiationService;

    @Autowired
    private KeyDerivationService keyDerivationService;

    @Autowired
    private ChoiceCodesKeyRepository choiceCodesKeyRepository;

    @Autowired
    private ProofValidator proofValidator;

    @Autowired
    private ServiceTransactionInfoProvider serviceTransactionInfoProvider;

    @Autowired
    private PayloadSigner payloadSigner;

    @Autowired
    private KeyManager keyManager;

    @Autowired
    private ComputedVerificationCardsRepository computedVerificationCardsRepository;

    @Autowired
    private PrimitivesServiceAPI primitivesService;

    @Value("${queues.cv-comp.req}")
    public void setComputationInputQueue(String queueName) {
        computationInputQueue = new Queue(queueName);
    }

    @Value("${queues.cv-comp.res}")
    public void setComputationOutputQueue(String queueName) {
        computationOutputQueue = new Queue(queueName);
    }

    @Override
    public void onMessage(Object message) {
        @SuppressWarnings("unchecked")
        ChoiceCodeVerificationDTO<PartialCodesInput> data = (ChoiceCodeVerificationDTO<PartialCodesInput>) message;

        serviceTransactionInfoProvider.generate("tenantID", "clientIP", "serverIP", data.getRequestId());
        if (isValid(data)) {
            compute(data);
        }
    }

    @Override
    public void startup() throws ChoiceCodesVerificationException {
        try {
            messagingService.createReceiver(computationInputQueue, this);
        } catch (MessagingException e) {
            LOG.error("Failed to startup choice codes computation service.", e);
            throw new ChoiceCodesVerificationException("Failed to startup choice codes computation service.", e);
        }
    }

    @Override
    public void shutdown() throws ChoiceCodesVerificationException {
        try {
            messagingService.destroyReceiver(computationInputQueue, this);
        } catch (MessagingException e) {
            LOG.error("Failed to shutdown choice codes computation service.", e);
            throw new ChoiceCodesVerificationException("Failed to shutdown choice codes computation service.", e);
        }
    }

    private void compute(ChoiceCodeVerificationDTO<PartialCodesInput> data) {
        try {
            ElGamalPrivateKey privateKey = getGenerationDerivationKey(data);
            ZpSubgroup group = privateKey.getGroup();
            List<BigInteger> codes = data.getPayload().getPartialCodesElements();
            if (isChoiceCodesComputation(data)) {
                Exponent choiceCodesExponent = deriveChoiceCodesExponent(data, privateKey);
                PowersAndProof<BigInteger> choiceCodesAndProof =
                    computeChoiceCodesAndProof(data, group, codes, choiceCodesExponent);
                ZpGroupElement choiceCodesDerivedKey = derivePublicKey(group, choiceCodesExponent);
                sendResponse(data, choiceCodesAndProof, choiceCodesDerivedKey, null);
            } else { // Cast code computation
                Exponent castCodeExponent = deriveCastCodeExponent(data, privateKey);
                List<BigInteger> hashedCodes = hashAndSquare(codes, group);
                PowersAndProof<BigInteger> choiceCodesAndProof =
                    computeChoiceCodesAndProof(data, group, hashedCodes, castCodeExponent);
                ZpGroupElement castCodeDerivedKey = derivePublicKey(group, castCodeExponent);
                sendResponse(data, choiceCodesAndProof, null, castCodeDerivedKey);
            }
        } catch (GeneralCryptoLibException | KeyManagementException | MessagingException | JsonProcessingException
                | PayloadSignatureException e) {
            LOG.error("Failed to handle choice code verification request.", e);
        }
    }

    private List<BigInteger> hashAndSquare(List<BigInteger> codes, ZpSubgroup group)
            throws GeneralCryptoLibException {
        // Hash the cast code to prevent "fake vote confirmation" attack (SV-6259)
        List<BigInteger> hashedCodes = new ArrayList<>();
        for(BigInteger code : codes) {
            BigInteger hash = new BigInteger(primitivesService.getHash(code.toByteArray()));
            BigInteger hashSquared = hash.multiply(hash);
            hashedCodes.add(new ZpGroupElement(hashSquared, group).getValue());
        }
        return hashedCodes;
    }

    private PowersAndProof<BigInteger> computeChoiceCodesAndProof(ChoiceCodeVerificationDTO<PartialCodesInput> data,
            ZpSubgroup group, List<BigInteger> codes, Exponent exponent) throws GeneralCryptoLibException, JsonProcessingException {
        PowersAndProof<BigInteger> choiceCodesAndProof = null;
        try {
            choiceCodesAndProof = exponentiationService.exponentiateCleartexts(codes, exponent, group);
        } catch (GeneralCryptoLibException e) {
            logErrorPartialChoiceCodesCannotComputed(data);
            logErrorProofKnowledgeExponentNotComputed(data);
            throw e;
        }
        logPartialChoiceCodesSuccessfullyComputed(data, choiceCodesAndProof.powers());
        logChoiceCodesExponentiationProofSuccessfullyComputed(data, codes, choiceCodesAndProof.powers(),
            choiceCodesAndProof.proof());
        return choiceCodesAndProof;
    }

    private Exponent deriveChoiceCodesExponent(ChoiceCodeVerificationDTO<PartialCodesInput> data,
            ElGamalPrivateKey privateKey) throws GeneralCryptoLibException {
        Exponent exponent = privateKey.getKeys().get(0);
        String seed = data.getVerificationCardId();
        BigInteger q = privateKey.getGroup().getQ();
        Exponent derivedExponent;
        try {
            derivedExponent = keyDerivationService.deriveKey(exponent, seed, q);
            logChoiceCodePrivateKeySuccessfullyComputed(data, derivedExponent, privateKey.getGroup());
        } catch (GeneralCryptoLibException e) {
            logChoiceCodePrivateKeyDerivationFailed(data);
            throw e;
        }
        return derivedExponent;
    }

    private Exponent deriveCastCodeExponent(ChoiceCodeVerificationDTO<PartialCodesInput> data,
            ElGamalPrivateKey privateKey) throws GeneralCryptoLibException {
        Exponent exponent = privateKey.getKeys().get(0);
        String seed = data.getVerificationCardId() + CONFIRM_STRING_PADDING;
        BigInteger q = privateKey.getGroup().getQ();
        Exponent derivedExponent;
        try {
            derivedExponent = keyDerivationService.deriveKey(exponent, seed, q);
            logVoteCastCodePrivateKeySuccessfullyComputed(data, derivedExponent, privateKey.getGroup());
        } catch (GeneralCryptoLibException e) {
            logVoteCastCodePrivateKeyDerivationFailed(data);
            throw e;
        }
        return derivedExponent;
    }

    private ZpGroupElement derivePublicKey(ZpSubgroup group, Exponent exponent) throws GeneralCryptoLibException {
        return group.getGenerator().exponentiate(exponent);
    }

    private ElGamalPrivateKey getGenerationDerivationKey(ChoiceCodeVerificationDTO<PartialCodesInput> data)
            throws KeyManagementException {
        String electionEventId = data.getElectionEventId();
        String verificationCardSetId = data.getVerificationCardSetId();
        return choiceCodesKeyRepository.getGenerationPrivateKey(electionEventId, verificationCardSetId);
    }

    private boolean isValid(ChoiceCodeVerificationDTO<PartialCodesInput> data) {
        if (isChoiceCodesComputation(data)) {
            try {
                computedVerificationCardsRepository.saveComputedVerificationCard(data.getElectionEventId(),
                    data.getVerificationCardId());
                logVerificationCardNotProcessed(data);
            } catch (ComputedVerificationCardExistsException e) {
                logVerificationCardAlreadyProcessedError(data);
                return false;
            }

            if (!checkChoiceCodesConsistency(data)) {
                return false;
            }

            ChoiceCodesVerificationInput choiceCodesVerificationInput =
                data.getPayload().getChoiceCodesVerificationInput();
            try {
                ZpSubgroup encryptionParameters = choiceCodesKeyRepository
                    .getEncryptionParameters(data.getElectionEventId(), data.getVerificationCardSetId());
                return proofValidator.validate(encryptionParameters, choiceCodesVerificationInput);
            } catch (KeyManagementException e) {
                return false;
            }
        } else { // Cast code computation
            if (!checkCastCodeAttempts(data)) {
                return false;
            }
            if (!checkConfirmationMessageConsistency(data)) {
                return false;
            }
            logConfirmationMessage(data);
            return true;
        }
    }

    private boolean checkConfirmationMessageConsistency(ChoiceCodeVerificationDTO<PartialCodesInput> data) {
        try {
            TraceableConfirmationMessage confirmationMessage = ObjectMappers.fromJson(
                data.getPayload().getBallotCastingKeyVerificationInput().getConfirmationMessage(),
                TraceableConfirmationMessage.class);
            String confirmationCodeString = new String(
                Base64.getDecoder().decode(confirmationMessage.getConfirmationKey()), StandardCharsets.UTF_8);
            BigInteger confirmationCodeValue = new BigInteger(confirmationCodeString);

            List<BigInteger> expectedConfirmationCodeToCompute = Collections.singletonList(confirmationCodeValue);
            List<BigInteger> computedConfirmationCodeToCompute = data.getPayload().getPartialCodesElements();

            if (computedConfirmationCodeToCompute == null) {
                LOG.warn("Unexpected scenario when processing cast code, the partial codes field in the payload was null");
                return false;
            }
            return computedConfirmationCodeToCompute.equals(expectedConfirmationCodeToCompute);
        } catch (IOException e) {
            LOG.error("Unexpected error checking the confirmation message consistency:", e);
            return false;
        }
    }

    private boolean checkChoiceCodesConsistency(ChoiceCodeVerificationDTO<PartialCodesInput> data) {
        try {
            Vote vote =
                ObjectMappers.fromJson(data.getPayload().getChoiceCodesVerificationInput().getVote(), Vote.class);

            String[] encryptedPartialCodes = vote.getEncryptedPartialChoiceCodes().split(";");

            List<BigInteger> expectedPartialCodesElements =
                Stream.of(encryptedPartialCodes).map(BigInteger::new).collect(Collectors.toList());
            List<BigInteger> computedPartialCodeElements = data.getPayload().getPartialCodesElements();
            
            if (computedPartialCodeElements == null) {
                LOG.warn("Unexpected scenario when processing choice codes, the partial codes field in the payload was null");
                return false;
            }
            return computedPartialCodeElements.equals(expectedPartialCodesElements);
        } catch (IOException e) {
            LOG.error("Unexpected error checking the vote consistency:", e);
            return false;
        }
    }

    private void logConfirmationAttemptsNotExceeded(ChoiceCodeVerificationDTO<PartialCodesInput> data, int attempts) {
        BallotCastingKeyVerificationInput ballotCastingKeyVerificationInput =
            data.getPayload().getBallotCastingKeyVerificationInput();
        LogContent logInfo = new LogContent.LogContentBuilder()
            .logEvent(ControlComponentsCommonsLogEvents.CONFIRMATION_ATTEMPTS_NOT_EXCEEDED)
            .user(ballotCastingKeyVerificationInput.getVotingCardId()).electionEvent(data.getElectionEventId())
            .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
            .additionalInfo("#confirmationMessage", ballotCastingKeyVerificationInput.getConfirmationMessage())
            .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
            .additionalInfo(ControlComponentsCommonsLogConstants.VOTE_CAST_ATTEMPTS, attempts + "").createLogInfo();
        secureLoggingWriter.log(Level.INFO, logInfo);
    }

    private void logConfirmationMessage(ChoiceCodeVerificationDTO<PartialCodesInput> data) {
        BallotCastingKeyVerificationInput ballotCastingKeyVerificationInput =
            data.getPayload().getBallotCastingKeyVerificationInput();
        LogContent logInfo = new LogContent.LogContentBuilder().logEvent(ControlComponentsCommonsLogEvents.CM_LOGGING)
            .user(ballotCastingKeyVerificationInput.getVotingCardId()).electionEvent(data.getElectionEventId())
            .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
            .additionalInfo("#confirmationMessage", ballotCastingKeyVerificationInput.getConfirmationMessage())
            .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
            .createLogInfo();
        secureLoggingWriter.log(Level.INFO, logInfo);
        auditLoggingWriter.log(Level.INFO, logInfo);
    }

    private void logConfirmationAttemptsExceeded(ChoiceCodeVerificationDTO<PartialCodesInput> data, int attempts) {
        BallotCastingKeyVerificationInput ballotCastingKeyVerificationInput =
            data.getPayload().getBallotCastingKeyVerificationInput();
        LogContent logInfo = new LogContent.LogContentBuilder()
            .logEvent(ControlComponentsCommonsLogEvents.ERROR_CONFIRMATION_ATTEMPTS_EXCEEDED)
            .user(ballotCastingKeyVerificationInput.getVotingCardId()).electionEvent(data.getElectionEventId())
            .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
            .additionalInfo("#confirmationMessage", ballotCastingKeyVerificationInput.getConfirmationMessage())
            .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
            .additionalInfo(ControlComponentsCommonsLogConstants.VOTE_CAST_ATTEMPTS, attempts + "").createLogInfo();
        secureLoggingWriter.log(Level.ERROR, logInfo);
    }

    private boolean checkCastCodeAttempts(ChoiceCodeVerificationDTO<PartialCodesInput> data) {
        try {
            computedVerificationCardsRepository.updateComputedVerificationCardVoteCastAttempt(data.getElectionEventId(),
                data.getVerificationCardId());

            final int attempts = computedVerificationCardsRepository
                .getComputeVerificationCardVoteCastAttempts(data.getElectionEventId(), data.getVerificationCardId());
            if (attempts > VOTE_CAST_MAX_ATTEMPTS) {
                logConfirmationAttemptsExceeded(data, attempts);
                return false;
            } else {
                logConfirmationAttemptsNotExceeded(data, attempts);
                return true;
            }
        } catch (ComputedVerificationCardNotFoundException e) {
            throw new IllegalStateException(e);
        }
    }

    private void logVerificationCardNotProcessed(ChoiceCodeVerificationDTO<PartialCodesInput> data) {
        LogContent logInfo = new LogContent.LogContentBuilder()
            .logEvent(ControlComponentsCommonsLogEvents.VERIFICATION_CARD_PROCESSED_CHECK)
            .user(data.getVerificationCardId()).electionEvent(data.getElectionEventId())
            .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
            .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
            .createLogInfo();
        secureLoggingWriter.log(Level.INFO, logInfo);
        auditLoggingWriter.log(Level.INFO, logInfo);
    }

    private void logVerificationCardAlreadyProcessedError(ChoiceCodeVerificationDTO<PartialCodesInput> data) {
        LogContent logInfo = new LogContent.LogContentBuilder()
            .logEvent(ControlComponentsCommonsLogEvents.ERROR_VERIFICATION_CARD_PROCESSED_CHECK)
            .user(data.getVerificationCardId()).electionEvent(data.getElectionEventId())
            .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
            .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
            .createLogInfo();
        secureLoggingWriter.log(Level.ERROR, logInfo);
    }

    private boolean isChoiceCodesComputation(ChoiceCodeVerificationDTO<PartialCodesInput> data) {
        return data.getPayload().getChoiceCodesVerificationInput() != null;
    }

    private void logChoiceCodesExponentiationProofSuccessfullyComputed(
            ChoiceCodeVerificationDTO<PartialCodesInput> data, List<BigInteger> codes, List<BigInteger> powers,
            Proof proof) throws GeneralCryptoLibException, JsonProcessingException {
        String verificationCardId = data.getVerificationCardId();
        String pcc = ObjectMappers.toJson(codes);
        String pccComp = ObjectMappers.toJson(powers);
        ArrayList<Exponent> proofExponents = new ArrayList<>();
        proofExponents.add(proof.getHashValue());
        proofExponents.addAll(proof.getValuesList());
        String pccProof = ObjectMappers.toJson(proofExponents);
        secureLoggingWriter.log(Level.INFO,
            new LogContent.LogContentBuilder()
                .logEvent(ControlComponentsCommonsLogEvents.PARTIAL_CODE_PROOF_KNOWLEDGE_EXPONENT_COMPUTED)
                .user(verificationCardId)
                .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                    serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                .additionalInfo(ControlComponentsCommonsLogConstants.PC, pcc)
                .additionalInfo(ControlComponentsCommonsLogConstants.COMPUTED_PC, pccComp)
                .additionalInfo(ControlComponentsCommonsLogConstants.PC_PROOF, pccProof)
                .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                .createLogInfo());
    }

    private void logPartialChoiceCodesSuccessfullyComputed(ChoiceCodeVerificationDTO<PartialCodesInput> data,
            List<BigInteger> powers) throws GeneralCryptoLibException, JsonProcessingException {
        String eeid = data.getElectionEventId();
        String verificationCardId = data.getVerificationCardId();
        List<BigInteger> codes = data.getPayload().getPartialCodesElements();
        String codesList = ObjectMappers.toJson(codes);
        String computedCodessList = ObjectMappers.toJson(powers);

        secureLoggingWriter.log(Level.INFO,
            new LogContent.LogContentBuilder().logEvent(ControlComponentsCommonsLogEvents.PARTIAL_CODE_COMPUTED)
                .user(verificationCardId).electionEvent(eeid)
                .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                    serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                .additionalInfo(ControlComponentsCommonsLogConstants.PC, codesList)
                .additionalInfo(ControlComponentsCommonsLogConstants.COMPUTED_PC, computedCodessList)
                .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                .createLogInfo());
    }

    private void logErrorPartialChoiceCodesCannotComputed(ChoiceCodeVerificationDTO<PartialCodesInput> data)
            throws GeneralCryptoLibException, JsonProcessingException {
        String eeid = data.getElectionEventId();
        String verificationCardId = data.getVerificationCardId();
        List<BigInteger> codes = data.getPayload().getPartialCodesElements();
        String pcc = ObjectMappers.toJson(codes);
        secureLoggingWriter.log(Level.ERROR,
            new LogContent.LogContentBuilder()
                .logEvent(ControlComponentsCommonsLogEvents.ERROR_PARTIAL_CODE_NOT_COMPUTED).user(verificationCardId)
                .electionEvent(eeid)
                .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                    serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                .additionalInfo(ControlComponentsCommonsLogConstants.PC, pcc)
                .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                .additionalInfo(ControlComponentsCommonsLogConstants.ERR_DESC,
                    ControlComponentsCommonsLogEvents.ERROR_PARTIAL_CODE_NOT_COMPUTED.getInfo())
                .createLogInfo());
    }

    private void logErrorProofKnowledgeExponentNotComputed(ChoiceCodeVerificationDTO<PartialCodesInput> data)
            throws GeneralCryptoLibException, JsonProcessingException {
        String eeid = data.getElectionEventId();
        String verificationCardId = data.getVerificationCardId();
        List<BigInteger> codes = data.getPayload().getPartialCodesElements();
        String pc = ObjectMappers.toJson(codes);
        secureLoggingWriter.log(Level.ERROR,
            new LogContent.LogContentBuilder()
                .logEvent(ControlComponentsCommonsLogEvents.ERROR_EXPONENT_PROOF_NOT_COMPUTED).user(verificationCardId)
                .electionEvent(eeid)
                .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                    serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                .additionalInfo(ControlComponentsCommonsLogConstants.PC, pc)
                .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                .additionalInfo(ControlComponentsCommonsLogConstants.ERR_DESC,
                    ControlComponentsCommonsLogEvents.ERROR_EXPONENT_PROOF_NOT_COMPUTED.getInfo())
                .createLogInfo());
    }

    private void logChoiceCodePrivateKeyDerivationFailed(ChoiceCodeVerificationDTO<PartialCodesInput> data) {
        String verificationCardId = data.getVerificationCardId();
        secureLoggingWriter.log(Level.ERROR,
            new LogContent.LogContentBuilder()
                .logEvent(ControlComponentsCommonsLogEvents.CHOICE_CODE_PRIVATE_KEY_DERIVED_COMP_FAILED)
                .objectId(verificationCardId)
                .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                    serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                .additionalInfo(ControlComponentsCommonsLogConstants.ERR_DESC,
                    "The derivation of the private key has failed")
                .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                .createLogInfo());
    }

    private void logChoiceCodePrivateKeySuccessfullyComputed(ChoiceCodeVerificationDTO<PartialCodesInput> data,
            Exponent derivedExponent, ZpSubgroup group) {
        try {
            String verificationCardId = data.getVerificationCardId();
            ZpGroupElement derivedPublicKey = derivePublicKey(group, derivedExponent);
            String derivedPublicKeyHash = getHashForLogFromPublicKey(derivedPublicKey);
            secureLoggingWriter.log(Level.INFO,
                new LogContent.LogContentBuilder().objectId(verificationCardId)
                    .logEvent(ControlComponentsCommonsLogEvents.CHOICE_CODE_PRIVATE_KEY_DERIVED_COMP)
                    .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                        serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                    .additionalInfo(ControlComponentsCommonsLogConstants.HASH_VOTER_CHOICE_CODE_GEN_PUBLIC_KEY,
                        derivedPublicKeyHash)
                    .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                    .createLogInfo());
        } catch (GeneralCryptoLibException e) {
            throw new IllegalArgumentException(e);
        }
    }

    private String getHashForLogFromPublicKey(ZpGroupElement derivedPublicKey) throws GeneralCryptoLibException {
        return Base64.getEncoder().encodeToString(primitivesService.getHash(derivedPublicKey.getValue().toByteArray()));
    }

    private void logVoteCastCodePrivateKeyDerivationFailed(ChoiceCodeVerificationDTO<PartialCodesInput> data) {
        String verificationCardId = data.getVerificationCardId();
        secureLoggingWriter.log(Level.ERROR,
            new LogContent.LogContentBuilder()
                .logEvent(ControlComponentsCommonsLogEvents.VOTE_CAST_CODE_EXPONENTIATION_KEY_DERIVED_COMP_FAILED)
                .objectId(verificationCardId)
                .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                    serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                .additionalInfo(ControlComponentsCommonsLogConstants.ERR_DESC,
                    "The derivation of the private key has failed")
                .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                .createLogInfo());
    }

    private void logVoteCastCodePrivateKeySuccessfullyComputed(ChoiceCodeVerificationDTO<PartialCodesInput> data,
            Exponent derivedExponent, ZpSubgroup group) {
        try {
            String verificationCardId = data.getVerificationCardId();
            ZpGroupElement derivedPublickey = derivePublicKey(group, derivedExponent);
            String derivedPublicKeyHash = getHashForLogFromPublicKey(derivedPublickey);
            secureLoggingWriter.log(Level.INFO,
                new LogContent.LogContentBuilder().objectId(verificationCardId)
                    .logEvent(ControlComponentsCommonsLogEvents.VOTE_CAST_CODE_EXPONENTIATION_KEY_DERIVED_COMP)
                    .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                        serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                    .additionalInfo(ControlComponentsCommonsLogConstants.HASH_VOTER_CHOICE_CODE_GEN_PUBLIC_KEY,
                        derivedPublicKeyHash)
                    .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                    .createLogInfo());
        } catch (GeneralCryptoLibException e) {
            throw new IllegalArgumentException(e);
        }
    }

    private void sendResponse(ChoiceCodeVerificationDTO<PartialCodesInput> data,
            PowersAndProof<BigInteger> choiceCodesAndProof, ZpGroupElement choiceCodesDerivedKeysJson,
            ZpGroupElement castCodeDerivedKeyJson)
            throws MessagingException, GeneralCryptoLibException, KeyNotFoundException, KeyManagementException,
            PayloadSignatureException, JsonProcessingException {
        ChoiceCodesVerificationResPayload payload = new ChoiceCodesVerificationResPayload();
        List<BigInteger> codes = data.getPayload().getPartialCodesElements();
        Map<BigInteger, BigInteger> primeToComputedPrime = new LinkedHashMap<>();
        for (int i = 0; i < codes.size(); i++) {
            primeToComputedPrime.put(codes.get(i), choiceCodesAndProof.powers().get(i));
        }
        payload.setPrimeToComputedPrime(primeToComputedPrime);
        payload.setExponentiationProofJson(choiceCodesAndProof.proof().toJson());
        payload.setChoiceCodesDerivedKeyJson(choiceCodesDerivedKeysJson != null ? Base64.getEncoder()
            .encodeToString(choiceCodesDerivedKeysJson.toJson().getBytes(StandardCharsets.UTF_8)) : null);
        payload.setCastCodeDerivedKeyJson(castCodeDerivedKeyJson != null
                ? Base64.getEncoder().encodeToString(castCodeDerivedKeyJson.toJson().getBytes(StandardCharsets.UTF_8))
                : null);

        UUID correlationId = data.getCorrelationId();
        String requestId = data.getRequestId();
        String eeid = data.getElectionEventId();
        String verificationCardSetId = data.getVerificationCardSetId();
        String verificationCardId = data.getVerificationCardId();
        ChoiceCodeVerificationDTO<ChoiceCodesVerificationResPayload> result = new ChoiceCodeVerificationDTO<>(
            correlationId, requestId, eeid, verificationCardSetId, verificationCardId, payload);
        try {
            payload.setSignature(payloadSigner.sign(payload, keyManager.getElectionSigningPrivateKey(eeid),
                keyManager.getElectionSigningCertificateChain(eeid)));
            logPartialCodesSigned(data, choiceCodesAndProof, result);
        } catch (PayloadSignatureException e) {
            logErrorPartialCodeProofNotSigned(data, choiceCodesAndProof);
            throw e;
        }

        messagingService.send(computationOutputQueue, result);
    }

    private void logPartialCodesSigned(ChoiceCodeVerificationDTO<PartialCodesInput> data,
            PowersAndProof<BigInteger> choiceCodesAndProof,
            ChoiceCodeVerificationDTO<ChoiceCodesVerificationResPayload> result) throws JsonProcessingException {
        String electionEventId = result.getElectionEventId();
        String verificationCardSetId = result.getVerificationCardSetId();
        String verificationCardId = result.getVerificationCardId();
        String partialCodes = ObjectMappers.toJson(data.getPayload().getPartialCodesElements());
        String computedPartialCodes = ObjectMappers.toJson(choiceCodesAndProof.powers());
        Proof proof = choiceCodesAndProof.proof();
        ArrayList<Exponent> proofExponents = new ArrayList<>();
        proofExponents.add(proof.getHashValue());
        proofExponents.addAll(proof.getValuesList());
        String computedPartialCodeProofs = ObjectMappers.toJson(proofExponents);
        String signature =
            Base64.getEncoder().encodeToString(result.getPayload().getSignature().getSignatureContents());

        secureLoggingWriter.log(Level.INFO,
            new LogContent.LogContentBuilder().objectId(verificationCardId).electionEvent(electionEventId)
                .logEvent(ControlComponentsCommonsLogEvents.PARTIAL_CODE_COMP_SIGN)
                .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                    serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                .additionalInfo(ControlComponentsCommonsLogConstants.VERIFICATION_CARD_SET_ID, verificationCardSetId)
                .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                .additionalInfo(ControlComponentsCommonsLogConstants.PC, partialCodes)
                .additionalInfo(ControlComponentsCommonsLogConstants.COMPUTED_PC, computedPartialCodes)
                .additionalInfo(ControlComponentsCommonsLogConstants.PC_PROOF, computedPartialCodeProofs)
                .additionalInfo(ControlComponentsCommonsLogConstants.PC_PROOF_SIGNATURE, signature).createLogInfo());
    }

    private void logErrorPartialCodeProofNotSigned(ChoiceCodeVerificationDTO<PartialCodesInput> data,
            PowersAndProof<BigInteger> choiceCodesAndProof) throws JsonProcessingException {
        String electionEventId = data.getElectionEventId();
        String verificationCardSetId = data.getVerificationCardSetId();
        String verificationCardId = data.getVerificationCardId();
        String partialCodes = ObjectMappers.toJson(data.getPayload().getPartialCodesElements());
        String computedPartialCodes = ObjectMappers.toJson(choiceCodesAndProof.powers());
        Proof proof = choiceCodesAndProof.proof();
        ArrayList<Exponent> proofExponents = new ArrayList<>();
        proofExponents.add(proof.getHashValue());
        proofExponents.addAll(proof.getValuesList());
        String computedPartialCodeProofs = ObjectMappers.toJson(proofExponents);

        secureLoggingWriter.log(Level.ERROR,
            new LogContent.LogContentBuilder().objectId(verificationCardId).electionEvent(electionEventId)
                .logEvent(ControlComponentsCommonsLogEvents.ERROR_PARTIAL_CODE_COMP_NOT_SIGNED)
                .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                    serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                .additionalInfo(ControlComponentsCommonsLogConstants.VERIFICATION_CARD_SET_ID, verificationCardSetId)
                .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                .additionalInfo(ControlComponentsCommonsLogConstants.PC, partialCodes)
                .additionalInfo(ControlComponentsCommonsLogConstants.COMPUTED_PC, computedPartialCodes)
                .additionalInfo(ControlComponentsCommonsLogConstants.PC_PROOF, computedPartialCodeProofs)
                .additionalInfo(ControlComponentsCommonsLogConstants.ERR_DESC,
                    ControlComponentsCommonsLogEvents.ERROR_PARTIAL_CODE_COMP_NOT_SIGNED.getInfo())
                .createLogInfo());
    }

}
