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

import static java.nio.file.Files.delete;
import static java.nio.file.Files.newDirectoryStream;
import static java.nio.file.Files.newOutputStream;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.DirectoryStream;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.PrivateKey;
import java.util.function.Supplier;

import javax.json.JsonObject;

import org.apache.commons.io.IOUtils;
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.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.VerificationCardSetData;
import com.scytl.products.ov.commons.beans.VoteVerificationContextData;
import com.scytl.products.ov.commons.beans.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.commons.configuration.JSONSignatureConfiguration;
import com.scytl.products.ov.commons.sign.JSONSigner;
import com.scytl.products.ov.commons.util.JsonUtils;
import com.scytl.products.ov.constants.Constants;
import com.scytl.products.ov.sdm.application.log.SdmSecureLogEvent;
import com.scytl.products.ov.sdm.domain.common.SignedObject;
import com.scytl.products.ov.sdm.domain.model.ballotbox.BallotBoxRepository;
import com.scytl.products.ov.sdm.domain.model.generator.DataGeneratorResponse;
import com.scytl.products.ov.sdm.domain.model.status.InvalidStatusTransitionException;
import com.scytl.products.ov.sdm.domain.model.status.Status;
import com.scytl.products.ov.sdm.domain.model.status.SynchronizeStatus;
import com.scytl.products.ov.sdm.domain.service.BallotBoxDataGeneratorService;
import com.scytl.products.ov.sdm.domain.service.BallotDataGeneratorService;
import com.scytl.products.ov.sdm.domain.service.ConfigConstants;
import com.scytl.products.ov.sdm.domain.service.VotingCardSetDataGeneratorService;
import com.scytl.products.ov.sdm.infrastructure.JsonConstants;
import com.scytl.products.ov.sdm.infrastructure.cc.ChoiceCodeGenerationRequestPayloadRepository;
import com.scytl.products.ov.sdm.infrastructure.cc.PayloadStorageException;
import com.scytl.products.ov.sdm.infrastructure.service.ConfigurationEntityStatusService;
import com.scytl.products.ov.utils.ConfigObjectMapper;

/**
 * This is an application service that manages voting card sets.
 */
@Service
public class VotingCardSetService extends BaseVotingCardSetService {

    private static final Logger LOGGER = LoggerFactory.getLogger(VotingCardSetService.class);

    @Autowired
    private BallotBoxRepository ballotBoxRepository;

    @Autowired
    private BallotBoxDataGeneratorService ballotBoxDataGeneratorService;

    @Autowired
    private BallotDataGeneratorService ballotDataGeneratorService;

    @Autowired
    private VotingCardSetDataGeneratorService votingCardSetDataGeneratorService;

    @Autowired
    private ConfigurationEntityStatusService configurationEntityStatusService;

    @Autowired
    private SignatureService signatureService;

    @Autowired
    private ExtendedAuthenticationService extendedAuthenticationService;

    @Autowired
    private SecureLoggingWriter secureLogger;

    @Autowired
    private VotingCardSetChoiceCodesService votingCardSetChoiceCodesService;
    
    @Autowired
    private ChoiceCodeGenerationRequestPayloadRepository choiceCodeGenerationRequestPayloadRepository;

    @Value("${OR_URL}")
    private String orchestratorUrl;

    @Value("${tenantID}")
    private String tenantId;

    @Autowired
    IdleStatusService idleStatusService;

