/*
 * 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.vv.domain.model.rule;

import java.io.IOException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.inject.Inject;
import javax.json.JsonObject;

import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.api.proofs.ProofsServiceAPI;
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.cryptolib.mathematical.groups.impl.ZpSubgroup;
import com.scytl.cryptolib.proofs.proof.Proof;
import com.scytl.products.ov.commons.beans.EncryptionParameters;
import com.scytl.products.ov.commons.beans.VoteVerificationContextData;
import com.scytl.products.ov.commons.beans.domain.model.vote.Vote;
import com.scytl.products.ov.commons.beans.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.commons.beans.validation.ValidationErrorType;
import com.scytl.products.ov.commons.crypto.Utils;
import com.scytl.products.ov.commons.domain.model.rule.AbstractRule;
import com.scytl.products.ov.commons.ui.ws.rs.ObjectMappers;
import com.scytl.products.ov.commons.util.JsonUtils;
import com.scytl.products.ov.commons.validation.ValidationError;
import com.scytl.products.ov.vv.domain.model.content.ElectoralAuthorityEntity;
import com.scytl.products.ov.vv.domain.model.content.ElectoralAuthorityRepository;
import com.scytl.products.ov.vv.domain.model.content.VerificationContentEntity;
import com.scytl.products.ov.vv.domain.model.content.VerificationContentRepository;
import com.scytl.products.ov.vv.domain.model.verification.Verification;
import com.scytl.products.ov.vv.domain.model.verification.VerificationRepository;
import com.scytl.products.ov.vv.domain.model.verificationset.VerificationSetEntity;
import com.scytl.products.ov.vv.domain.model.verificationset.VerificationSetRepository;

/**
 * This is an implementation of a validator of the Plaintext equality Proof.
 */
public class PlainTextEqualityProofRule implements AbstractRule<Vote> {

    // The name of the json parameter p (group).
    static final String JSON_PARAMETER_P = "p";

    // The name of the json parameter q (order).
    static final String JSON_PARAMETER_Q = "q";

    // The name of the json parameter generator.
    static final String JSON_PARAMETER_GENERATOR = "g";

    // The name of the json parameter encryptionParameters.
    static final String JSON_PARAMETER_ENCRYPTION_PARAMETERS = "encryptionParameters";

    static final String JSON_PUBLIC_KEY = "publicKey";

    static final String JSON_PARAMETER_ELECTORAL_AUTHORITY_ID = "electoralAuthorityId";

    static final String JSON_CHOICE_CODES_PUBLIC_KEY = "choicesCodesEncryptionPublicKey";

    @Inject
    VerificationSetRepository verificationSetRepository;

    @Inject
    VerificationRepository verificationRepository;

    @Inject
    VerificationContentRepository verificationContentRepository;

    @Inject
    ElectoralAuthorityRepository electoralAuthorityRepository;

    @Inject
    private ProofsServiceAPI proofsService;

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

