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

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.scytl.cryptolib.api.elgamal.ElGamalServiceAPI;
import com.scytl.cryptolib.api.exceptions.CryptoLibException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.api.primitives.PrimitivesServiceAPI;
import com.scytl.cryptolib.api.proofs.ProofsServiceAPI;
import com.scytl.cryptolib.elgamal.bean.ElGamalComputationsValues;
import com.scytl.cryptolib.elgamal.bean.ElGamalPrivateKey;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.elgamal.cryptoapi.Ciphertext;
import com.scytl.cryptolib.elgamal.cryptoapi.CryptoAPIElGamalDecrypter;
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.cryptoapi.ProofProverAPI;
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.oscore.logging.core.provider.TransactionInfoProvider;
import com.scytl.products.ov.mixnet.commons.beans.VoteWithProof;
import com.scytl.products.ov.mixnet.commons.constants.Constants;
import com.scytl.products.ov.mixnet.commons.tools.ElGamalHelper;
import com.scytl.products.ov.mixnet.infrastructure.log.DecryptionLogConstants;
import com.scytl.products.ov.mixnet.infrastructure.log.DecryptionLogEvents;

/**
 * Decrypts a list of ballots one by one. A more complex implementation might use a batch process or queues to
 * parallelise work.
 */
public abstract class AbstractBallotDecryptionService implements BallotDecryptionService {

    private static final Logger LOG = LoggerFactory.getLogger(AbstractBallotDecryptionService.class);

    private final TransactionInfoProvider transactionInfoProvider;

    private final PrimitivesServiceAPI primitivesService;

    private final ElGamalServiceAPI elGamalService;

    private final ProofsServiceAPI proofsService;

    private final SecureLoggingWriter secureLog;

    public AbstractBallotDecryptionService(TransactionInfoProvider transactionInfoProvider,
            PrimitivesServiceAPI primitivesService, ElGamalServiceAPI elGamalService, ProofsServiceAPI proofsService, 
            SecureLoggingWriter secureLog) {
        this.transactionInfoProvider = transactionInfoProvider;
        this.primitivesService = primitivesService;
        this.elGamalService = elGamalService;
        this.proofsService = proofsService;
        this.secureLog = secureLog;
    }

    /**
     * Decrypts ballots.
     *
     * @param encryptedBallots
     *            the encrypted ballots
     */
    @Override
    public List<VoteWithProof> decrypt(final List<Ciphertext> encryptedBallots, final ElGamalPrivateKey ballotDecryptionKey,
            final int batchNum, final int partitionSize, final int offset) {
        if (transactionInfoProvider.get() == null) {
            // Running in a new thread, create a new transaction info object.
            transactionInfoProvider.generate(Constants.LOGGING_TENANT, Constants.LOGGING_LOCALHOST,
                Constants.LOGGING_LOCALHOST);
        }

        // Reset the decrypter and prover services with the received keys.
        try {
            CryptoAPIElGamalDecrypter decrypter = elGamalService.createDecrypter(ballotDecryptionKey);
            ProofProverAPI prover = proofsService.createProofProverAPI(ballotDecryptionKey.getGroup());
            ElGamalPublicKey publicKey = computeElGamalPublicKey(ballotDecryptionKey);

            List<VoteWithProof> votesWithProof = new ArrayList<>(encryptedBallots.size());
            for (Ciphertext ballot : encryptedBallots) {
                votesWithProof.add(processItem(ballot, publicKey, ballotDecryptionKey, decrypter, prover));
            }
            
            return votesWithProof;
        } catch (GeneralCryptoLibException e) {
            throw new CryptoLibException("The ballot decryption service failed to process an item", e);
        }
    }

    private static ElGamalPublicKey computeElGamalPublicKey(
            ElGamalPrivateKey privateKey) throws GeneralCryptoLibException {
        List<Exponent> exponents = privateKey.getKeys();
        ZpSubgroup group = privateKey.getGroup();
        ZpGroupElement generator = group.getGenerator();
        List<ZpGroupElement> elements = new ArrayList<>(exponents.size());
        for (Exponent exponent : exponents) {
            elements.add(generator.exponentiate(exponent));
        }
        return new ElGamalPublicKey(elements , group);
    }

