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

import java.io.IOException;
import java.io.ObjectInputStream;
import java.math.BigInteger;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import javax.ejb.Stateless;
import javax.inject.Inject;

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

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;
import com.scytl.cryptolib.primitives.primes.utils.PemUtils;
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.messaging.PayloadVerificationException;
import com.scytl.products.ov.commons.beans.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.commons.beans.validation.CryptographicOperationException;
import com.scytl.products.ov.commons.domain.model.platform.PlatformCARepository;
import com.scytl.products.ov.commons.dto.ChoiceCodesVerificationResPayload;
import com.scytl.products.ov.commons.dto.PartialCodesInput;
import com.scytl.products.ov.commons.infrastructure.remote.client.RetrofitConsumer;
import com.scytl.products.ov.commons.tracking.TrackIdInstance;
import com.scytl.products.ov.commons.validation.ValidationResult;
import com.scytl.products.ov.commons.verify.PayloadVerifier;
import com.scytl.products.ov.vv.domain.model.choicecode.CodesComputeResults;
import com.scytl.products.ov.vv.domain.model.platform.VvPlatformCARepository;
import com.scytl.products.ov.vv.domain.service.ValidationService;
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;
import com.scytl.products.ov.vv.infrastructure.remote.OrchestratorClient;

import okhttp3.ResponseBody;

/**
 * Compute partial choice codes
 */
@Stateless
public class CodesComputeService {

    @Inject
    private ValidationService validationService;

    @Inject
    private OrchestratorClient ccOrchestratorClient;

    @Inject
    private TrackIdInstance trackId;

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

    @Inject
    private SecureLoggingWriter secureLoggerWriter;

    @Inject
    private PayloadVerifier payloadVerifier;

    @Inject
    @VvPlatformCARepository
    private PlatformCARepository platformCARepository;

    static final String CC_ORCHESTRATOR_PATH = "choicecodes";

