/*
 * 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.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;

import javax.json.JsonException;

import org.apache.commons.lang3.StringUtils;
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.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.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.domain.LogContent.LogContentBuilder;
import com.scytl.products.oscore.logging.api.writer.SecureLoggingWriter;
import com.scytl.products.ov.channel.cc.commons.keymanagement.FingerprintGeneratorException;
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.slogger.ServiceTransactionInfoProvider;
import com.scytl.products.ov.channel.cc.commons.util.FingerprintGenerator;
import com.scytl.products.ov.commons.beans.domain.model.Information.VoterInformation;
import com.scytl.products.ov.commons.beans.domain.model.authentication.AuthenticationToken;
import com.scytl.products.ov.commons.beans.domain.model.vote.Vote;
import com.scytl.products.ov.commons.beans.validation.ValidationErrorType;
import com.scytl.products.ov.commons.crypto.Utils;
import com.scytl.products.ov.commons.dto.ChoiceCodesVerificationInput;
import com.scytl.products.ov.commons.ui.ws.rs.ObjectMappers;
import com.scytl.products.ov.commons.validation.ValidationError;

@Service
public class ProofValidatorImpl implements ProofValidator {

    @Autowired
    private ProofsServiceAPI proofsService;

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

    private GroupElementsCompressor<ZpGroupElement> compressor = new GroupElementsCompressor<>();

    @Autowired
    private SecureLoggingWriter secureLoggingWriter;

    @Autowired
    private SecureLoggingWriter auditLoggingWriter;

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

    @Autowired
    private ServiceTransactionInfoProvider serviceTransactionInfoProvider;

    @Autowired
    private FingerprintGenerator fingerprintGenerator;

    @Override
    public boolean validate(ZpSubgroup encryptionParameters,
            ChoiceCodesVerificationInput choiceCodesVerificationInput) {
        Vote voteObject;
        try {
            voteObject = ObjectMappers.fromJson(choiceCodesVerificationInput.getVote(), Vote.class);
            String hashVote = getHashOfVote(voteObject);
            String hashAuthToken = getHashOfAuthToken(voteObject.getAuthenticationToken());

            ValidationError exponentiationProofValidation =
                validateExponentiationProof(new ValidationError(), encryptionParameters, voteObject);

            boolean exponentiationProofValidationResult = validateSuccess(exponentiationProofValidation, voteObject,
                hashVote, hashAuthToken, ControlComponentsCommonsLogEvents.EXPONENTIATION_PROOF_NOT_VALID);

            ValidationError plaintextEqualityProofValidation = validatePlaintextEqualityProof(new ValidationError(),
                encryptionParameters, voteObject, choiceCodesVerificationInput);
            boolean plaintextEqualityProofValidationResult =
                validateSuccess(plaintextEqualityProofValidation, voteObject, hashVote, hashAuthToken,
                    ControlComponentsCommonsLogEvents.PLAINTEXT_EQUALITY_PROOF_NOT_VALID);

            ValidationError schnorrProofValidation =
                validateSchnorrProof(new ValidationError(), encryptionParameters, voteObject);
            boolean schnorrProofValidationResult = validateSuccess(schnorrProofValidation, voteObject, hashVote,
                hashAuthToken, ControlComponentsCommonsLogEvents.SCHNORR_PROOF_NOT_VALID);

            if (exponentiationProofValidationResult && plaintextEqualityProofValidationResult
                && schnorrProofValidationResult) {
                logSuccessfulValidation(voteObject, hashVote, hashAuthToken);
                return true;
            }
            return false;

        } catch (IOException | GeneralCryptoLibException e) {
            LOG.error("Error trying to validate if it is valid.", e);
            return false;
        }
    }

    private void logSuccessfulValidation(Vote voteObject, String hashVote, String hashAuthToken) {
        LogContentBuilder logContentBuilder =
            new LogContent.LogContentBuilder().logEvent(ControlComponentsCommonsLogEvents.SUCCESSFUL_VOTE_VALIDATION);
        LogContent logContent = createLogContent(voteObject, hashVote, hashAuthToken, logContentBuilder);
        secureLoggingWriter.log(Level.INFO, logContent);
        auditLoggingWriter.log(Level.INFO, logContent);
    }

    private boolean validateSuccess(ValidationError proofValidation, Vote voteObject, String hashVote,
            String hashAuthToken, ControlComponentsCommonsLogEvents logEvent) throws GeneralCryptoLibException {
        boolean proofValidationResult = !ValidationErrorType.FAILED.equals(proofValidation.getValidationErrorType());
        if (!proofValidationResult) {
            String additionalHashId = "";
            String additionalHash = "";
            switch (logEvent) {
            case SCHNORR_PROOF_NOT_VALID:
                additionalHashId = "#hash_Schnorr";
                additionalHash = getHashForProof(Proof.fromJson(voteObject.getSchnorrProof()));
                break;
            case EXPONENTIATION_PROOF_NOT_VALID:
                additionalHashId = "#hash_Exp";
                additionalHash = getHashForProof(Proof.fromJson(voteObject.getExponentiationProof()));
                break;
            case PLAINTEXT_EQUALITY_PROOF_NOT_VALID:
                additionalHashId = "#hash_Plain";
                additionalHash = getHashForProof(Proof.fromJson(voteObject.getPlaintextEqualityProof()));
                break;
            default:
                break;
            }

            LogContentBuilder logContentBuilder =
                new LogContent.LogContentBuilder().additionalInfo(additionalHashId, additionalHash)
                    .additionalInfo(ControlComponentsCommonsLogConstants.ERR_DESC,
                        StringUtils.join(proofValidation.getErrorArgs()))
                    .logEvent(logEvent);
            LogContent logContent = createLogContent(voteObject, hashVote, hashAuthToken, logContentBuilder);
            secureLoggingWriter.log(Level.ERROR, logContent);
        }
        return proofValidationResult;
    }

    private LogContent createLogContent(Vote voteObject, String hashVote, String hashAuthToken,
            LogContentBuilder logContentBuilder) {
        LogContent logContent = logContentBuilder.user(voteObject.getVotingCardId())
            .electionEvent(voteObject.getElectionEventId()).objectId(hashAuthToken)
            .additionalInfo(ControlComponentsCommonsLogConstants.REQUEST_ID,
                serviceTransactionInfoProvider.getServiceTransactionInfo().getTrackId())
            .additionalInfo("#hash_Vote", hashVote)
            .additionalInfo("#encryptedOptions", voteObject.getEncryptedOptions())
            .additionalInfo("#encryptedWriteIns", voteObject.getEncryptedWriteIns())
            .additionalInfo("#correctnessIds", voteObject.getCorrectnessIds())
            .additionalInfo("#verCardPKSignature", voteObject.getVerificationCardPKSignature())
            .additionalInfo("#auhTokenSignature", voteObject.getAuthenticationTokenSignature())
            .additionalInfo("#schnorrProof", voteObject.getSchnorrProof())
            .additionalInfo("#voteSignature", voteObject.getSignature())
            .additionalInfo(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, controlComponentId)
            .createLogInfo();
        return logContent;
    }

    @SuppressWarnings("deprecation")
    private ValidationError validatePlaintextEqualityProof(ValidationError validationError,
            ZpSubgroup encryptionParameters, Vote vote, ChoiceCodesVerificationInput choiceCodesVerificationInput) {
        try {

            ElGamalPublicKey elGamalElectoralPublicKey =
                ElGamalPublicKey.fromJson(choiceCodesVerificationInput.getElGamalElectoralPublicKey());

            // Retrieval of the publicEncryptionChoiceCodesPublicKey

            ElGamalPublicKey elGamalChoiceCodesPublicKey =
                ElGamalPublicKey.fromJson(choiceCodesVerificationInput.getCompressesedChoiceCodesEncryptionPK());

            ZpSubgroup mathematicalGroup = encryptionParameters;

            // 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 =
                compressor.compress(elGamalChoiceCodesPublicKey.getKeys());
            ElGamalPublicKey compressesedChoiceCodesEncryptionPK =
                new ElGamalPublicKey(Arrays.asList(compressedChoiceCodesPKGroupElement), mathematicalGroup);

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

    private ValidationError validateSchnorrProof(ValidationError result, ZpSubgroup encryptionParameters, Vote vote) {
        try {
            BigInteger encryptedParamC0 = new BigInteger(Utils.getC0FromEncryptedOptions(vote.getEncryptedOptions()));
            ZpGroupElement groupElement = new ZpGroupElement(encryptedParamC0, encryptionParameters);

            if (proofsService.createProofVerifierAPI(encryptionParameters).verifySchnorrProof(groupElement,
                vote.getVotingCardId(), vote.getElectionEventId(), Proof.fromJson(vote.getSchnorrProof()))) {
                result.setValidationErrorType(ValidationErrorType.SUCCESS);
            }
        } catch (GeneralCryptoLibException | NumberFormatException e) {
            LOG.error("", e);
        }

        return result;
    }

    private ValidationError validateExponentiationProof(ValidationError result, ZpSubgroup encryptionParameters,
            Vote vote) {
        try {

            // small validation-> check if ciphertext elements are from group
            // defined by g, p and q
            List<ZpGroupElement> groupElements = new ArrayList<>();
            String[] ciphertextElements = Utils.getCiphertextElementsFromEncryptedOptions(vote.getEncryptedOptions());
            for (String elementValue : ciphertextElements) {
                ZpGroupElement groupElement = new ZpGroupElement(new BigInteger(elementValue), encryptionParameters);
                groupElements.add(groupElement);
            }

            // create the input parameter for the cryptolib validator of
            // exponentiation proof
            // create a list of exponentiated elements as group elements of
            // verification card public key and ciphertext
            // elements
            List<ZpGroupElement> exponentiatedElements = new ArrayList<>();
            String verificationCardPublicKeyString =
                new String(Base64.getDecoder().decode(vote.getVerificationCardPublicKey()), StandardCharsets.UTF_8);
            // it is assumed that there is only one exponent in the public key
            ZpGroupElement verificationCardPublicKey =
                ElGamalPublicKey.fromJson(verificationCardPublicKeyString).getKeys().get(0);
            exponentiatedElements.add(verificationCardPublicKey);
            ciphertextElements = Utils.getCiphertextElementsFromEncryptedOptions(vote.getCipherTextExponentiations());
            for (String elementValue : ciphertextElements) {
                ZpGroupElement exponentiatedElement =
                    new ZpGroupElement(new BigInteger(elementValue), encryptionParameters);
                exponentiatedElements.add(exponentiatedElement);
            }

            // create a list of base elements for generator and ciphertext
            // elements
            List<ZpGroupElement> baseElements = new ArrayList<>();
            ZpGroupElement groupElementGenerator =
                new ZpGroupElement(encryptionParameters.getG(), encryptionParameters);
            baseElements.add(groupElementGenerator);
            for (ZpGroupElement groupElement : groupElements) {
                baseElements.add(groupElement);
            }

            if (proofsService.createProofVerifierAPI(encryptionParameters).verifyExponentiationProof(
                exponentiatedElements, baseElements, Proof.fromJson(vote.getExponentiationProof()))) {
                result.setValidationErrorType(ValidationErrorType.SUCCESS);
            }
            // for now any exception that could occur will be just caught and
            // logged
            // making the result of the validation being a false
            // the number format exception is in case there is a problem with
            // the instantiation of a BigInteger for the encryption parameters
        } catch (GeneralCryptoLibException | NumberFormatException e) {
            LOG.error("", e);
        }
        return result;

    }

    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 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());
    }

    public String getHashOfVote(Vote vote) {
        try {
            return fingerprintGenerator.generate(Arrays.asList(vote.getFieldsAsStringArray()),
                field -> field == null ? new byte[0] : field.getBytes(StandardCharsets.UTF_8));
        } catch (JsonException | IllegalStateException | FingerprintGeneratorException e) {
            throw new IllegalArgumentException(e);
        }
    }

    public String getHashForProof(Proof proof) throws GeneralCryptoLibException {
        ArrayList<Exponent> exponentiatonProofExponents = new ArrayList<>();
        exponentiatonProofExponents.add(proof.getHashValue());
        exponentiatonProofExponents.addAll(proof.getValuesList());
        try {
            return fingerprintGenerator.generate(exponentiatonProofExponents,
                element -> element.getValue().toByteArray());
        } catch (FingerprintGeneratorException e) {
            throw new IllegalArgumentException(e);
        }
    }

    public String getHashOfAuthToken(String authenticationTokenString) {
        try {
            // auth token fields
            AuthenticationToken authenticationToken =
                ObjectMappers.fromJson(authenticationTokenString, AuthenticationToken.class);
            String id = authenticationToken.getId();

            VoterInformation voterInformation = authenticationToken.getVoterInformation();
            String tenantId = voterInformation.getTenantId();
            String electionEventId = voterInformation.getElectionEventId();
            String votingCardId = voterInformation.getVotingCardId();
            String ballotId = voterInformation.getBallotId();
            String credentialId = voterInformation.getCredentialId();
            String verificationCardId = voterInformation.getVerificationCardId();
            String ballotBoxId = voterInformation.getBallotBoxId();
            String verificationCardSetId = voterInformation.getVerificationCardSetId();
            String votingCardSetId = voterInformation.getVotingCardSetId();

            String timestamp = authenticationToken.getTimestamp();
            String signature = authenticationToken.getSignature();

            // hash of concatenation of all auth token fields
            return fingerprintGenerator.generate(
                Arrays.asList(id, tenantId, electionEventId, votingCardId, ballotId, credentialId, verificationCardId,
                    ballotBoxId, verificationCardSetId, votingCardSetId, timestamp, signature),
                field -> field.getBytes(StandardCharsets.UTF_8));
        } catch (FingerprintGeneratorException | IOException e) {
            throw new IllegalArgumentException(e);
        }
    }
}