    /**
     * Download the computed values for a votingCardSet
     *
     * @throws InvalidStatusTransitionException
     *             if the original status does not allow the download
     * @throws GeneralCryptoLibException
     *             if there is an exception parsing encryption parameters or
     *             combining the node contributions
     */
    public void download(String votingCardSetId, String electionEventId)
            throws ResourceNotFoundException, InvalidStatusTransitionException, IOException, GeneralCryptoLibException {
        
        if (!idleStatusService.isIdReady(votingCardSetId)) {
            return;
        }

        try {
            secureLog(SdmSecureLogEvent.DOWNLOADING_VCS, electionEventId, votingCardSetId);

            Status fromStatus = Status.COMPUTED;
            Status toStatus = Status.VCS_DOWNLOADED;

            checkVotingCardSetStatusTransition(electionEventId, votingCardSetId, fromStatus, toStatus);

            JsonObject votingCardSetJson = votingCardSetRepository.getVotingCardSetJson(electionEventId, votingCardSetId);
            String verificationCardSetId = getVerificationCardSetId(votingCardSetJson);

            deleteNodeContributions(electionEventId, verificationCardSetId);
        
            int chunkCount;
            try {
                chunkCount = choiceCodeGenerationRequestPayloadRepository.getCount(electionEventId, verificationCardSetId);
            } catch (PayloadStorageException e) {
                throw new IllegalStateException("Failed to get the chunk count.", e);
            }
        
            for (int i = 0; i < chunkCount; i++) {
                try (InputStream contributions =
                        votingCardSetChoiceCodesService.download(electionEventId, verificationCardSetId, i)) {
                    writeNodeContributions(electionEventId, verificationCardSetId, i, contributions);
                }
            }

            configurationEntityStatusService.update(toStatus.name(), votingCardSetId, votingCardSetRepository);

        } finally {
            idleStatusService.freeIdleStatus(votingCardSetId);
        }

        secureLog(SdmSecureLogEvent.VCS_DOWNLOADED_SUCCESSFULLY, electionEventId, votingCardSetId);
    }

    private void deleteNodeContributions(String electionEventId,
            String verificationCardSetId)
            throws IOException {
        Path folder =
            pathResolver.resolve(ConfigConstants.CONFIG_FILES_BASE_DIR)
                .resolve(electionEventId)
                .resolve(ConfigConstants.CONFIG_DIR_NAME_ONLINE)
                .resolve(ConfigConstants.CONFIG_DIR_NAME_VOTERVERIFICATION)
                .resolve(verificationCardSetId);
        Filter<Path> filter = VotingCardSetService::isNodeContributions;
        try (DirectoryStream<Path> files =
            newDirectoryStream(folder, filter)) {
            for (Path file : files) {
                delete(file);
            }
        }
    }

    private static boolean isNodeContributions(Path file) {
        String name = file.getFileName().toString();
        return name.startsWith(
            ConfigConstants.CONFIG_FILE_NAME_NODE_CONTRIBUTIONS)
            && name.endsWith(Constants.JSON);
    }

    private void writeNodeContributions(String electionEventId,
            String verificationCardSetId, int chunkId,
            InputStream contributions)
            throws IOException {
        String fileName =
            ConfigConstants.CONFIG_FILE_NAME_NODE_CONTRIBUTIONS + "."
                + chunkId + Constants.JSON;
        Path file =
            pathResolver.resolve(ConfigConstants.CONFIG_FILES_BASE_DIR)
                .resolve(electionEventId)
                .resolve(ConfigConstants.CONFIG_DIR_NAME_ONLINE)
                .resolve(ConfigConstants.CONFIG_DIR_NAME_VOTERVERIFICATION)
                .resolve(verificationCardSetId).resolve(fileName);

        try (OutputStream stream = newOutputStream(file)) {
            IOUtils.copy(contributions, stream);
        }
    }