    /**
     * Decrypts a ballot and generates a proof of the process.
     */
    private VoteWithProof processItem(final Ciphertext encryptedBallot, final ElGamalPublicKey publicKey,
            final ElGamalPrivateKey ballotDecryptionKey, final CryptoAPIElGamalDecrypter decrypter,
            final ProofProverAPI prover) {
        VoteWithProof voteWithProof;
        List<ZpGroupElement> results;
        List<BigInteger> decryptedVotes;
        List<ZpGroupElement> plaintexts;
        List<ZpGroupElement> groupElements = null;

        if (transactionInfoProvider.get() == null) {
            // Running in a new thread, create a new transaction info object.
            transactionInfoProvider.generate(Constants.LOGGING_TENANT, Constants.LOGGING_LOCALHOST,
                Constants.LOGGING_LOCALHOST);
        }

        try {
            LOG.debug("Decrypting ballot...");

            // Convert the encrypted ballot to ElGamal computations values,
            // required
            // by the decrypter.
            groupElements = getGroupElements(encryptedBallot);
            ElGamalComputationsValues values =
                new ElGamalComputationsValues(groupElements.get(0), groupElements.subList(1, groupElements.size()));

            // Decrypt the ballot.
            results = decrypter.decrypt(values, true);
            results = provideDecryptionFinalResults(values, results);

            // Extract the votes and plaintexts from the ballot.
            decryptedVotes = new ArrayList<>(results.size());
            plaintexts = new ArrayList<>(results.size());
            for (ZpGroupElement zpGroupElement : results) {
                BigInteger decryptedVote = zpGroupElement.getValue();
                decryptedVotes.add(decryptedVote);
                plaintexts.add(new ZpGroupElement(decryptedVote, ballotDecryptionKey.getGroup()));
            }
            LOG.debug("Votes decrypted successfully.");
        } catch (GeneralCryptoLibException e) {
            logError(e, generateHash(groupElements), DecryptionLogEvents.RECEBPK_ERROR_DECRYPTING_VOTE);
            throw new CryptoLibException(e);
        }
        
        try {
            LOG.debug("Generating decryption proof...");

            // Transform from one ElGamalEncryptedBallot implementation to the
            // one
            // required by the prover.
            Ciphertext ciphertext =
                com.scytl.products.ov.mixnet.commons.beans.ElGamalEncryptedBallot.create(groupElements);

            // Generate a proof for the votes.
            Proof proof =
                prover.createDecryptionProof(publicKey, ciphertext, provideProofInput(plaintexts), ballotDecryptionKey);
            voteWithProof = new VoteWithProof(ciphertext, decryptedVotes, proof.toJson());
            LOG.debug("Decryption proof created successfully.");

            return voteWithProof;
        } catch (GeneralCryptoLibException e) {
            logError(e, generateHash(groupElements), DecryptionLogEvents.RECEBPK_ERROR_GENERATING_DECRYPTION_PROOF);
            throw new CryptoLibException(e);
        }
    }

    protected abstract List<ZpGroupElement> provideProofInput(List<ZpGroupElement> plaintexts)
            throws GeneralCryptoLibException;

    protected abstract List<ZpGroupElement> provideDecryptionFinalResults(ElGamalComputationsValues values,
            List<ZpGroupElement> results) throws GeneralCryptoLibException;

    /**
     * Extract Zp group elements from an encrypted ballot.
     *
     * @return a list of the group elements, fronted by the gamma element and followed by the phi elements.
     */
    private static List<ZpGroupElement> getGroupElements(Ciphertext encryptedBallot) throws GeneralCryptoLibException {
        List<ZpGroupElement> groupElements = new ArrayList<>();

        groupElements.add(encryptedBallot.getGamma());
        groupElements.addAll(encryptedBallot.getPhis());
        return groupElements;
    }

    /**
     * Commit the details of an error condition to the secure log.
     *
     * @param e
     *            the error
     * @param hash
     *            a hash of the encrypted ballot
     * @param event
     *            an identifier of the operation that failed
     */
    private void logError(GeneralCryptoLibException e, String hash, DecryptionLogEvents event) {
        secureLog.log(Level.WARN,
            new LogContent.LogContentBuilder().logEvent(event).user("adminID")
                .additionalInfo(DecryptionLogConstants.INFO_HASH_VOTE, hash)
                .additionalInfo(DecryptionLogConstants.INFO_ERR_DESC, ExceptionUtils.getRootCauseMessage(e))
                .createLogInfo());
    }

    /**
     * Get a hash of the encrypted ballot for log messages.
     *
     * @param encryptedBallot
     *            an encrypted ballot
     * @return a hash based on the values of the encrypted ballot
     */
    private String generateHash(List<ZpGroupElement> encryptedBallot) {
        try {
            byte[] hash = primitivesService.getHash(ElGamalHelper.concatValues(encryptedBallot));
            return Base64.getEncoder().encodeToString(hash);
        } catch (GeneralCryptoLibException e) {
            // Wrap in an unchecked exception as this code runs inside a lambda.
            throw new CryptoLibException(e);
        }
    }
}