    /**
     * Executes a plain text equality proof validation.
     * 
     * @param vote
     *            - vote to be validated
     * @return boolean value with the result of the validation
     */
    @Override
    public ValidationError execute(Vote vote) {
        ValidationError result = new ValidationError();

        VerificationContentEntity verificationContent = null;
        VerificationSetEntity verificationSetEntity = null;
        ElectoralAuthorityEntity electoralAuthority = null;
        VoteVerificationContextData voteVerificationContextData = null;
        JsonObject verificationSetJSON = null;
        try {
            Verification verification;
            verification = verificationRepository.findByTenantIdElectionEventIdVerificationCardId(vote.getTenantId(),
                vote.getElectionEventId(), vote.getVerificationCardId());

            verificationContent = verificationContentRepository.findByTenantIdElectionEventIdVerificationCardSetId(
                vote.getTenantId(), vote.getElectionEventId(), verification.getVerificationCardSetId());

            verificationSetEntity = verificationSetRepository.findByTenantIdElectionEventIdVerificationCardSetId(
                vote.getTenantId(), vote.getElectionEventId(), verification.getVerificationCardSetId());

            voteVerificationContextData =
                ObjectMappers.fromJson(verificationContent.getJson(), VoteVerificationContextData.class);

            verificationSetJSON = JsonUtils.getJsonObject(verificationSetEntity.getJSON());

            String electoralAuthorityId = voteVerificationContextData.getElectoralAuthorityId();

            electoralAuthority = electoralAuthorityRepository.findByTenantIdElectionEventIdElectoralAuthorityId(
                vote.getTenantId(), vote.getElectionEventId(), electoralAuthorityId);

        } catch (ResourceNotFoundException | IOException e) {
            LOG.error("", e);
            return result;
        }
        try {
            // Retrieval of the encryption parameters and electoral public key
            EncryptionParameters encryptionParameters = voteVerificationContextData.getEncryptionParameters();
            String generatorEncryptParam = encryptionParameters.getG();
            String pEncryptParam = encryptionParameters.getP();
            String qEncryptParam = encryptionParameters.getQ();

            LOG.debug("Parameters to be used for PlaintText Equality proof are:");
            LOG.debug("g = " + generatorEncryptParam);
            LOG.debug("p = " + pEncryptParam);
            LOG.debug("q = " + qEncryptParam);

            String electoralPublicKeyString =
                JsonUtils.getJsonObject(electoralAuthority.getJson()).getString(JSON_PUBLIC_KEY);

            ElGamalPublicKey elGamalElectoralPublicKey =
                ElGamalPublicKey.fromJson(new String(Base64.decodeBase64(electoralPublicKeyString)));

            // Retrieval of the publicEncryptionChoiceCodesPublicKey

            String choiceCodesEncryptionPKString =
                new String(Base64.decodeBase64(verificationSetJSON.getString(JSON_CHOICE_CODES_PUBLIC_KEY)));
            ElGamalPublicKey elGamalChoiceCodesPublicKey = ElGamalPublicKey.fromJson(choiceCodesEncryptionPKString);

            ZpSubgroup mathematicalGroup = new ZpSubgroup(new BigInteger(generatorEncryptParam),
                new BigInteger(pEncryptParam), new BigInteger(qEncryptParam));

            // Retrieval of the C'0 and C'1 values
            BigInteger C0CipherText =
                new BigInteger(Utils.getC0FromEncryptedOptions(vote.getCipherTextExponentiations()));
            BigInteger C1CipherText =
                new BigInteger(Utils.getC1FromEncryptedOptions(vote.getCipherTextExponentiations()));

            LOG.debug("C'0 value = " + C0CipherText);
            LOG.debug("C'1 value = " + C1CipherText);

            List<ZpGroupElement> primaryCiphertext = new ArrayList<>();
            primaryCiphertext.add(new ZpGroupElement(C0CipherText, mathematicalGroup));
            primaryCiphertext.add(new ZpGroupElement(C1CipherText, mathematicalGroup));

            // Retrieval of D0 and D'1 as result of compressing the D1-Dn
            // elements of the partial choice codes
            String encryptedChoiceCodes = vote.getEncryptedPartialChoiceCodes();
            ZpGroupElement D1prima = getCompressedListFromChoiceCodes(mathematicalGroup, encryptedChoiceCodes);
            BigInteger valueD0 = new BigInteger(encryptedChoiceCodes.split(";")[0]);
            ZpGroupElement D0 = new ZpGroupElement(valueD0, mathematicalGroup);
            List<ZpGroupElement> secondaryCiphertext = new ArrayList<>();
            secondaryCiphertext.add(D0);
            secondaryCiphertext.add(D1prima);

            LOG.debug("D0 value = " + D0);
            LOG.debug("D'1 value = " + D1prima);

            // The choice Codes encryption PK is compressed applying the same
            // technique.Its keys are used as input
            ZpGroupElement compressedChoiceCodesPKGroupElement =
                getCompressedList(elGamalChoiceCodesPublicKey.getKeys());
            ElGamalPublicKey compressesedChoiceCodesEncryptionPK =
                new ElGamalPublicKey(Arrays.asList(compressedChoiceCodesPKGroupElement), mathematicalGroup);

            if (proofsService.createProofVerifierAPI(mathematicalGroup).verifyPlaintextEqualityProof(primaryCiphertext,
                getKeyComposedOfFirstSubkeyOnly(elGamalElectoralPublicKey), secondaryCiphertext,
                compressesedChoiceCodesEncryptionPK, Proof.fromJson(vote.getPlaintextEqualityProof()))) {
                result.setValidationErrorType(ValidationErrorType.SUCCESS);
            }
        } catch (GeneralCryptoLibException | NumberFormatException e) {
            LOG.error("", e);
        }
        return result;
    }

    /**
     * Gets the name of the rule
     * 
     * @return
     */
    @Override
    public String getName() {
        return RuleNames.VOTE_PLAINTEXT_EQUALITY_PROOF.getText();
    }

    private ZpGroupElement getCompressedListFromChoiceCodes(ZpSubgroup mathematicalGroup, String encryptedChoiceCodes)
            throws GeneralCryptoLibException {
        String[] partials = encryptedChoiceCodes.split(";");
        List<ZpGroupElement> elements = new ArrayList<>();
        for (int i = 1; i < partials.length; i++) {
            elements.add(new ZpGroupElement(new BigInteger(partials[i]), mathematicalGroup));
        }
        return getCompressedList(elements);
    }

    private ZpGroupElement getCompressedList(List<ZpGroupElement> elements) throws GeneralCryptoLibException {
        GroupElementsCompressor<ZpGroupElement> compressor = new GroupElementsCompressor<>();
        return compressor.compress(elements);

    }

    private ElGamalPublicKey getKeyComposedOfFirstSubkeyOnly(ElGamalPublicKey origianlKey)
            throws GeneralCryptoLibException {

        List<ZpGroupElement> firstSubKey = new ArrayList<>();
        firstSubKey.add(origianlKey.getKeys().get(0));
        return new ElGamalPublicKey(firstSubKey, origianlKey.getGroup());
    }
}
