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

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
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.util.FingerprintGenerator;
import com.scytl.products.ov.commons.beans.domain.model.ballotbox.BallotBoxId;
import com.scytl.products.ov.commons.beans.domain.model.messaging.PayloadSignature;
import com.scytl.products.ov.commons.beans.domain.model.vote.EncryptedVote;
import com.scytl.products.ov.commons.beans.domain.model.vote.VoteSetId;
import com.scytl.products.ov.commons.dto.mixing.MixingPayload;

/**
 * Creates a helper that commits a particular request's messages to the secure log.
 */
public class SecureLoggingHelper {
    private static final Logger LOG = LoggerFactory.getLogger(SecureLoggingHelper.class);

    private final SecureLoggingWriter secureLoggingWriter;

    private final FingerprintGenerator fingerprintGenerator;

    private final VoteSetId voteSetId;

    private Map<String, String> secureLogDataTemplate;

    /**
     * Initialise the secure logging helper with the required data.
     */
    public SecureLoggingHelper(SecureLoggingWriter secureLoggingWriter,
            FingerprintGenerator fingerprintGenerator, String nodeName, String requestId,
            VoteSetId voteSetId) {
        this.secureLoggingWriter = secureLoggingWriter;
        this.fingerprintGenerator = fingerprintGenerator;
        this.voteSetId = voteSetId;

        // Create an immutable map of fixed secure log data items.
        Map<String, String> tempMap = new HashMap<>();
        tempMap.put(ControlComponentsCommonsLogConstants.CONTROL_COMPONENT_ID, nodeName);
        tempMap.put(ControlComponentsCommonsLogConstants.REQUEST_ID, requestId);
        tempMap.put(ControlComponentsCommonsLogConstants.VOTE_SET_ID, voteSetId.toString());
        secureLogDataTemplate = Collections.unmodifiableMap(tempMap);
    }

    private void putPreviousVoteEncryptionKeyHashInResult(Map<String, String> result,
            ElGamalPublicKey previousVoteEncryptionKey) {
        try {
            result.put(ControlComponentsCommonsLogConstants.PREVIOUS_VOTE_ENCRYPTION_KEY,
                    getEncryptionKeyHash(previousVoteEncryptionKey));
        } catch (FingerprintGeneratorException e) {
            LOG.warn("Could not compute the fingerprint for the previous vote encryption key", e);

        }
    }

    void logBallotBoxSignatureOutcome(final MixingPayload payload, final boolean success) {
        ControlComponentsCommonsLogEvents logEvent = success
                ? ControlComponentsCommonsLogEvents.VOTE_SET_SIGNATURE_VALIDATED
                : ControlComponentsCommonsLogEvents.ERROR_IN_VOTE_SET_SIGNATURE;
        Map<String, String> localSecureLogInfo =
                completeSecureLoggerInfoBoxSignature(payload, success, logEvent);
        LogContentBuilder logContent = buildLogContent(logEvent, localSecureLogInfo);
        secureLoggingWriter.log(success ? Level.INFO : Level.ERROR, logContent.createLogInfo());
    }

    private LogContentBuilder buildLogContent(ControlComponentsCommonsLogEvents logEvent,
            Map<String, String> secureLogInfo) {
        LogContentBuilder logContent = new LogContent.LogContentBuilder().logEvent(logEvent);
        BallotBoxId ballotBoxId = voteSetId.getBallotBoxId();
        logContent.objectId(ballotBoxId.getId());
        logContent.electionEvent(ballotBoxId.getElectionEventId());
        for (String key : secureLogInfo.keySet()) {
            logContent.additionalInfo(key, secureLogInfo.get(key));
        }
        return logContent;
    }

