/*
 * 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.security.KeyManagementException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

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.proofs.ProofsServiceAPI;
import com.scytl.cryptolib.elgamal.bean.ElGamalPrivateKey;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.elgamal.bean.WitnessImpl;
import com.scytl.cryptolib.elgamal.cryptoapi.Witness;
import com.scytl.cryptolib.mathematical.groups.activity.ExponentCompressor;
import com.scytl.cryptolib.mathematical.groups.activity.GroupElementsCompressor;
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.keymanagement.KeyManager;
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.slogger.ServiceTransactionInfoProvider;
import com.scytl.products.ov.commons.beans.domain.model.messaging.PayloadSignature;
import com.scytl.products.ov.commons.beans.domain.model.messaging.PayloadSignatureException;
import com.scytl.products.ov.commons.dto.ChoiceCodeVerificationDTO;
import com.scytl.products.ov.commons.dto.ChoiceCodesVerificationDecryptResPayload;
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 ChoiceCodesDecryptionServiceImpl implements ChoiceCodesDecryptionService, MessageListener {

    private Logger LOG = LoggerFactory.getLogger("std");

    private Queue decryptionInputQueue;

    private Queue decryptionOutputQueue;

    @Autowired
    private GroupElementsCompressor<ZpGroupElement> groupElementsCompressor;

    @Autowired
    private ProofsServiceAPI proofsService;

    @Autowired
    private MessagingService messagingService;

    @Autowired
    ChoiceCodesKeyRepository choiceCodesKeyRepository;

    @Autowired
    private SecureLoggingWriter secureLoggingWriter;

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

    @Autowired
    private ServiceTransactionInfoProvider serviceTransactionInfoProvider;

    @Autowired
    private PayloadSigner payloadSigner;

    @Autowired
    private KeyManager keyManager;

    @Value("${queues.cv-dec.req}")
    public void setDecryptionInputQueue(String queueName) {
        decryptionInputQueue = new Queue(queueName);
    }

    @Value("${queues.cv-dec.res}")
    public void setDecryptionOutputQueue(String queueName) {
        decryptionOutputQueue = new Queue(queueName);
    }

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

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

    @Override
    public void onMessage(Object message) {
        @SuppressWarnings("unchecked")
        ChoiceCodeVerificationDTO<String> data = (ChoiceCodeVerificationDTO<String>) message;
        serviceTransactionInfoProvider.generate("tenantID", "clientIP", "serverIP", data.getRequestId());
        decrypt(data);
    }

    private void decrypt(ChoiceCodeVerificationDTO<String> data) {
        LOG.info("Decrypting choice codes");

        String requestId = data.getRequestId();
        String eeid = data.getElectionEventId();
        String verificationCardSetId = data.getVerificationCardSetId();
        String verificationCardId = data.getVerificationCardId();
        String gammaStr = data.getPayload();
        try {
            ZpGroupElement gamma = ZpGroupElement.fromJson(gammaStr);
            ElGamalPrivateKey decryptionPrivateKey =
                choiceCodesKeyRepository.getDecryptionPrivateKey(eeid, verificationCardSetId);

            List<Exponent> exponents = decryptionPrivateKey.getKeys();

            if (exponents.isEmpty()) {
                LOG.error("Choice Code Encryption private key did not contain any exponent");
                throw new IllegalStateException("Choice Code Encryption private key did not contain any exponent");
            }
            String pccc = gamma.getValue().toString();
            List<ZpGroupElement> exponentiatedGammas = new ArrayList<>(exponents.size());
            List<String> decryptContributionResult = new ArrayList<>(exponents.size());
            try {
                for (Exponent exponent : exponents) {
                    ZpGroupElement exponentiated = gamma.exponentiate(exponent);
                    exponentiatedGammas.add(exponentiated);
                    decryptContributionResult.add(exponentiated.toJson());
                }
            } catch (GeneralCryptoLibException e) {
                logErrorPartialDecryptionNotComputed(data, pccc);
                throw e;
            }

            String pdcc = ObjectMappers.toJson(exponentiatedGammas);
            logCodeDecryptionSuccessfullyComputed(verificationCardId, pccc, pdcc);

            ElGamalPublicKey choiceCodeNodeEncryptionPublicKey =
                choiceCodesKeyRepository.getDecryptionPublicKey(eeid, verificationCardSetId);
            Proof exponentiationProof;
            try {
                exponentiationProof = calculateDecryptionProof(decryptionPrivateKey, choiceCodeNodeEncryptionPublicKey,
                    gamma, exponentiatedGammas);
            } catch (GeneralCryptoLibException e) {
                logErrorKnowledgeProofNotComputed(verificationCardId, pccc, pdcc);
                throw e;
            }
            logCodeDecryptionExponentiationProofSuccessfullyComputed(verificationCardId, pccc, pdcc,
                exponentiationProof);

            ChoiceCodesVerificationDecryptResPayload resultPayload = new ChoiceCodesVerificationDecryptResPayload();

            resultPayload.setDecryptContributionResult(decryptContributionResult);
            resultPayload.setExponentiationProofJson(exponentiationProof.toJson());
            resultPayload.setPublicKeyJson(choiceCodeNodeEncryptionPublicKey.toJson());
            PayloadSignature payloadSignature;
            try {
                payloadSignature = payloadSigner.sign(resultPayload, keyManager.getElectionSigningPrivateKey(eeid),
                    keyManager.getElectionSigningCertificateChain(eeid));
                resultPayload.setSignature(payloadSignature);
            } catch (PayloadSignatureException e) {
                logErrorDecryptionProofNotSigned(eeid, verificationCardSetId, verificationCardId, pccc, pdcc,
                    exponentiationProof);
                throw e;
            }
            logCodeDecryptionSignature(eeid, verificationCardSetId, verificationCardId, pccc, pdcc, exponentiationProof,
                payloadSignature);

            UUID correlationId = data.getCorrelationId();

            ChoiceCodeVerificationDTO<ChoiceCodesVerificationDecryptResPayload> result =
                new ChoiceCodeVerificationDTO<>(correlationId, requestId, eeid, verificationCardSetId,
                    verificationCardId, resultPayload);

            messagingService.send(decryptionOutputQueue, result);

        } catch (GeneralCryptoLibException | JsonProcessingException e) {
            LOG.error("Failed to obtain the decryption input parameters", e);
        } catch (MessagingException e) {
            LOG.error("Failed to send choice codes decryption result", e);
        } catch (KeyManagementException e) {
            LOG.error("Failed to get the decryption private key from the key repository", e);
        } catch (PayloadSignatureException e) {
            LOG.error("Failed to sign choice codes decryption result", e);
        }
    }

    private void logErrorPartialDecryptionNotComputed(ChoiceCodeVerificationDTO<String> data, String pccc) {
        String requestId = data.getRequestId();
        String eeid = data.getElectionEventId();
        String verificationCardId = data.getVerificationCardId();
        secureLoggingWriter
            .log(Level.ERROR,
                new LogContent.LogContentBuilder()
                    .logEvent(ControlComponentsCommonsLogEvents.ERROR_PARTIAL_DECRYPTION_NOT_COMPUTED)
                    .user(verificationCardId).additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID, requestId)
                    .electionEvent(eeid).additionalInfo(ControlComponentsCommonsLogConstants.PCCC, pccc)
                    .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                    .additionalInfo(ControlComponentsCommonsLogConstants.ERR_DESC,
                        ControlComponentsCommonsLogEvents.ERROR_PARTIAL_DECRYPTION_NOT_COMPUTED.getInfo())
                    .createLogInfo());

    }

    private void logCodeDecryptionSuccessfullyComputed(String verificationCardId, String pccc, String pdcc) {
        // [SL PDEC-1]
        // Partial Decryption -- Partial decryption successfully computed
        secureLoggingWriter.log(Level.INFO,
            new LogContent.LogContentBuilder().logEvent(ControlComponentsCommonsLogEvents.PARTIAL_DECRYPTION_COMPUTED)
                .user(verificationCardId)
                .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                    serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                .additionalInfo(ControlComponentsCommonsLogConstants.PCCC, pccc)
                .additionalInfo(ControlComponentsCommonsLogConstants.PD_CC, pdcc)
                .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                .createLogInfo());
    }

    private void logCodeDecryptionExponentiationProofSuccessfullyComputed(String verificationCardId, String pccc,
            String pdcc, Proof exponentiationProof) throws GeneralCryptoLibException, JsonProcessingException {
        // [SL] pcccomp hash of the encrypted partial choice code
        // computed
        // [SL PCCCOMP-5]
        // Partial Choice Codes computation -- Proof of knowledge of the
        // exponent for computing partial choice codes successfully computed
        // Secure Log placed here because is easier to get log info
        ArrayList<Exponent> exponentiatonProofExponents = new ArrayList<>();
        exponentiatonProofExponents.add(exponentiationProof.getHashValue());
        exponentiatonProofExponents.addAll(exponentiationProof.getValuesList());
        String pccProof = ObjectMappers.toJson(exponentiatonProofExponents);

        // [SL] proof of knowledge
        secureLoggingWriter.log(Level.INFO,
            new LogContent.LogContentBuilder()
                .logEvent(ControlComponentsCommonsLogEvents.PARTIAL_DECRYPTION_PROOF_KNOWLEDGE_EXPONENT_COMPUTED)
                .user(verificationCardId)
                .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                    serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                .additionalInfo(ControlComponentsCommonsLogConstants.PCCC, pccc)
                .additionalInfo(ControlComponentsCommonsLogConstants.PD_CC, pdcc)
                .additionalInfo(ControlComponentsCommonsLogConstants.DEC_CC_PROOF, pccProof)
                .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                .createLogInfo());
    }

    private void logErrorKnowledgeProofNotComputed(String verificationCardId, String pccc, String pdcc)
            throws GeneralCryptoLibException {

        // [SL] proof of knowledge
        secureLoggingWriter
            .log(Level.ERROR,
                new LogContent.LogContentBuilder()
                    .logEvent(ControlComponentsCommonsLogEvents.ERROR_KNOWLEDGE_PROOF_NOT_COMPUTED)
                    .user(verificationCardId)
                    .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                        serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                    .additionalInfo(ControlComponentsCommonsLogConstants.PCCC, pccc)
                    .additionalInfo(ControlComponentsCommonsLogConstants.PD_CC, pdcc)
                    .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                    .additionalInfo(ControlComponentsCommonsLogConstants.ERR_DESC,
                        ControlComponentsCommonsLogEvents.ERROR_KNOWLEDGE_PROOF_NOT_COMPUTED.getInfo())
                    .createLogInfo());
    }

    private void logCodeDecryptionSignature(String eeid, String verificationCardSetId, String verificationCardId,
            String pccc, String pdcc, Proof exponentiationProof, PayloadSignature signature)
            throws JsonProcessingException {

        ArrayList<Exponent> exponentiatonProofExponents = new ArrayList<>();
        exponentiatonProofExponents.add(exponentiationProof.getHashValue());
        exponentiatonProofExponents.addAll(exponentiationProof.getValuesList());
        String pccProof = ObjectMappers.toJson(exponentiatonProofExponents);

        secureLoggingWriter.log(Level.INFO,
            new LogContent.LogContentBuilder().logEvent(ControlComponentsCommonsLogEvents.PARTIAL_DECRYPTION_SIGN)
                .user(verificationCardId).electionEvent(eeid).user(verificationCardId)
                .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                    serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                .additionalInfo(ControlComponentsCommonsLogConstants.PCCC, pccc)
                .additionalInfo(ControlComponentsCommonsLogConstants.PD_CC, pdcc)
                .additionalInfo(ControlComponentsCommonsLogConstants.DEC_CC_PROOF, pccProof)
                .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                .additionalInfo(ControlComponentsCommonsLogConstants.VERIFICATION_CARD_SET_ID, verificationCardSetId)
                .createLogInfo());
    }

    private void logErrorDecryptionProofNotSigned(String electionEventId, String verificationCardSetId,
            String verificationCardId, String pccc, String pdcc, Proof exponentiationProof)
            throws JsonProcessingException {

        ArrayList<Exponent> exponentiatonProofExponents = new ArrayList<>();
        exponentiatonProofExponents.add(exponentiationProof.getHashValue());
        exponentiatonProofExponents.addAll(exponentiationProof.getValuesList());
        String pccProof = ObjectMappers.toJson(exponentiatonProofExponents);

        secureLoggingWriter
            .log(Level.ERROR,
                new LogContent.LogContentBuilder()
                    .logEvent(ControlComponentsCommonsLogEvents.ERROR_PARTIAL_DECRYPTION_PROOF_NOT_SIGNED)
                    .user(verificationCardId).electionEvent(electionEventId).user(verificationCardId)
                    .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                        serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
                    .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
                    .additionalInfo(ControlComponentsCommonsLogConstants.VERIFICATION_CARD_SET_ID,
                        verificationCardSetId)
                    .additionalInfo(ControlComponentsCommonsLogConstants.PCCC, pccc)
                    .additionalInfo(ControlComponentsCommonsLogConstants.PD_CC, pdcc)
                    .additionalInfo(ControlComponentsCommonsLogConstants.DEC_CC_PROOF, pccProof)
                    .additionalInfo(ControlComponentsCommonsLogConstants.ERR_DESC,
                        ControlComponentsCommonsLogEvents.ERROR_PARTIAL_DECRYPTION_PROOF_NOT_SIGNED.getInfo())
                    .createLogInfo());
    }

    /**
     * Calculate the Zero-Knowledge Proof for the exponentiation operation that
     * has been done to partially decrypt the gamma value. For the sake of
     * message size, the proof is not calculated over the exponentiation for
     * each of the private key elements, but over the result of compressing all
     * the elements. Therefore, to validate this proof - in particular, the
     * generator base element exponentiation - the result of compressing all the
     * elements of the public key will have to be used.
     *
     * @param decryptionPrivateKey
     *            Private key used for the exponentiation operation
     * @param choiceCodeNodeEncryptionPublicKey
     *            public pair to decryptionPrivateKey
     * @param gamma
     *            base exponentiated value
     * @param exponentiatedGammas
     *            result of exponentiating the base value to the private key
     *            elements
     * @return the calculated exponentiation proof
     * @throws GeneralCryptoLibException
     *             if there is an error during the cryptographic operations
     */
    private Proof calculateDecryptionProof(ElGamalPrivateKey decryptionPrivateKey,
            ElGamalPublicKey choiceCodeNodeEncryptionPublicKey, ZpGroupElement gamma,
            List<ZpGroupElement> exponentiatedGammas) throws GeneralCryptoLibException {
        ZpGroupElement compressedExponentiatedGammas = groupElementsCompressor.compress(exponentiatedGammas);

        List<ZpGroupElement> publicKeyElements = choiceCodeNodeEncryptionPublicKey.getKeys();

        ZpGroupElement compressedPublicKeyElements = groupElementsCompressor.compress(publicKeyElements);

        ZpSubgroup privateKeyGroup = decryptionPrivateKey.getGroup();

        ExponentCompressor<ZpSubgroup> exponentCompressor = new ExponentCompressor<>(privateKeyGroup);

        List<Exponent> privateKeyElements = decryptionPrivateKey.getKeys();

        Exponent compressedPrivateKey = exponentCompressor.compress(privateKeyElements);

        List<ZpGroupElement> exponentiatedElements =
            Arrays.asList(compressedPublicKeyElements, compressedExponentiatedGammas);
        List<ZpGroupElement> baseElements = Arrays.asList(privateKeyGroup.getGenerator(), gamma);
        Witness witness = new WitnessImpl(compressedPrivateKey);

        return proofsService.createProofProverAPI(privateKeyGroup).createExponentiationProof(exponentiatedElements,
            baseElements, witness);

    }

}
