/*
 * 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.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

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

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

import com.fasterxml.jackson.core.JsonProcessingException;
import com.scytl.cryptolib.api.asymmetric.AsymmetricServiceAPI;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
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.commons.beans.domain.model.confirmation.TraceableConfirmationMessage;
import com.scytl.products.ov.commons.beans.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.commons.beans.validation.CryptographicOperationException;
import com.scytl.products.ov.commons.dto.BallotCastingKeyVerificationInput;
import com.scytl.products.ov.commons.dto.CastCodeAndComputeResults;
import com.scytl.products.ov.commons.dto.PartialCodesInput;
import com.scytl.products.ov.commons.tracking.TrackIdInstance;
import com.scytl.products.ov.commons.ui.ws.rs.ObjectMappers;
import com.scytl.products.ov.commons.util.JsonUtils;
import com.scytl.products.ov.vv.domain.model.choicecode.CodesComputeResults;
import com.scytl.products.ov.vv.domain.model.content.CastCodeSignerCertRepository;
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;
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;

/**
 * Convert partial choice codes into long codes
 */
@Stateless
public class CastCodesServiceImpl implements CastCodeService {

    static final String PUBLIC_KEY = "choicesCodesEncryptionPublicKey";

    private static final List<String[]> SINGLETON_LIST_WITH_EMPTY_CORRECTNESS_IDS =
        Collections.singletonList(new String[] {});

    // The verification repository that will give us the verification card set
    // id for the voter verification card id
    @Inject
    private VerificationRepository verificationRepository;

    // The service that will be used to calculate the long codes
    @Inject
    private AsymmetricServiceAPI asymmetricService;

    // The service used to calculate long choice codes
    @Inject
    private LongCodesService longCodesService;

    // The service used to retrieve short choice codes
    @Inject
    private ShortCodesService shortCodesService;

    // The verification set repository that will give us the verification card
    // set for the voter verification card
    @Inject
    private VerificationSetRepository verificationSetRepository;

    // The certificate repository that will give us the vote cast code signing
    // certificates
    @Inject
    private CastCodeSignerCertRepository castCodeSignerCertRepository;

    @Inject
    private CodesComputeService codesComputeService;

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

    @Inject
    private TrackIdInstance trackIdInstance;

    @Inject
    private SecureLoggingWriter secureLoggerWriter;