    void logMixingAndDecryptingOutcome(final MixingPayload inputPayload,
            final MixingPayload outputPayload) {
        boolean success = outputPayload != null;
        ControlComponentsCommonsLogEvents logEvent = success
                ? ControlComponentsCommonsLogEvents.VOTE_SET_MIXING_DECRYPTED
                : ControlComponentsCommonsLogEvents.ERROR_IN_VOTE_SET_MIXING_DECRYPTED;
        Map<String, String> localSecureLogInfo =
                completeSecureLoggerInfoMixingAndDecrypting(inputPayload, outputPayload, logEvent);
        LogContentBuilder logContent = buildLogContent(logEvent, localSecureLogInfo);
        secureLoggingWriter.log(success ? Level.INFO : Level.ERROR, logContent.createLogInfo());
    }

    void logVoteEncryptionKeyRemovalOutcome(String electoralAuthorityId,
            ElGamalPublicKey originalVoteEncryptionKey,
            final ElGamalPublicKey remainingVoteEncryptionKey) {
        boolean success = null != remainingVoteEncryptionKey;
        ControlComponentsCommonsLogEvents logEvent = success
                ? ControlComponentsCommonsLogEvents.VOTE_ENCRYPTION_KEY_REMOVAL
                : ControlComponentsCommonsLogEvents.ERROR_IN_VOTE_ENCRYPTION_KEY_REMOVAL;
        Map<String, String> localSecureLogInfo =
                completeSecureLoggerInfoVoteEncryptionKeyRemoval(electoralAuthorityId,
                        originalVoteEncryptionKey, remainingVoteEncryptionKey, success, logEvent);
        LogContentBuilder logContent = buildLogContent(logEvent, localSecureLogInfo);
        secureLoggingWriter.log(success ? Level.INFO : Level.ERROR, logContent.createLogInfo());
    }

    void logVoteSetSignedOutcome(final MixingPayload payload) {
        boolean success = payload.getSignature() != null;
        ControlComponentsCommonsLogEvents logEvent = success
                ? ControlComponentsCommonsLogEvents.VOTE_SET_SUCCESSFULLY_SIGNED
                : ControlComponentsCommonsLogEvents.ERROR_IN_VOTE_SET_SIGNING;
        Map<String, String> localSecureLogInfo =
                completeSecureLoggerInfoVoteSetSignatureOutcome(payload, logEvent, success);
        LogContentBuilder logContent = buildLogContent(logEvent, localSecureLogInfo);
        secureLoggingWriter.log(success ? Level.INFO : Level.ERROR, logContent.createLogInfo());
    }

    private Map<String, String> completeSecureLoggerInfoBoxSignature(final MixingPayload payload,
            final boolean success, final ControlComponentsCommonsLogEvents logEvent) {
        Map<String, String> result = cloneTemplate();
        List<EncryptedVote> votes = payload.getVotes();
        putDecryptedVotesHashInResult(result, votes);
        ElGamalPublicKey voteEncryptionKey = payload.getVoteEncryptionKey();
        putVoteEncryptionKeyHashInResult(result, voteEncryptionKey);
        result.put(ControlComponentsCommonsLogConstants.TIMESTAMP,
                payload.getTimestamp().toString());
        PayloadSignature signature = payload.getSignature();
        putSignatureInResult(result, signature);

        // These objects are null for the initial mixing payload and
        // should not be logged in that case.
        List<String> commitmentParameters = payload.getCommitmentParameters();
        if (null != commitmentParameters) {
            putCommitmentParametersHashInResult(result, commitmentParameters);
        }

        List<String> decryptionProofs = payload.getDecryptionProofs();
        if (null != decryptionProofs) {
            putDecryptionProofsHashInResult(result, decryptionProofs);
        }

        List<EncryptedVote> shuffledVotes = payload.getShuffledVotes();
        if (null != shuffledVotes) {
            putShuffledVotesHashInResult(result, shuffledVotes);
        }

        String shuffleProof = payload.getShuffleProof();
        if (null != shuffledVotes) {
            putShuffledProofHashInResult(result, shuffleProof);
        }

        List<EncryptedVote> previousVotes = payload.getPreviousVotes();
        if (null != previousVotes) {
            putPreviousVotesHashInResult(result, previousVotes);
        }

        ElGamalPublicKey previousVoteEncryptionKey = payload.getPreviousVoteEncryptionKey();
        if (null != previousVoteEncryptionKey) {
            putPreviousVoteEncryptionKeyHashInResult(result, previousVoteEncryptionKey);
        }

        if (!success) {
            result.put(ControlComponentsCommonsLogConstants.ERR_DESC, logEvent.getInfo());
        }

        return result;
    }