    /**
     * Generates the voting card set data based on the given votingCardSetId.
     * The generation contains 3 steps: generate the ballot box data, generate
     * the ballot file and finally the voting card set data.
     *
     * @param electionEventId
     *            The id of the election event.
     * @param votingCardSetId
     *            The id of the voting card set for which the data is generated.
     * @return a DataGeneratorResponse containing information about the result
     *         of the generation.
     * @throws InvalidStatusTransitionException
     *             if the original status does not allow the generation
     */
    public DataGeneratorResponse generate(final String votingCardSetId, final String electionEventId)
            throws IOException, ResourceNotFoundException, InvalidStatusTransitionException {
        
        if (!idleStatusService.isIdReady(votingCardSetId)) {
            return new DataGeneratorResponse();
        }

        try {

            Status fromStatus = Status.VCS_DOWNLOADED;
            Status toStatus = Status.GENERATED;
            DataGeneratorResponse result;

            checkVotingCardSetStatusTransition(electionEventId, votingCardSetId, fromStatus, toStatus);

            validateIds(votingCardSetId, electionEventId);
            // generate the needed data (encryption parameters file, ballot box,
            // ballot)
            result = generateDataNeeded(votingCardSetId, electionEventId);
            if (!result.isSuccessful()) {
                return result;
            }
            // generate voting card set data
            result = votingCardSetDataGeneratorService.generate(votingCardSetId, electionEventId);
            return result;

        } finally {
            idleStatusService.freeIdleStatus(votingCardSetId);
        }
    }

    // Generates the needed data like ballot box and ballot.
    private DataGeneratorResponse generateDataNeeded(final String votingCardSetId, final String electionEventId)
            throws IOException {
        DataGeneratorResponse result = new DataGeneratorResponse();

        // get voting card set from repository
        String ballotBoxId = votingCardSetRepository.getBallotBoxId(votingCardSetId);
        if (StringUtils.isEmpty(ballotBoxId)) {
            result.setSuccessful(false);
            secureLogger.log(Level.ERROR,
                new LogContent.LogContentBuilder().logEvent(SdmSecureLogEvent.VCS_GENERATION_FAILED)
                    .objectId(votingCardSetId).electionEvent(electionEventId)
                    .additionalInfo("#err_desc", "ballotBoxId is empty").createLogInfo());
            return result;
        }

        // get ballot box from repository
        String ballotId = ballotBoxRepository.getBallotId(ballotBoxId);
        if (StringUtils.isEmpty(ballotId)) {
            result.setSuccessful(false);

            secureLogger.log(Level.ERROR,
                new LogContent.LogContentBuilder().logEvent(SdmSecureLogEvent.VCS_GENERATION_FAILED)
                    .objectId(votingCardSetId).electionEvent(electionEventId)
                    .additionalInfo("#err_desc", "ballotId is empty").createLogInfo());
            return result;
        }

        // generate ballot data
        result = ballotDataGeneratorService.generate(ballotId, electionEventId);
        if (!result.isSuccessful()) {

            secureLogger.log(Level.ERROR,
                new LogContent.LogContentBuilder().logEvent(SdmSecureLogEvent.VCS_GENERATION_FAILED)
                    .objectId(votingCardSetId).electionEvent(electionEventId)
                    .additionalInfo("#err_desc", "Ballot data was not generated").createLogInfo());

            return result;
        }

        // generate ballot box data if it is not already done
        // for now the status locked means that it is not generated yet
        String ballotBoxAsJson = ballotBoxRepository.find(ballotBoxId);
        String ballotBoxStatus =
            JsonUtils.getJsonObject(ballotBoxAsJson).getString(JsonConstants.JSON_ATTRIBUTE_NAME_STATUS);
        if (Status.LOCKED.name().equals(ballotBoxStatus)) {
            result = ballotBoxDataGeneratorService.generate(ballotBoxId, electionEventId);
            if (!result.isSuccessful()) {

                secureLogger.log(Level.ERROR,
                    new LogContent.LogContentBuilder().logEvent(SdmSecureLogEvent.VCS_GENERATION_FAILED)
                        .objectId(votingCardSetId).electionEvent(electionEventId)
                        .additionalInfo("#err_desc", "Generating ballot box data failed.").createLogInfo());

                return result;
            }
            configurationEntityStatusService.update(Status.READY.name(), ballotBoxId, ballotBoxRepository);
        }
        return result;
    }

