/*
 * 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.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.elgamal.cryptoapi.Ciphertext;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;
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.mixnet.commons.ballots.ElGamalEncryptedBallots;
import com.scytl.products.ov.mixnet.commons.beans.proofs.ShuffleProof;
import com.scytl.products.ov.mixnet.commons.configuration.BGParams;
import com.scytl.products.ov.mixnet.commons.configuration.BGParamsProvider;
import com.scytl.products.ov.mixnet.commons.configuration.locations.LocationsProvider;
import com.scytl.products.ov.mixnet.commons.exceptions.VerifierException;
import com.scytl.products.ov.mixnet.commons.homomorphic.impl.GjosteenElGamal;
import com.scytl.products.ov.mixnet.commons.io.CommitmentParamsReader;
import com.scytl.products.ov.mixnet.commons.io.ElGamalEncryptedBallotsLoader;
import com.scytl.products.ov.mixnet.commons.io.ElgamalPublicKeyReader;
import com.scytl.products.ov.mixnet.commons.io.JSONProofsReader;
import com.scytl.products.ov.mixnet.commons.io.ZpGroupReader;
import com.scytl.products.ov.mixnet.commons.proofs.bg.commitments.CommitmentParams;
import com.scytl.products.ov.mixnet.commons.tools.CiphertextTools;
import com.scytl.products.ov.mixnet.commons.tools.MatrixArranger;
import com.scytl.products.ov.mixnet.commons.tools.MultiExponentiation;
import com.scytl.products.ov.mixnet.commons.tools.MultiExponentiationImpl;
import com.scytl.products.ov.mixnet.commons.validation.EncryptedBallotsDuplicationValidator;
import com.scytl.products.ov.mixnet.commons.validation.EncryptedBallotsValidator;
import com.scytl.products.ov.mixnet.commons.validation.EncryptedBallotsValidatorManager;
import com.scytl.products.ov.mixnet.infrastructure.log.MixingAndVerifyingLogEvents;
import com.scytl.products.ov.mixnet.proofs.bg.shuffle.ShuffleProofVerifier;

public class BGVerifier {

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

    private final JSONProofsReader proofsReader;

    private final BGParamsProvider bGParamsProvider;

    private ElGamalEncryptedBallotsLoader elgamalEncryptedBallotsLoader;

    private ZpSubgroup zpGroup;

    private ElGamalPublicKey publicKey;

    private CiphertextTools ciphertextTools;

    private GjosteenElGamal gjosteenElGamal;

    private MultiExponentiation multiExponentiation;

    private final SecureLoggingWriter loggingWriter;

    /**
     * Constructor.
     * 
     * @param loggingWriter
     */
    public BGVerifier(SecureLoggingWriter loggingWriter) {
        this(new BGParamsProvider(), new JSONProofsReader(), loggingWriter);
    }
    
    /**
     * Constructor. For internal use only.
     * 
     * @param bGParamsProvider
     * @param proofsReader
     * @param loggingWriter 
     */
    BGVerifier(final BGParamsProvider bGParamsProvider, final JSONProofsReader proofsReader, SecureLoggingWriter loggingWriter) {
        this.bGParamsProvider = bGParamsProvider;
        this.proofsReader = proofsReader;
        this.loggingWriter = loggingWriter;
    }

	public boolean verify(final LocationsProvider locationsProvider) throws VerifierException {

        final Map<Integer, Boolean> result = new HashMap<>();
        Boolean verified;

        for (int i = 1; i <= locationsProvider.getVerifierLocationsProvider().getEncryptedBallotsInputNumber(); i++) {
            int batchName = i;
            try {
                initializeBatch(batchName, locationsProvider);

                final ElGamalEncryptedBallots encryptedBallots = elgamalEncryptedBallotsLoader.loadCSV(zpGroup,
                    locationsProvider.getVerifierLocationsProvider().getEncryptedBallotsInputOfBatch(batchName));

                final int encryptedBallotsSize = encryptedBallots.getBallots().size();

                final BGParams batchBGParams = bGParamsProvider.getParamsForGiven(encryptedBallotsSize);

                final Integer m = batchBGParams.getM();
                final Integer n = batchBGParams.getN();
                final Integer mu = batchBGParams.getMu();
                final Integer numiterations = batchBGParams.getNumIterations();

                final CommitmentParams commitmentParams = createCommitmentParams(batchName, locationsProvider);

                final Ciphertext[][] encryptedBallotsCiphertext =
                    MatrixArranger.arrangeInCiphertextMatrix(encryptedBallots, m, n);

                final ElGamalEncryptedBallots reencryptedBallots = elgamalEncryptedBallotsLoader.loadCSV(zpGroup,
                    locationsProvider.getVerifierLocationsProvider().getReEncryptedBallotsInputOfBatch(batchName));

                final Ciphertext[][] reencryptedBallotsCiphertext =
                    MatrixArranger.arrangeInCiphertextMatrix(reencryptedBallots, m, n);

                final ShuffleProofVerifier shuffleProofVerifier = new ShuffleProofVerifier(zpGroup, gjosteenElGamal,
                    commitmentParams, encryptedBallotsCiphertext, reencryptedBallotsCiphertext, m, n, mu, numiterations,
                    ciphertextTools, multiExponentiation);

                final ShuffleProof shuffleProof = proofsReader
                    .read(locationsProvider.getVerifierLocationsProvider().getProofsInputOfBatch(batchName));

                verified = shuffleProofVerifier.verifyProof(shuffleProof.getInitialMessage(),
                    shuffleProof.getFirstAnswer(), shuffleProof.getSecondAnswer());

                result.put(batchName, verified);

                if (verified) {
                    loggingWriter.log(Level.INFO,
                        new LogContent.LogContentBuilder()
                            .logEvent(MixingAndVerifyingLogEvents.SHUFREENCR_POSITIVE_RESULT_VERIFYING_A_BATCH)
                            .user("adminID").electionEvent("-").additionalInfo("mbb_id", "-")
                            .additionalInfo("batch_name", Integer.toString(batchName)).createLogInfo());
                } else {
                    loggingWriter.log(Level.WARN,
                        new LogContent.LogContentBuilder()
                            .logEvent(MixingAndVerifyingLogEvents.SHUFREENCR_NEGATIVE_RESULT_VERIFYING_A_BATCH)
                            .user("adminID").electionEvent("-").additionalInfo("mbb_id", "-")
                            .additionalInfo("batch_name", Integer.toString(batchName)).createLogInfo());
                }

            } catch (final Exception e) {
                String errorMsg = "An error occurred while verifying batch " + batchName;
                LOG.error(errorMsg + ". Exception message: " + e.getMessage());

                loggingWriter.log(Level.ERROR,
                    new LogContent.LogContentBuilder()
                        .logEvent(MixingAndVerifyingLogEvents.SHUFREENCR_ERROR_VERIFYING_CRYPTOGRAPHIC_PROOF_OF_MIXING)
                        .user("adminID").electionEvent("-").additionalInfo("mbb_id", "-")
                        .additionalInfo("error_msg", e.getMessage()).createLogInfo());

                throw new VerifierException(errorMsg, e);
            }
        }

        try {
            storeOutputData(result, locationsProvider);
        } catch (IOException e) {
            LOG.error("An error occurred while writing the final results. Exception message: " + e.getMessage(), e);
        }

        boolean finalResult = true;

        for (final Boolean b : result.values()) {
            finalResult &= b;
        }

        if (finalResult) {
            LOG.info("All batches have been verified correctly.");

            loggingWriter.log(Level.INFO,
                new LogContent.LogContentBuilder()
                    .logEvent(MixingAndVerifyingLogEvents.SHUFREENCR_POSITIVE_RESULT_VERIFYING_ALL_BATCH_OUTPUTS)
                    .user("adminID").electionEvent("-").additionalInfo("mbb_id", "-").createLogInfo());
        } else {

            loggingWriter.log(Level.WARN,
                new LogContent.LogContentBuilder()
                    .logEvent(MixingAndVerifyingLogEvents.SHUFREENCR_NEGATIVE_RESULT_VERIFYING_ALL_BATCH_OUTPUTS)
                    .user("adminID").electionEvent("-").additionalInfo("mbb_id", "-").createLogInfo());
        }

        return finalResult;

    }

    private static void storeOutputData(final Map<Integer, Boolean> result, final LocationsProvider locationsProvider)
            throws IOException {

        final ObjectMapper mapper = new ObjectMapper();

        final OutputStream verifierOutputStream =
            locationsProvider.getVerifierLocationsProvider().getVerifierOutputStream();
        mapper.writeValue(verifierOutputStream, result);
        verifierOutputStream.close();
    }

    private void initializeBatch(final int batchName, final LocationsProvider locationsProvider)
            throws IOException, GeneralCryptoLibException {
        zpGroup = createZpGroup(locationsProvider, batchName);
        publicKey = createElGamalPublicKey(batchName, locationsProvider);
        gjosteenElGamal = createGjosteenElGamal();
        elgamalEncryptedBallotsLoader = createElGamalEncryptedBallotsLoader();
        multiExponentiation = MultiExponentiationImpl.getInstance();
        ciphertextTools = createCiphertextTools();
    }

    private CommitmentParams createCommitmentParams(final int batchName, final LocationsProvider locationsProvider)
            throws IOException {

        return CommitmentParamsReader.readCommitmentParamsFromStream(zpGroup,
            locationsProvider.getVerifierLocationsProvider().getCommitmentParametersInputOfBatch(batchName));
    }

    private static ZpSubgroup createZpGroup(final LocationsProvider locationsProvider, final int batchName)
            throws IOException {

        return ZpGroupReader
            .build(locationsProvider.getVerifierLocationsProvider().getEncryptionParametersInputOfBatch(batchName));
    }

    private static ElGamalPublicKey createElGamalPublicKey(final int batchName, final LocationsProvider locationsProvider)
            throws IOException, GeneralCryptoLibException {

        return ElgamalPublicKeyReader.readPublicKeyFromStream(
            locationsProvider.getVerifierLocationsProvider().getPublicKeyInputOfBatch(batchName));

    }

    private GjosteenElGamal createGjosteenElGamal() {
        return new GjosteenElGamal(zpGroup, publicKey);
    }

    private static ElGamalEncryptedBallotsLoader createElGamalEncryptedBallotsLoader() {

        final List<EncryptedBallotsValidator> listValidators = new ArrayList<>();
        listValidators.add(new EncryptedBallotsDuplicationValidator());
        EncryptedBallotsValidatorManager validator = new EncryptedBallotsValidatorManager(listValidators);

        return new ElGamalEncryptedBallotsLoader(validator);
    }

    private CiphertextTools createCiphertextTools() {
        return new CiphertextTools(multiExponentiation);
    }
}