    /**
     * @see com.scytl.products.ov.vv.service.CastCodeService#retrieveCastCodeAndSignature(String,
     *      String, String, TraceableConfirmationMessage)
     */
    @Override
    public CastCodeAndComputeResults retrieveCastCodeAndSignature(final String tenantId, final String eeid,
            final String verificationCardId, TraceableConfirmationMessage confirmationMessage)
            throws ResourceNotFoundException, CryptographicOperationException {

        LOG.info("Generating the vote cast code for tenant: {}, election event: {} and verification card: {}.",
            tenantId, eeid, verificationCardId);

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

        String verificationCardSetId = verification.getVerificationCardSetId();

        X509Certificate castCodeSigningCert =
            castCodeSignerCertRepository.findByTenantEEIDVerificationCardSetId(tenantId, eeid, verificationCardSetId);

        PublicKey castCodeSigningPublicKey = castCodeSigningCert.getPublicKey();

        String confirmationCodeString =
            new String(Base64.getDecoder().decode(confirmationMessage.getConfirmationKey()), StandardCharsets.UTF_8);
        BigInteger confirmationCodeValue = new BigInteger(confirmationCodeString);

        List<BigInteger> confirmationCodeToCompute = Collections.singletonList(confirmationCodeValue);

        VerificationSetEntity verificationSet = verificationSetRepository
            .findByTenantIdElectionEventIdVerificationCardSetId(tenantId, eeid, verificationCardSetId);
        JsonObject object = JsonUtils.getJsonObject(verificationSet.getJSON());
        String elgamalPublicKeyBase64 = object.getString(PUBLIC_KEY);
        byte[] elgamalPublicKeyBytes =
            Base64.getDecoder().decode(elgamalPublicKeyBase64.getBytes(StandardCharsets.UTF_8));
        String publicKeyString = new String(elgamalPublicKeyBytes, StandardCharsets.UTF_8);

        List<ZpGroupElement> confirmationCodesList = null;
        try {
            ElGamalPublicKey publicKey = ElGamalPublicKey.fromJson(publicKeyString);
            ZpSubgroup group = publicKey.getGroup();

            CodesComputeResults computedPartialCodes =
                codesComputeService.computePartialCodes(tenantId, eeid, verificationCardSetId, verificationCardId,
                    group, createVerificationPayload(confirmationCodeToCompute, confirmationMessage));

            confirmationCodesList = confirmationCodeToCompute.stream()
                .map(computedPartialCodes.getCombinedPartialChoiceCodes()::get).collect(Collectors.toList());

            String hashPvcc = SecureLoggerUtil.getHashForLogFromBigIntegerCollection(confirmationCodeToCompute);
            String hashPCVcc = SecureLoggerUtil.getHashForLogFromZpGroupElementIntegerCollection(confirmationCodesList);
            // [SL] hash_pCVCC hash of the consolidated partial vote cast
            // code
            // [SL PVCCCON-4]
            // Partial Vote Cast Code consolidation -- Partial Vote Cast
            // Code correctly consolidated

            secureLoggerWriter.log(Level.INFO,
                new LogContent.LogContentBuilder()
                    .logEvent(VoteVerificationLogEvents.PARTIAL_VOTE_CAST_CODE_CORRECTLY_CONSOLIDATED)
                    .user(verificationCardId).electionEvent(eeid)
                    .additionalInfo(VoteVerificationLogConstants.INFO_TRACK_ID, trackIdInstance.getTrackId())
                    .additionalInfo("#hash_pVCCC", hashPvcc).additionalInfo("#hash_pCVCC", hashPCVcc).createLogInfo());

            List<byte[]> longCodes;
            try {
                longCodes = longCodesService.calculateLongCodes(eeid, verificationCardId, confirmationCodesList,
                    SINGLETON_LIST_WITH_EMPTY_CORRECTNESS_IDS);
            } catch (CryptographicOperationException e) {
                secureLoggerWriter.log(Level.ERROR, new LogContent.LogContentBuilder()
                    .logEvent(VoteVerificationLogEvents.ERROR_COMPUTING_LONG_VOTE_CAST_CODE)
                    .objectId(verificationCardId).user(verificationCardId).electionEvent(eeid)
                    .additionalInfo(VoteVerificationLogConstants.INFO_TRACK_ID, trackIdInstance.getTrackId())
                    .additionalInfo(VoteVerificationLogConstants.INFO_VERIFICATION_CARD_ID, verificationCardId)
                    .additionalInfo(VoteVerificationLogConstants.INFO_VERIFICATION_CARD_SET_ID, verificationCardSetId)
                    .additionalInfo(VoteVerificationLogConstants.INFO_ERR_DESC,
                        "Long cast code can not be retrieved: " + ExceptionUtils.getRootCauseMessage(e))
                    .createLogInfo());
                throw e;
            }

            List<String> shortCodes;
            try {
                shortCodes = shortCodesService.retrieveShortCodes(tenantId, eeid, verificationCardId, longCodes);

            } catch (ResourceNotFoundException | CryptographicOperationException e) {
                secureLoggerWriter.log(Level.ERROR,
                    new LogContent.LogContentBuilder()
                        .logEvent(VoteVerificationLogEvents.ERROR_RETRIEVING_VOTE_CAST_CODE)
                        .objectId(verificationCardId).user(verificationCardId).electionEvent(eeid)
                        .additionalInfo(VoteVerificationLogConstants.INFO_TRACK_ID, trackIdInstance.getTrackId())
                        .additionalInfo(VoteVerificationLogConstants.INFO_VERIFICATION_CARD_ID, verificationCardId)
                        .additionalInfo(VoteVerificationLogConstants.INFO_VERIFICATION_CARD_SET_ID,
                            verificationCardSetId)
                        .additionalInfo(VoteVerificationLogConstants.INFO_ERR_DESC,
                            "Cast code can not be retrieved: " + ExceptionUtils.getRootCauseMessage(e))
                        .createLogInfo());
                throw e;
            }
            String[] castCodeAndSignature = shortCodes.get(0).split(";");

            CastCodeAndComputeResults castCodeMessage = new CastCodeAndComputeResults();
            castCodeMessage.setVoteCastCode(castCodeAndSignature[0]);
            castCodeMessage.setSignature(castCodeAndSignature[1]);

            byte[] castCodeBytes = castCodeMessage.getVoteCastCode().getBytes(StandardCharsets.UTF_8);
            byte[] castCodeSignatureBytes = Base64.getDecoder().decode(castCodeMessage.getSignature());
            byte[] verificationCardIdAsBytes = verificationCardId.getBytes(StandardCharsets.UTF_8);

            if (!asymmetricService.verifySignature(castCodeSignatureBytes, castCodeSigningPublicKey, castCodeBytes,
                verificationCardIdAsBytes)) {
                secureLoggerWriter.log(Level.ERROR,
                    new LogContent.LogContentBuilder()
                        .logEvent(VoteVerificationLogEvents.ERROR_INVALID_SIGNATURE_VOTE_CAST_CODE)
                        .objectId(verificationCardId).user(verificationCardId).electionEvent(eeid)
                        .additionalInfo(VoteVerificationLogConstants.INFO_TRACK_ID, trackIdInstance.getTrackId())
                        .additionalInfo(VoteVerificationLogConstants.INFO_VERIFICATION_CARD_ID, verificationCardId)
                        .additionalInfo(VoteVerificationLogConstants.INFO_VERIFICATION_CARD_SET_ID,
                            verificationCardSetId)
                        .additionalInfo(VoteVerificationLogConstants.INFO_ERR_DESC, "Cast code signature not verify")
                        .createLogInfo());
                throw new CryptographicOperationException("Cast code signature does not verify");
            } else {
                secureLoggerWriter.log(Level.INFO, new LogContent.LogContentBuilder()
                    .logEvent(VoteVerificationLogEvents.VOTE_CAST_CODE_SIGNATURE_VALIDATED).objectId(verificationCardId)
                    .user(verificationCardId).electionEvent(eeid)
                    .additionalInfo(VoteVerificationLogConstants.INFO_TRACK_ID, trackIdInstance.getTrackId())
                    .additionalInfo(VoteVerificationLogConstants.INFO_VERIFICATION_CARD_ID, verificationCardId)
                    .additionalInfo(VoteVerificationLogConstants.INFO_VERIFICATION_CARD_SET_ID, verificationCardSetId)
                    .createLogInfo());
            }

            secureLoggerWriter.log(Level.INFO,
                new LogContent.LogContentBuilder().logEvent(VoteVerificationLogEvents.VOTE_CAST_CODE_RETRIEVED)
                    .objectId(verificationCardId).user(verificationCardId).electionEvent(eeid)
                    .additionalInfo(VoteVerificationLogConstants.INFO_TRACK_ID, trackIdInstance.getTrackId())
                    .createLogInfo());

            castCodeMessage.setComputationResults(ObjectMappers.toJson(computedPartialCodes.getComputationResults()));

            return castCodeMessage;

        } catch (GeneralCryptoLibException | JsonProcessingException e) {
            secureLoggerWriter.log(Level.ERROR,
                new LogContent.LogContentBuilder().logEvent(VoteVerificationLogEvents.ERROR_RETRIEVING_VOTE_CAST_CODE)
                    .objectId(verificationCardId).user(verificationCardId).electionEvent(eeid)
                    .additionalInfo(VoteVerificationLogConstants.INFO_TRACK_ID, trackIdInstance.getTrackId())
                    .additionalInfo(VoteVerificationLogConstants.INFO_VERIFICATION_CARD_ID, verificationCardId)
                    .additionalInfo(VoteVerificationLogConstants.INFO_VERIFICATION_CARD_SET_ID, verificationCardSetId)
                    .additionalInfo(VoteVerificationLogConstants.INFO_ERR_DESC,
                        "Cast code can not be retrieved: " + ExceptionUtils.getRootCauseMessage(e))
                    .createLogInfo());
            throw new CryptographicOperationException("Error retrieving the cast code message:", e);
        }

    }

    private PartialCodesInput createVerificationPayload(List<BigInteger> confirmationCodeToCompute,
            TraceableConfirmationMessage confirmationMessage) throws JsonProcessingException {
        BallotCastingKeyVerificationInput ballotCastingKeyVerificationInput = new BallotCastingKeyVerificationInput();
        ballotCastingKeyVerificationInput.setConfirmationMessage(ObjectMappers.toJson(confirmationMessage));
        ballotCastingKeyVerificationInput.setVotingCardId(confirmationMessage.getVotingCardId());
        PartialCodesInput partialCodesInput = new PartialCodesInput();
        partialCodesInput.setPartialCodesElements(confirmationCodeToCompute);
        partialCodesInput.setBallotCastingKeyVerificationInput(ballotCastingKeyVerificationInput);
        return partialCodesInput;
    }

}
