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

import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import javax.ejb.Stateless;
import javax.enterprise.inject.Any;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
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.ElGamalPublicKey;
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.writer.SecureLoggingWriter;
import com.scytl.products.ov.commons.beans.domain.model.vote.Vote;
import com.scytl.products.ov.commons.beans.exceptions.ApplicationException;
import com.scytl.products.ov.commons.beans.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.commons.beans.validation.ValidationErrorType;
import com.scytl.products.ov.commons.domain.model.rule.AbstractRule;
import com.scytl.products.ov.commons.domain.service.RuleExecutor;
import com.scytl.products.ov.commons.dto.ChoiceCodesVerificationResPayload;
import com.scytl.products.ov.commons.tracking.TrackIdInstance;
import com.scytl.products.ov.commons.ui.ws.rs.ObjectMappers;
import com.scytl.products.ov.commons.validation.ValidationError;
import com.scytl.products.ov.commons.validation.ValidationResult;
import com.scytl.products.ov.vv.domain.model.verification.Verification;
import com.scytl.products.ov.vv.domain.model.verification.VerificationDerivedKeys;
import com.scytl.products.ov.vv.domain.model.verification.VerificationDerivedKeysRepository;
import com.scytl.products.ov.vv.domain.model.verification.VerificationRepository;
import com.scytl.products.ov.vv.domain.model.verificationset.VerificationSetData;
import com.scytl.products.ov.vv.domain.model.verificationset.VerificationSetEntity;
import com.scytl.products.ov.vv.domain.model.verificationset.VerificationSetRepository;
import com.scytl.products.ov.vv.infrastructure.log.SecureLoggerUtil;
import com.scytl.products.ov.vv.infrastructure.log.VoteVerificationLogConstants;
import com.scytl.products.ov.vv.infrastructure.log.VoteVerificationLogEvents;

/**
 * This service handles the validations.
 */
@Stateless(name = "vv-validationService")
public class ValidationServiceImpl implements ValidationService {
    private final Collection<AbstractRule<Vote>> rules = new ArrayList<>();

    // 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_CHOICE_CODES_PUBLIC_KEY = "choicesCodesEncryptionPublicKey";

    @Inject
    private RuleExecutor<Vote> ruleExecutor;

    @Inject
    private VerificationSetRepository verificationSetRepository;

    @Inject
    private VerificationDerivedKeysRepository verificationDerivedKeysRepository;

    @Inject
    private VerificationRepository verificationRepository;

    @Inject
    private ProofsServiceAPI proofsService;

    @Inject
    private SecureLoggingWriter secureLoggerWriter;

    @Inject
    private TrackIdInstance trackId;
    
    @Inject
    private PrimitivesServiceAPI primitivesService;

    @Inject
    @Any
    void setRules(Instance<AbstractRule<Vote>> instance) {
        for (AbstractRule<Vote> rule : instance) {
            rules.add(rule);
        }
    }

    /**
     * @see com.scytl.products.ov.vv.domain.service.ValidationService#validate(com.scytl.products.ov.commons.beans.domain.model.vote.Vote)
     */
    @Override
    public ValidationResult validate(final Vote vote) throws ApplicationException {
        // execute the rules that exist
        ValidationError executorRulesResult = ruleExecutor.execute(rules, vote);
        ValidationResult validationResult = new ValidationResult();
        validationResult.setResult(executorRulesResult.getValidationErrorType().equals(ValidationErrorType.SUCCESS));
        validationResult.setValidationError(executorRulesResult);
        return validationResult;
    }