    /**
     * Provide a starting point for secure log data.
     *
     * @return a hash map that can be added to, pre-populated with the required data.
     */
    private Map<String, String> cloneTemplate() {
        return new HashMap<>(secureLogDataTemplate);
    }

    private Map<String, String> completeSecureLoggerInfoMixingAndDecrypting(
            final MixingPayload inputPayload, final MixingPayload outputPayload,
            final ControlComponentsCommonsLogEvents logEvent) {
        Map<String, String> result = cloneTemplate();

        List<EncryptedVote> previousVotes = inputPayload.getVotes();
        putPreviousVotesHashInResult(result, previousVotes);
        ElGamalPublicKey voteEncryptionKey = inputPayload.getVoteEncryptionKey();
        putVoteEncryptionKeyHashInResult(result, voteEncryptionKey);
        if (outputPayload != null) {
            putCommitmentParametersHashInResult(result, outputPayload.getCommitmentParameters());
            putDecryptedVotesHashInResult(result, outputPayload.getVotes());
            putDecryptionProofsHashInResult(result, outputPayload.getDecryptionProofs());
            putShuffledVotesHashInResult(result, outputPayload.getShuffledVotes());
            putShuffledProofHashInResult(result, outputPayload.getShuffleProof());
        } else {
            result.put(ControlComponentsCommonsLogConstants.ERR_DESC, logEvent.getInfo());
        }
        return result;
    }

    private void putShuffledProofHashInResult(Map<String, String> result, String shuffleProof) {
        try {
            result.put(ControlComponentsCommonsLogConstants.SHUFFLED_PROOF,
                    shuffleProof == null || shuffleProof.isEmpty()
                            ? ""
                            : fingerprintGenerator.generate(shuffleProof));
        } catch (FingerprintGeneratorException e) {
            LOG.warn("Could not compute the fingerprint for the shuffle proof", e);

        }
    }

    private void putShuffledVotesHashInResult(Map<String, String> result,
            List<EncryptedVote> shuffledVoteSet) {
        try {
            result.put(ControlComponentsCommonsLogConstants.SHUFFLED_VOTES,
                    getVotesHash(shuffledVoteSet));
        } catch (FingerprintGeneratorException e) {
            LOG.warn("Could not compute the fingerprint for the shuffled votes", e);
        }
    }

    private void putDecryptionProofsHashInResult(Map<String, String> result,
            List<String> decryptionProofs) {
        try {
            result.put(ControlComponentsCommonsLogConstants.DECRYPTION_PROOFS,
                    getHashFromList(decryptionProofs));
        } catch (FingerprintGeneratorException e) {
            LOG.warn("Could not compute the fingerprint for the decryption proofs", e);
        }
    }

    private void putDecryptedVotesHashInResult(Map<String, String> result,
            List<EncryptedVote> decryptedVoteSet) {
        try {
            result.put(ControlComponentsCommonsLogConstants.VOTES, getVotesHash(decryptedVoteSet));
        } catch (FingerprintGeneratorException e) {
            LOG.warn("Could not compute the fingerprint for the decrypted votes", e);
        }
    }

    private void putCommitmentParametersHashInResult(Map<String, String> result,
            List<String> commitmentParameters) {
        try {
            result.put(ControlComponentsCommonsLogConstants.COMMITMENT_PARAMETERS,
                    getHashFromList(commitmentParameters));
        } catch (FingerprintGeneratorException e) {
            LOG.warn("Could not compute the fingerprint for the commitment parameters", e);
        }
    }

