/*
 * 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.mixing.service;

import java.security.KeyManagementException;
import java.util.List;
import java.util.function.Supplier;

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.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.mathematical.groups.activity.GroupElementsCompressor;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
import com.scytl.products.ov.channel.cc.commons.validations.MissingSignatureException;
import com.scytl.products.ov.channel.cc.mixing.service.conversion.ConverterException;
import com.scytl.products.ov.channel.cc.mixing.service.conversion.MixingOutputConverter;
import com.scytl.products.ov.channel.cc.mixing.service.logging.SecureLoggingHelperFactory;
import com.scytl.products.ov.commons.beans.domain.model.messaging.InvalidSignatureException;
import com.scytl.products.ov.commons.beans.domain.model.messaging.PayloadSignatureException;
import com.scytl.products.ov.commons.beans.domain.model.messaging.PayloadVerificationException;
import com.scytl.products.ov.commons.beans.domain.model.vote.VoteSetId;
import com.scytl.products.ov.commons.beans.exceptions.LambdaException;
import com.scytl.products.ov.commons.dto.MixingDTO;
import com.scytl.products.ov.commons.dto.mixing.MixingPayload;
import com.scytl.products.ov.commons.sign.PayloadSigner;
import com.scytl.products.ov.commons.verify.PayloadVerifier;
import com.scytl.products.ov.mixnet.commons.configuration.ApplicationConfig;
import com.scytl.products.ov.mixnet.commons.configuration.locations.LocationsProvider;
import com.scytl.products.ov.mixnet.commons.exceptions.MixingException;
import com.scytl.products.ov.mixnet.commons.exceptions.VerifierException;
import com.scytl.products.ov.mixnet.mvc.MixingController;
import com.scytl.products.ov.mixnet.mvc.VerifyingController;

@Service
public class InternalMixDecryptServiceImpl implements InternalMixDecryptService {

    private static final String ERROR_MESSAGE_TEMPLATE = "Mixing of vote set {} failed when {}";

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

    @Value("${queues.md-mixdec.req}")
    private String queueName;

    @Value("${keys.nodeId}")
    private String nodeName;

    @Autowired
    private MixingPropertiesProvider mixingPropertiesProvider;

    @Autowired
    private KeyRepository keyRepository;

    @Autowired
    private MixingController mixingController;

    @Autowired
    private VerifyingController verifyingController;

    @Autowired
    private GroupElementsCompressor<ZpGroupElement> groupElementsCompressor;

    @Autowired
    private PayloadSigner payloadSigner;

    @Autowired
    private PayloadVerifier payloadVerifier;

    @Autowired
    private SecureLoggingHelperFactory secureLoggingHelperFactory;

    @Autowired
    private MixingOutputConverter mixingOutputConverter;

    @Override
    public void executeMixing(MixingDTO dto) {
        MixingPayload inputPayload = dto.getPayload();
        VoteSetId voteSetId = inputPayload.getVoteSetId();

        // Initialise secure logging.
        SecureLoggingHelper secureLoggingHelper =
            secureLoggingHelperFactory.create(dto.getRequestId(), inputPayload.getVoteSetId());

        try {
            validateSignature(inputPayload);
            secureLoggingHelper.logBallotBoxSignatureOutcome(inputPayload, true);

            LOG.debug("Starting the mixing and decryption of vote set {}", voteSetId);

            ApplicationConfig applicationConfig = mixingPropertiesProvider.createApplicationConfigforBallotBox(dto);

            // ------8<--------8<--------8<--------8<--------8<--------8<-------
            // WARNING!
            //
            // The load balancer configuration is manipulated to ensure that
            // only one batch is ever produced, in order to sidestep the
            // associated maintenance overhead without having to remove the
            // feature just yet.
            //
            // Currently the minBatchSize is set to an acceptable value, this
            // will be made configurable in another PR.

            int minBatchSize = Integer.MAX_VALUE / 2;
            applicationConfig.setNumberOfParallelBatches(1);
            applicationConfig.setMinimumBatchSize(minBatchSize);

            // ------8<--------8<--------8<--------8<--------8<--------8<-------
            LocationsProvider locationsProvider = LocationsProvider.build(applicationConfig);
            // Run the mixing.

            // Perform the mixing. The results will be accessible using the
            // locations provider.
            mixingController.mix(applicationConfig, locationsProvider);

            // Verify the outcome of the mixing.
            verifyingController.verify(applicationConfig, locationsProvider);

            LOG.info("Mixing of {} finished, gathering results...", dto.getPayload().getVoteSetId());

            // Replace the DTO with a new one built from the mixing results.
            updateDTO(dto, locationsProvider, secureLoggingHelper);
        } catch (InvalidSignatureException invalidSignatureException) {
            secureLoggingHelper.logBallotBoxSignatureOutcome(inputPayload, false);
            LOG.error("The payload signature is not valid", invalidSignatureException);
            dto.setError(invalidSignatureException);
        } catch (MissingSignatureException missingSignatureException) {
            secureLoggingHelper.logBallotBoxSignatureOutcome(inputPayload, false);
            LOG.error("The payload signature is missing", missingSignatureException);
            dto.setError(missingSignatureException);
        } catch (MixingException mixingException) {
            LOG.error(ERROR_MESSAGE_TEMPLATE, voteSetId, "mixing", mixingException);
            dto.setError(mixingException);
        } catch (VerifierException verifierException) {
            LOG.error(ERROR_MESSAGE_TEMPLATE, voteSetId, "verification", verifierException);
            dto.setError(verifierException);
        } catch (Throwable error) {
            // This catch clause is intentionally wide; errors must always go
            // into a DTO.
            LOG.error(ERROR_MESSAGE_TEMPLATE, voteSetId, "building mixing response", error);
            dto.setError(error);
        }
    }

    /**
     * Construct an output mixing DTO from the input DTO and the output payload
     *
     * @param dto
     *            the input DTO
     * @param locationsProvider
     *            the locations of the input data
     * @param secureLoggingHelper
     *            an instance of the secure logging helper, to be able to commit
     *            secure log messages at the appropriate moment
     * @return the output DTO
     */
    private void updateDTO(MixingDTO dto, LocationsProvider locationsProvider, SecureLoggingHelper secureLoggingHelper)
            throws ConverterException, PayloadSignatureException, KeyManagementException {

        // Extract the results of the mixing into a mixing payload.
        MixingPayload outputPayload = createOutputPayload(dto.getPayload(), locationsProvider, secureLoggingHelper);

        // Sign the generated payload.
        signOutputPayload(outputPayload, secureLoggingHelper);

        // Build a new DTO with the signed output payload.
        LOG.info("Updating mixing DTO with payload {}...", outputPayload.getVoteSetId());
        dto.update(queueName, nodeName, outputPayload);
    }

    /**
     * Signs an output mixing payload.
     *
     * @param outputPayload
     *            the payload to be signed
     * @param secureLoggingHelper
     *            the secure logging helper that will log the outcome of the
     *            operation
     */
    private void signOutputPayload(MixingPayload outputPayload, SecureLoggingHelper secureLoggingHelper)
            throws PayloadSignatureException, KeyManagementException {
        try {
            String electionEventId = outputPayload.getVoteSetId().getBallotBoxId().getElectionEventId();

            LOG.debug("Signing output payload {}...", outputPayload.getVoteSetId());
            outputPayload.setSignature(payloadSigner.sign(outputPayload, keyRepository.getSigningKey(electionEventId),
                keyRepository.getVerificationCertificateChain(electionEventId)));
            LOG.info("Output payload {} signed.", outputPayload.getVoteSetId());
        } finally {
            secureLoggingHelper.logVoteSetSignedOutcome(outputPayload);
        }
    }

    /**
     * Creates an output mixing payload from the results of the mixing process.
     *
     * @param inputPayload
     *            the initial data that was passed to the mixing process
     * @param locationsProvider
     *            the mixing process outputs
     * @param secureLoggingHelper
     *            the secure logging helper
     * @return the output mixing payload
     */
    private MixingPayload createOutputPayload(MixingPayload inputPayload, LocationsProvider locationsProvider,
            SecureLoggingHelper secureLoggingHelper)
            throws ConverterException {
        MixingPayload outputPayload = null;
        try {
            String electoralAuthorityId = inputPayload.getElectoralAuthorityId();
            String electionEventId = inputPayload.getVoteSetId().getBallotBoxId().getElectionEventId();

            // Create a supplier that removes the used part of the combined vote
            // encryption key.
            Supplier<ElGamalPublicKey> remainingVoteEncryptionKeySupplier =
                () -> dropUsedVoteEncryptionKey(secureLoggingHelper, inputPayload.getVoteEncryptionKey(),
                    electionEventId, electoralAuthorityId);

            // Compile the output of the mixing into a mixing payload.
            outputPayload =
                mixingOutputConverter.convert(inputPayload, locationsProvider, remainingVoteEncryptionKeySupplier);
        } finally {
            secureLoggingHelper.logMixingAndDecryptingOutcome(inputPayload, outputPayload);
        }

        return outputPayload;
    }

    /**
     * Removes the used part of the combined vote encryption key.
     *
     * @param secureLoggingHelper
     *            secure logging helper to log the outcome of the operation.
     * @param combinedVoteEncryptionKey
     *            the combined vote encryption key
     * @param electionEventId
     *            the election linked to the electoral authority
     * @param electoralAuthorityId
     *            the electoral authority the used key is associated with
     * @return the vote encryption key minus the used part
     */
    private ElGamalPublicKey dropUsedVoteEncryptionKey(SecureLoggingHelper secureLoggingHelper,
            ElGamalPublicKey combinedVoteEncryptionKey, String electionEventId, String electoralAuthorityId) {
        ElGamalPublicKey remainingVoteEncryptionKey = null;

        try {
            // Find the current electoral authority's vote encryption key.
            ElGamalPublicKey electoralAuthorityVoteEncryptionKey =
                keyRepository.getBallotEncryptionKey(electionEventId, electoralAuthorityId);

            int maxSubKeys = combinedVoteEncryptionKey.getKeys().size();

            // Compress keys if necessary.
            List<ZpGroupElement> compressedList = groupElementsCompressor
                .buildListWithCompressedFinalElement(maxSubKeys, electoralAuthorityVoteEncryptionKey.getKeys());
            ElGamalPublicKey usedVoteEncryptionKey =
                new ElGamalPublicKey(compressedList, electoralAuthorityVoteEncryptionKey.getGroup());

            // Remove the used key from the combined key.
            remainingVoteEncryptionKey = combinedVoteEncryptionKey.divide(usedVoteEncryptionKey);
        } catch (GeneralCryptoLibException | KeyManagementException e) {
            throw new LambdaException(e);
        } finally {
            secureLoggingHelper.logVoteEncryptionKeyRemovalOutcome(electoralAuthorityId, combinedVoteEncryptionKey,
                remainingVoteEncryptionKey);
        }

        return remainingVoteEncryptionKey;
    }

    /**
     * Ensures that a payload's signature is valid.
     *
     * @param payload
     *            the payload to validate
     */
    private void validateSignature(MixingPayload payload)
            throws KeyManagementException, InvalidSignatureException, MissingSignatureException,
            PayloadVerificationException {
        VoteSetId voteSetId = payload.getVoteSetId();

        LOG.info("Checking the signature of payload {}...", voteSetId);

        if (payload.getSignature() == null) {
            throw new MissingSignatureException(voteSetId.toString());
        }

        if (!payloadVerifier.isValid(payload, keyRepository.getPlatformCACertificate())) {
            throw new InvalidSignatureException(voteSetId);
        }

        LOG.info("Vote set {} accepted for mixing", voteSetId);
    }
}