    /**
     * Change the state of the voting card set from generated to SIGNED for a
     * given election event and voting card set id.
     *
     * @param electionEventId
     *            the election event id.
     * @param votingCardSetId
     *            the voting card set id.
     * @param privateKeyPEM
     * @return true if the status is successfully changed to signed. Otherwise,
     *         false.
     * @throws ResourceNotFoundException
     *             if the voting card set is not found.
     */
    public boolean sign(final String electionEventId, final String votingCardSetId, final String privateKeyPEM)
            throws ResourceNotFoundException, GeneralCryptoLibException, IOException {

        boolean result = false;

        JsonObject votingCardSetJson = votingCardSetRepository.getVotingCardSetJson(electionEventId, votingCardSetId);

        if (votingCardSetJson != null
            && votingCardSetJson.containsKey(JsonConstants.JSON_ATTRIBUTE_NAME_STATUS)) {
            String status = votingCardSetJson.getString(JsonConstants.JSON_ATTRIBUTE_NAME_STATUS);
            if (Status.GENERATED.name().equals(status)) {

                PrivateKey privateKey = PemUtils.privateKeyFromPem(privateKeyPEM);

                String verificationCardSetId = getVerificationCardSetId(votingCardSetJson);

                LOGGER.info("Signing voting card set " + votingCardSetId);
                LOGGER.info("Signing voter material configuration");
                signVoterMaterial(electionEventId, votingCardSetId, privateKey);
                LOGGER.info("Signing verification card set " + verificationCardSetId);
                LOGGER.info("Signing vote verification configuration");
                signVoteVerification(electionEventId, verificationCardSetId, votingCardSetId, privateKey);
                LOGGER.info("Signing the extended authentication");
                signExtendedAuthentication(electionEventId, votingCardSetId, verificationCardSetId, privateKey);
                LOGGER.info("Changing the status of the voting card set");
                configurationEntityStatusService.updateWithSynchronizedStatus(Status.SIGNED.name(), votingCardSetId,
                    votingCardSetRepository, SynchronizeStatus.PENDING);
                result = true;
            }
        }

        return result;
    }

    private void signVoteVerification(final String electionEventId, final String verificationCardSetId,
            final String votingCardSetId, final PrivateKey privateKey) throws IOException {

        Path voteVerificationPath = pathResolver.resolve(ConfigConstants.CONFIG_FILES_BASE_DIR).resolve(electionEventId)
            .resolve(ConfigConstants.CONFIG_DIR_NAME_ONLINE).resolve(ConfigConstants.CONFIG_DIR_NAME_VOTERVERIFICATION)
            .resolve(verificationCardSetId);

        boolean correctSigning = signAllVoteVerificationCSVFiles(privateKey, voteVerificationPath);

        if (!correctSigning) {
            LOGGER
                .error("An error occurred while signing the verification card set, rolling back to its original state");
            signatureService.deleteSignaturesFromCSVs(voteVerificationPath);

            Path voterMaterialPath = pathResolver.resolve(ConfigConstants.CONFIG_FILES_BASE_DIR)
                .resolve(electionEventId).resolve(ConfigConstants.CONFIG_DIR_NAME_ONLINE)
                .resolve(ConfigConstants.CONFIG_DIR_NAME_VOTERMATERIAL).resolve(votingCardSetId);

            signatureService.deleteSignaturesFromCSVs(voterMaterialPath);
        } else {
            signJSONs(privateKey, voteVerificationPath);
        }

    }