    private void putVoteEncryptionKeyHashInResult(Map<String, String> result,
            ElGamalPublicKey voteEncryptionKey) {
        try {
            result.put(ControlComponentsCommonsLogConstants.VOTE_ENCRYPTION_KEY,
                    getEncryptionKeyHash(voteEncryptionKey));
        } catch (FingerprintGeneratorException e) {
            LOG.warn("Could not compute the fingerprint for the encryption key", e);
        }
    }

    private void putPreviousVotesHashInResult(Map<String, String> result,
            List<EncryptedVote> previousVotes) {
        try {
            result.put(ControlComponentsCommonsLogConstants.PREVIOUS_VOTES,
                    getVotesHash(previousVotes));
        } catch (FingerprintGeneratorException e) {
            LOG.warn("Could not compute the fingerprint for the previous votes", e);
        }
    }

    private Map<String, String> completeSecureLoggerInfoVoteEncryptionKeyRemoval(
            String electoralAuthorityId, ElGamalPublicKey previousVoteEncryptionKey,
            ElGamalPublicKey remainingVoteEncryptionKey, final boolean success,
            final ControlComponentsCommonsLogEvents logEvent) {
        Map<String, String> result = cloneTemplate();
        result.put(ControlComponentsCommonsLogConstants.ELECTORAL_BOARD_ID, electoralAuthorityId);
        putPreviousVoteEncryptionKeyHashInResult(result, previousVoteEncryptionKey);
        if (!success) {
            result.put(ControlComponentsCommonsLogConstants.ERR_DESC, logEvent.getInfo());
        } else {
            putVoteEncryptionKeyHashInResult(result, remainingVoteEncryptionKey);
        }
        return result;
    }

    private Map<String, String> completeSecureLoggerInfoVoteSetSignatureOutcome(
            final MixingPayload payload, final ControlComponentsCommonsLogEvents logEvent,
            final boolean success) {
        Map<String, String> secureLogData = cloneTemplate();
        putDecryptedVotesHashInResult(secureLogData, payload.getVotes());
        secureLogData.put(ControlComponentsCommonsLogConstants.TIMESTAMP,
                payload.getTimestamp().toString());
        putCommitmentParametersHashInResult(secureLogData, payload.getCommitmentParameters());
        putDecryptionProofsHashInResult(secureLogData, payload.getDecryptionProofs());
        putShuffledVotesHashInResult(secureLogData, payload.getShuffledVotes());
        putShuffledProofHashInResult(secureLogData, payload.getShuffleProof());
        putPreviousVotesHashInResult(secureLogData, payload.getPreviousVotes());
        putVoteEncryptionKeyHashInResult(secureLogData, payload.getVoteEncryptionKey());
        PayloadSignature signature = payload.getSignature();
        if (signature != null) {
            putSignatureInResult(secureLogData, signature);
        }
        if (!success) {
            secureLogData.put(ControlComponentsCommonsLogConstants.ERR_DESC, logEvent.getInfo());
        }
        return secureLogData;
    }

    private void putSignatureInResult(Map<String, String> result, PayloadSignature signature) {
        result.put(ControlComponentsCommonsLogConstants.VOTE_SET_SIGNATURE,
                Base64.getEncoder().encodeToString(signature.getSignatureContents()));
    }

    private String getHashFromList(final List<String> list) throws FingerprintGeneratorException {
        return fingerprintGenerator
                .generate(list, element -> element.getBytes(StandardCharsets.UTF_8));
    }

    private String getEncryptionKeyHash(final ElGamalPublicKey voteEncryptionKey)
            throws FingerprintGeneratorException {
        if (voteEncryptionKey != null) {
            return fingerprintGenerator.generate(voteEncryptionKey);
        }
        return "";
    }

    private String getVotesHash(final List<EncryptedVote> votes)
            throws FingerprintGeneratorException {
        if (votes != null) {
            return fingerprintGenerator
                    .generate(votes, vote -> vote.toString().getBytes(StandardCharsets.UTF_8));
        }
        return "";
    }
}