    @Override
    public ValidationResult validate(String tenantId, String electionEventId, String verificationCardId,
            ChoiceCodesVerificationResPayload choiceCodesComputationResult)
            throws GeneralCryptoLibException, ResourceNotFoundException, JsonParseException, JsonMappingException,
            IOException {

        Verification verification = verificationRepository.findByTenantIdElectionEventIdVerificationCardId(tenantId,
            electionEventId, verificationCardId);

        // Retrieve the encryption parameters from the verification context
        VerificationSetEntity verificationContent =
            verificationSetRepository.findByTenantIdElectionEventIdVerificationCardSetId(tenantId, electionEventId,
                verification.getVerificationCardSetId());

        VerificationSetData verificationSetData =
            ObjectMappers.fromJson(verificationContent.getJSON(), VerificationSetData.class);

        ElGamalPublicKey elGamalChoiceCodesPublicKey = ElGamalPublicKey
            .fromJson(new String(Base64.getDecoder().decode(verificationSetData.getChoicesCodesEncryptionPublicKey()),
                StandardCharsets.UTF_8));

        ZpSubgroup encryptionParameters = elGamalChoiceCodesPublicKey.getGroup();
        
        String choiceCodesDerivedKeyJson = choiceCodesComputationResult.getChoiceCodesDerivedKeyJson();
        String castCodeDerivedKeyJson = choiceCodesComputationResult.getCastCodeDerivedKeyJson();
        String derivedKey = getExistingDerivedKey(tenantId, electionEventId, verificationCardId, choiceCodesDerivedKeyJson,
            castCodeDerivedKeyJson);
        if (derivedKey == null) {
            ValidationResult validationResult = new ValidationResult();
            validationResult.setResult(false);
            validationResult.setValidationError(new ValidationError(ValidationErrorType.INVALID_VERIFICATION_CARD_KEY));
            return validationResult;
        }

        // Prepare lists for proof verification input
        List<ZpGroupElement> baseElements = new ArrayList<>();
        List<ZpGroupElement> exponentiatedElems = new ArrayList<>();

        // First elements of the lists are the G and the derived key
        baseElements.add(encryptionParameters.getGenerator());
        ZpGroupElement derivedPublicKey = ZpGroupElement.fromJson(new String(
            Base64.getDecoder().decode(derivedKey), StandardCharsets.UTF_8));
        exponentiatedElems.add(derivedPublicKey);

        Map<BigInteger, BigInteger> primeToComputedPrime = choiceCodesComputationResult.getPrimeToComputedPrime();

        if (choiceCodesDerivedKeyJson != null) {
            for (Map.Entry<BigInteger, BigInteger> entry : primeToComputedPrime.entrySet()) {
                // The rest of elements of the lists are the primes and the exponentiated primes
                baseElements.add(new ZpGroupElement(entry.getKey(), encryptionParameters));
                exponentiatedElems.add(new ZpGroupElement(entry.getValue(), encryptionParameters));
            }
        } else if (castCodeDerivedKeyJson != null) {
            for (Map.Entry<BigInteger, BigInteger> entry : primeToComputedPrime.entrySet()) {                
                // The rest of elements of the lists are the primes and the exponentiated primes
                // Hash^2 the Cast code to have the same input as the verification computation in the control component, when the proof was generated
                BigInteger hashedPrime = new BigInteger(primitivesService.getHash(entry.getKey().toByteArray()));
                BigInteger squaredHashedPrime = hashedPrime.multiply(hashedPrime);
                baseElements.add(new ZpGroupElement(squaredHashedPrime, encryptionParameters));
                exponentiatedElems.add(new ZpGroupElement(entry.getValue(), encryptionParameters));
            }
        } else {
            throw new IllegalArgumentException("No derived key was provided for cast code or choice codes validation");
        }

        // And finally obtain the proof for verification
        Proof expProof = Proof.fromJson(choiceCodesComputationResult.getExponentiationProofJson());

        // [SL] hash of the encrypted partial choice code received
        String hashPcc = SecureLoggerUtil.getHashForLogFromZpGroupElementIntegerCollection(baseElements);

        // [SL] hash of the encrypted partial choice code computed
        String hashPccComp = SecureLoggerUtil.getHashForLogFromZpGroupElementIntegerCollection(exponentiatedElems);

        ArrayList<Exponent> exponentiatonProofExponents = new ArrayList<>();
        exponentiatonProofExponents.add(expProof.getHashValue());
        exponentiatonProofExponents.addAll(expProof.getValuesList());
        String pccProof = SecureLoggerUtil.getHashForLogFromExponentCollection(exponentiatonProofExponents);

        boolean verified = proofsService.createProofVerifierAPI(encryptionParameters)
            .verifyExponentiationProof(exponentiatedElems, baseElements, expProof);
        if (!verified) {
            // [SL PCCCON-3]
            // Partial Choice Codes consolidation -- Proof not valid
            secureLoggerWriter.log(Level.ERROR,
                new LogContent.LogContentBuilder().logEvent(VoteVerificationLogEvents.PROOF_NOT_VALID_COMP)
                    .additionalInfo(VoteVerificationLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                    .additionalInfo(VoteVerificationLogConstants.INFO_ERR_DESC, "Proof not valid")
                    .additionalInfo("#pCC_proof", pccProof).additionalInfo("#hash_pCC", hashPcc)
                    .additionalInfo("#hash_pCC_comp", hashPccComp).createLogInfo());

        }
        return new ValidationResult(verified);
    }


    private String getExistingDerivedKey(String tenantId, String electionEventId, String verificationCardId,
            String choiceCodesDerivedKeyJson, String castCodeDerivedKeyJson) throws JsonParseException, JsonMappingException, IOException {

        // Retrieve from database the derived key commitment for this
        // verification card id
        // (obtained during computation)
        VerificationDerivedKeys verificationDerivedKey;
        try {
            verificationDerivedKey = verificationDerivedKeysRepository
                .findByTenantIdElectionEventIdVerificationCardId(tenantId, electionEventId, verificationCardId);

        } catch (ResourceNotFoundException rnfe) {
            throw new IllegalStateException("No existing derived keys found for tenantId " + tenantId + " eeId "
                + electionEventId + " verificationCardId " + verificationCardId, rnfe);
        }

        if (choiceCodesDerivedKeyJson != null) {
            List<String> ccodeDerivedKeyCommitmentList =
                ObjectMappers.fromJson(verificationDerivedKey.getCcodeDerivedKeyCommitment(), List.class);
            if (checkDerivedKeyExists(ccodeDerivedKeyCommitmentList, choiceCodesDerivedKeyJson)) {
                return choiceCodesDerivedKeyJson;
            } else {
                return null;
            }

        } else if (castCodeDerivedKeyJson != null) {
            List<String> bckDerivedExpCommitmentList =
                ObjectMappers.fromJson(verificationDerivedKey.getBckDerivedExpCommitment(), List.class);
            if (checkDerivedKeyExists(bckDerivedExpCommitmentList, castCodeDerivedKeyJson)) {
                return castCodeDerivedKeyJson;
            } else {
                return null;
            }

        } else {
            throw new IllegalArgumentException("No derived key was provided for cast code or choice codes validation");
        }
    }

    private boolean checkDerivedKeyExists(List<String> derivedKeysJson, String otherDerivedKeyJson) {
        return derivedKeysJson.contains(otherDerivedKeyJson);
    }
}