    private void signExtendedAuthentication(final String electionEventId, final String votingCardSetId,
            final String verificationCardSetId, final PrivateKey privateKey) throws IOException {

        boolean signatureResult =
            extendedAuthenticationService.signExtendedAuthentication(electionEventId, votingCardSetId, privateKey);

        if (!signatureResult) {

            LOGGER.error(
                "An error occurred while signing the extended authentication , rolling back to its original state");
            Path voteVerificationPath = pathResolver.resolve(ConfigConstants.CONFIG_FILES_BASE_DIR)
                .resolve(electionEventId).resolve(ConfigConstants.CONFIG_DIR_NAME_ONLINE)
                .resolve(ConfigConstants.CONFIG_DIR_NAME_VOTERVERIFICATION).resolve(verificationCardSetId);
            signatureService.deleteSignaturesFromCSVs(voteVerificationPath);

            Path voterMaterialPath = pathResolver.resolve(ConfigConstants.CONFIG_FILES_BASE_DIR)
                .resolve(electionEventId).resolve(ConfigConstants.CONFIG_DIR_NAME_ONLINE)
                .resolve(ConfigConstants.CONFIG_DIR_NAME_VOTERMATERIAL).resolve(votingCardSetId);

            signatureService.deleteSignaturesFromCSVs(voterMaterialPath);
        }

    }

    private void signJSONs(final PrivateKey privateKey, final Path voteVerificationPath) throws IOException {

        ConfigObjectMapper mapper = new ConfigObjectMapper();
        JSONSigner signer = new JSONSigner(JSONSignatureConfiguration.RSA_PSS_SHA256);

        Path verificationCardSetPath =
            voteVerificationPath.resolve(ConfigConstants.CONFIG_FILE_NAME_VERIFICATIONSET_DATA).toAbsolutePath();

        Path signedVerificationCardSetPath =
            voteVerificationPath.resolve(ConfigConstants.CONFIG_FILE_NAME_SIGNED_VERIFICATIONSET_DATA).toAbsolutePath();

        VerificationCardSetData verificationCardSetData =
            mapper.fromJSONFileToJava(new File(verificationCardSetPath.toString()), VerificationCardSetData.class);

        LOGGER.info("Signing verification card set data");
        String signedVerificationCardSetData = signer.sign(privateKey, verificationCardSetData);
        SignedObject signedSignedVerificationCardSetDataObject = new SignedObject();
        signedSignedVerificationCardSetDataObject.setSignature(signedVerificationCardSetData);
        mapper.fromJavaToJSONFile(signedSignedVerificationCardSetDataObject,
            new File(signedVerificationCardSetPath.toString()));

        Path verificationContextPath =
            voteVerificationPath.resolve(ConfigConstants.CONFIG_FILE_NAME_VERIFICATION_CONTEXT_DATA);

        Path signedVerificationContextPath =
            voteVerificationPath.resolve(ConfigConstants.CONFIG_FILE_NAME_SIGNED_VERIFICATION_CONTEXT_DATA);

        VoteVerificationContextData voteVerificationContextData =
            mapper.fromJSONFileToJava(new File(verificationContextPath.toString()), VoteVerificationContextData.class);

        LOGGER.info("Signing vote verification context data");
        String signedVoteVerificationContextData = signer.sign(privateKey, voteVerificationContextData);
        SignedObject signedSignedVoteVerificationContextDataObject = new SignedObject();
        signedSignedVoteVerificationContextDataObject.setSignature(signedVoteVerificationContextData);
        mapper.fromJavaToJSONFile(signedSignedVoteVerificationContextDataObject,
            new File(signedVerificationContextPath.toString()));
    }