    /**
     * Contributes to the computation of the encrypted partial choice codes.
     * 
     * @param tenantId
     *            - tenant identifier.
     * @param eeId
     *            - election event identifier.
     * @param verificationCardSetId
     *            - verification card set id
     * @param verificationCardId
     *            - verification card id
     * @param zpSubgroup
     *            - the mathematical group of the input codes
     * @param partialCodesInput
     *            - encrypted partial codes of the vote
     * @return a map with the computed partial choice codes contributed by the
     *         choice code nodes
     * @throws ResourceNotFoundException
     * @throws CryptographicOperationException
     * @throws IOException
     * @throws ClassNotFoundException
     */
    public CodesComputeResults computePartialCodes(String tenantId, String eeId, String verificationCardSetId,
            String verificationCardId, ZpSubgroup zpSubgroup, PartialCodesInput partialCodesInput)
            throws ResourceNotFoundException, CryptographicOperationException {

        try {
            LOG.info("Requesting the choice code nodes compute contributions");

            List<ChoiceCodesVerificationResPayload> ccnContributions = collectChoiceCodeNodesComputeContributions(
                tenantId, eeId, verificationCardSetId, verificationCardId, partialCodesInput);

            return computePartialCodesFromComputeResult(zpSubgroup, partialCodesInput.getPartialCodesElements(),
                ccnContributions);
        } catch (GeneralCryptoLibException e) {
            String hashPcc;
            try {
                hashPcc =
                    SecureLoggerUtil.getHashForLogFromBigIntegerCollection(partialCodesInput.getPartialCodesElements());
            } catch (GeneralCryptoLibException hashPccError) {
                LOG.error("Could not calculate the hash for the input codes", hashPccError);
                hashPcc = "ERROR";
            }
            // [SL] list of encrypted partial choice codes hashes
            // [SL PCCCON-5] Medium
            // Partial Choice Codes consolidation -- Partial Choice Codes cannot
            // be consolidated
            secureLoggerWriter.log(Level.ERROR,
                new LogContent.LogContentBuilder()
                    .logEvent(VoteVerificationLogEvents.PARTIAL_CODES_CANNOT_BE_CONSOLIDATED)
                    .additionalInfo(VoteVerificationLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                    .additionalInfo(VoteVerificationLogConstants.INFO_ERR_DESC,
                        "Partial Choice Codes cannot be consolidated: " + ExceptionUtils.getRootCauseMessage(e))
                    .additionalInfo("#hash_pCC", hashPcc).createLogInfo());

            LOG.error("Error computing partial choice codes: " + e.getMessage());
            throw new CryptographicOperationException("Error computing partial choice codes:", e);
        } catch (IOException | ClassNotFoundException e) {
            LOG.error("Error computing partial choice codes: " + e.getMessage());
            throw new IllegalStateException("Error computing partial choice codes:", e);
        } catch (PayloadVerificationException e) {
            LOG.error("Error verifying partial choice codes signatures: " + e.getMessage());
            throw new IllegalStateException("Error verifying partial choice codes signatures:", e);
        }
    }

    /**
     * @param zpSubgroup
     * @param partialCodesInput
     * @param ccnContributions
     * @return
     */
    public CodesComputeResults computePartialCodesFromComputeResult(ZpSubgroup zpSubgroup,
            List<BigInteger> partialCodesElements, List<ChoiceCodesVerificationResPayload> ccnContributions) {
        LOG.info("Combining the choice code nodes compute contributions");

        List<Map<BigInteger, BigInteger>> contributedPartialChoiceCodes = ccnContributions.stream()
            .map(ChoiceCodesVerificationResPayload::getPrimeToComputedPrime).collect(Collectors.toList());

        Map<BigInteger, ZpGroupElement> combinedPartialChoiceCodes =
            combineChoiceCodeNodesComputeContributions(partialCodesElements, contributedPartialChoiceCodes, zpSubgroup);

        return new CodesComputeResults(combinedPartialChoiceCodes, ccnContributions);
    }

    private List<ChoiceCodesVerificationResPayload> collectChoiceCodeNodesComputeContributions(String tenantId,
            String eeId, String verificationCardSetId, String verificationCardId, PartialCodesInput partialCodesInput)
            throws GeneralCryptoLibException, ResourceNotFoundException, ClassNotFoundException, IOException,
            PayloadVerificationException {

        try (ResponseBody response = RetrofitConsumer
            .processResponse(ccOrchestratorClient.getChoiceCodeNodesComputeContributions(trackId.getTrackId(),
                CC_ORCHESTRATOR_PATH, tenantId, eeId, verificationCardSetId, verificationCardId, partialCodesInput))) {

            String hashPCC =
                SecureLoggerUtil.getHashForLogFromBigIntegerCollection(partialCodesInput.getPartialCodesElements());
            // [SL PCCREQ-1]
            // Partial Choice Codes computation request -- Partial Choice Code
            // computation correctly received
            secureLoggerWriter.log(Level.INFO, new LogContent.LogContentBuilder().
            /*
             * Not available here and probably not the right object. If any, I
             * would put HASH_PCC as the object here
             */
            // objectId(hashAT).
                user(verificationCardId).electionEvent(eeId)
                .logEvent(VoteVerificationLogEvents.CONTROL_COMPONENT_COMPUTE_CONTRIBUTION_SUCCESFULLY_RECIEVED)
                .additionalInfo(VoteVerificationLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                .additionalInfo(VoteVerificationLogConstants.HASH_PCC, hashPCC).createLogInfo());

            @SuppressWarnings("unchecked")
            List<ChoiceCodesVerificationResPayload> choiceCodesVerificationResults =
                (List<ChoiceCodesVerificationResPayload>) new ObjectInputStream(response.byteStream()).readObject();

            verifySignatures(tenantId, eeId, verificationCardId, choiceCodesVerificationResults);

            verifyProofs(tenantId, eeId, verificationCardId, choiceCodesVerificationResults);

            return choiceCodesVerificationResults;
        }
    }

    private void verifySignatures(String tenantId, String eeId, String verificationCardId,
            List<ChoiceCodesVerificationResPayload> choiceCodesVerificationResults)
            throws GeneralCryptoLibException, ResourceNotFoundException, PayloadVerificationException {

        X509Certificate rootCertificate = (X509Certificate) PemUtils
            .certificateFromPem(platformCARepository.getRootCACertificate().getCertificateContent());

        for (ChoiceCodesVerificationResPayload choiceCodesComputationResult : choiceCodesVerificationResults) {
            if (!payloadVerifier.isValid(choiceCodesComputationResult, rootCertificate)) {
                throw new IllegalStateException("Signature invalid for tenantId " + tenantId + " eeId " + eeId
                    + " verificationCardId " + verificationCardId);
            }
        }
    }

    private void verifyProofs(String tenantId, String eeId, String verificationCardId,
            List<ChoiceCodesVerificationResPayload> choiceCodesComputationResults)
            throws GeneralCryptoLibException, ResourceNotFoundException, JsonParseException, JsonMappingException,
            IOException {

        for (ChoiceCodesVerificationResPayload choiceCodesComputationResult : choiceCodesComputationResults) {
            ValidationResult validationResult =
                validationService.validate(tenantId, eeId, verificationCardId, choiceCodesComputationResult);
            if (!validationResult.isResult()) {
                throw new IllegalStateException("Proof invalid for tenantId " + tenantId + " eeId " + eeId
                    + " verificationCardId " + verificationCardId);

            }
        }
    }

    private Map<BigInteger, ZpGroupElement> combineChoiceCodeNodesComputeContributions(
            List<BigInteger> partialCodesElements, List<Map<BigInteger, BigInteger>> ccnContributions,
            ZpSubgroup zpSubgroup) {

        if (ccnContributions.isEmpty()) {
            LOG.error("Empty Choice Code Nodes compute contributions");
            throw new IllegalStateException("Choice Code Nodes did not return their compute contributions");
        }

        Map<BigInteger, ZpGroupElement> result = new HashMap<BigInteger, ZpGroupElement>();

        for (BigInteger partialCodeElement : partialCodesElements) {
            ZpGroupElement combinedContribution = zpSubgroup.getIdentity();
            for (Map<BigInteger, BigInteger> partialCodeElementToNodeContributionMap : ccnContributions) {
                try {
                    ZpGroupElement nodeContribution =
                        new ZpGroupElement(partialCodeElementToNodeContributionMap.get(partialCodeElement), zpSubgroup);
                    combinedContribution = combinedContribution.multiply(nodeContribution);
                } catch (GeneralCryptoLibException e) {
                    LOG.error("Error combining the Choice Code Nodes compute contributions");
                    throw new IllegalStateException("Choice Code Nodes contributions could not be combined", e);
                }
            }
            result.put(partialCodeElement, combinedContribution);
        }

        return result;
    }

}