    private boolean signAllVoteVerificationCSVFiles(final PrivateKey privateKey, final Path voteVerificationPath) {
        return evaluateUploadResult(() -> {
            long numberFailed = 0;
            try {
                numberFailed = Files.walk(voteVerificationPath, 1).filter(csvPath -> {
                    Boolean failed = Boolean.FALSE;
                    try {
                        String name = csvPath.getFileName().toString();
                        if ((name.startsWith(ConfigConstants.CONFIG_FILE_NAME_CODES_MAPPING)
                            && name.endsWith(ConfigConstants.CSV))
                            || (name.startsWith(ConfigConstants.CONFIG_FILE_VERIFICATION_CARD_DATA)
                                && name.endsWith(ConfigConstants.CSV))
                            || (name.startsWith(ConfigConstants.CONFIG_FILE_NAME_DERIVED_KEYS)
                                && name.endsWith(ConfigConstants.CSV))) {

                            LOGGER.info("Signing file " + name);
                            String signatureB64 = signatureService.signCSV(privateKey, csvPath.toFile());
                            LOGGER.info("Saving signature");
                            signatureService.saveCSVSignature(signatureB64, csvPath);
                        }
                    } catch (IOException | GeneralCryptoLibException e) {
                    	LOGGER.warn("Error trying to sign All Vote Verification CSV Files." + e);
                        failed = true;
                    }
                    return failed;
                }).count();
            } catch (IOException e) {
            	LOGGER.warn("Error trying to sign All Vote Verification CSV Files." + e);
                return Boolean.FALSE;
            }
            return numberFailed == 0;
        });
    }

    private void signVoterMaterial(final String electionEventId, final String votingCardSetId,
            final PrivateKey privateKey) throws IOException {

        Path voterMaterialPath = pathResolver.resolve(ConfigConstants.CONFIG_FILES_BASE_DIR).resolve(electionEventId)
            .resolve(ConfigConstants.CONFIG_DIR_NAME_ONLINE).resolve(ConfigConstants.CONFIG_DIR_NAME_VOTERMATERIAL)
            .resolve(votingCardSetId);

        boolean correctSigning = signAllVoterMaterialCSVFiles(privateKey, voterMaterialPath);

        if (!correctSigning) {
            LOGGER.error("An error occurred while signing the voting card set, rolling back to its original state");
            signatureService.deleteSignaturesFromCSVs(voterMaterialPath);
        }

    }

    private boolean signAllVoterMaterialCSVFiles(final PrivateKey privateKey, final Path voterMaterialPath) {
        return evaluateUploadResult(() -> {
            long numberFailed = 0;
            try {
                numberFailed = Files.walk(voterMaterialPath, 1).filter(csvPath -> {
                    Boolean failed = Boolean.FALSE;
                    try {
                        String name = csvPath.getFileName().toString();
                        if ((name.startsWith(ConfigConstants.CONFIG_FILE_NAME_VOTER_INFORMATION)
                            && name.endsWith(ConfigConstants.CSV))
                            || (name.startsWith(ConfigConstants.CONFIG_FILE_NAME_CREDENTIAL_DATA)
                                && name.endsWith(ConfigConstants.CSV))) {

                            LOGGER.info("Signing file " + name);
                            String signatureB64 = signatureService.signCSV(privateKey, csvPath.toFile());
                            LOGGER.info("Saving signature");
                            signatureService.saveCSVSignature(signatureB64, csvPath);
                        }
                    } catch (IOException | GeneralCryptoLibException e) {
                    	LOGGER.warn("Error trying to sign All Vote Material CSV Files." + e);
                        failed = true;
                    }
                    return failed;
                }).count();
            } catch (IOException e) {
            	LOGGER.warn("Error trying to sign All Vote Material CSV Files." + e);
            	return Boolean.FALSE;
            }
            return numberFailed == 0;
        });
    }

    private boolean evaluateUploadResult(final Supplier<Boolean> arg) {
        return arg.get();
    }

    /**
     * Gets the verification card set identifier corresponding to a voting card
     * set.
     * 
     * @param electionEventId
     *            the election event identifier
     * @param votingCardSetId
     *            the voting card set identifier
     * @return the verification card set identifier
     * @throws ResourceNotFoundException
     */
    protected String getVerificationCardSetId(JsonObject votingCardSetJson)
            throws ResourceNotFoundException {

        return votingCardSetJson.getString(JsonConstants.JSON_ATTRIBUTE_NAME_VERIFICATIONCARDSETID);
    }
}
