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

import com.scytl.products.ov.datapacks.beans.CertificateParameters.Type;
import com.scytl.products.ov.datapacks.beans.CreateVotingCardSetCertificatePropertiesContainer;

import java.io.FileInputStream;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Path;
import java.security.SignatureException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Base64.Decoder;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import javax.annotation.PostConstruct;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonString;
import javax.json.JsonValue;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.lang.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.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.primitives.primes.utils.PemUtils;
import com.scytl.products.ov.commons.path.PathResolver;
import com.scytl.products.ov.commons.util.JsonUtils;
import com.scytl.products.ov.sdm.application.exception.DatabaseException;
import com.scytl.products.ov.sdm.application.service.CertificateManagementException;
import com.scytl.products.ov.sdm.application.service.ElectionEventService;
import com.scytl.products.ov.sdm.application.service.PlatformRootCAService;
import com.scytl.products.ov.sdm.domain.common.JobStatus;
import com.scytl.products.ov.sdm.domain.model.ballotbox.BallotBoxRepository;
import com.scytl.products.ov.sdm.domain.model.config.CreateVotingCardSetInput;
import com.scytl.products.ov.sdm.domain.model.config.StartVotingCardGenerationJobResponse;
import com.scytl.products.ov.sdm.domain.model.config.VotingCardGenerationJobStatus;
import com.scytl.products.ov.sdm.domain.model.electionevent.ElectionEventRepository;
import com.scytl.products.ov.sdm.domain.model.generator.DataGeneratorResponse;
import com.scytl.products.ov.sdm.domain.model.status.Status;
import com.scytl.products.ov.sdm.domain.model.votingcardset.VotingCardSetRepository;
import com.scytl.products.ov.sdm.domain.service.ConfigConstants;
import com.scytl.products.ov.sdm.domain.service.ProgressManagerService;
import com.scytl.products.ov.sdm.domain.service.VotingCardSetDataGeneratorService;
import com.scytl.products.ov.sdm.domain.service.exception.VotingCardSetDataGeneratorServiceException;
import com.scytl.products.ov.sdm.domain.service.utils.SystemTenantPublicKeyLoader;
import com.scytl.products.ov.sdm.infrastructure.JsonConstants;
import com.scytl.products.ov.sdm.infrastructure.service.ConfigurationEntityStatusService;

/**
 * This implementation contains a call to the configuration engine for
 * generating the voting card set data.
 */
@Service
public class VotingCardSetDataGeneratorServiceImpl
        implements VotingCardSetDataGeneratorService {

    // The name of the json parameter number of voting card to generate.
    private static final String JSON_PARAM_NAME_NR_OF_VC_TO_GENERATE =
        "numberOfVotingCardsToGenerate";

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

    private static final String BASE_JOBS_URL_PATH =
        "/{tenantId}/{electionEventId}/jobs";

    @Autowired
    private VotingCardSetRepository votingCardSetRepository;

    @Autowired
    private BallotBoxRepository ballotBoxRepository;

    @Autowired
    private ElectionEventRepository electionEventRepository;

    @Autowired
    private SystemTenantPublicKeyLoader systemTenantPublicKeyLoader;

    @Autowired
    private PathResolver pathResolver;

    @Autowired
    private ConfigurationEntityStatusService configurationEntityStatusService;

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

    @Value("${CONFIG_GENERATOR_URL}")
    private String configServiceBaseUrl;

    @Autowired
    private RestTemplate restClient;

    @Autowired
    private ProgressManagerService<VotingCardGenerationJobStatus> progressManagerService;

    @Autowired
    private PlatformRootCAService platformRootCAService;

    @Autowired
    private CCPublicKeySignatureValidator keySignatureValidator;
    
    @Autowired
    private ElectionEventService electionEventService;

    @Value("${credential.sign.certificate.properties}")
    private String credentialSignCertificateProperties;

    @Value("${credential.auth.certificate.properties}")
    private String credentialAuthCertificateProperties;

    @Value("${voting.card.set.certificate.properties}")
    private String votingCardSetCertificateProperties;

    @Value("${verification.card.set.certificate.properties}")
    private String verificationCardSetCertificateProperties;
    
    private ExecutorService jobCompletionExecutor;

    @PostConstruct
    void setup() {
        jobCompletionExecutor = Executors.newCachedThreadPool();
    }

    /**
     * This implementation creates the input and used it to call the
     * configuration engine. First prepares the list of parameters to be sent as
     * input. One of the parameters, which is the path where the ballot can be
     * found, is hardcoded for now.
     *
     * @see com.scytl.products.ov.sdm.domain.service.BallotDataGeneratorService#generate(String,
     *      String)
     */
    @Override
    public DataGeneratorResponse generate(final String id,
            final String electionEventId) {
        DataGeneratorResponse result = new DataGeneratorResponse();

        try {

            // basic validation of the input
            if (StringUtils.isEmpty(id)) {
                result.setSuccessful(false);
                return result;
            }

            String votingCardSetAsJson = votingCardSetRepository.find(id);
            // simple check if there is a voting card set data returned
            if (JsonConstants.JSON_EMPTY_OBJECT
                .equals(votingCardSetAsJson)) {
                result.setSuccessful(false);
                return result;
            }

            // create the list of parameters to call the configuration json
            JsonObject votingCardSet =
                JsonUtils.getJsonObject(votingCardSetAsJson);
            String verificationCardSetId = votingCardSet.getString(
                JsonConstants.JSON_ATTRIBUTE_NAME_VERIFICATIONCARDSETID);
            Path configPath = pathResolver
                .resolve(ConfigConstants.CONFIG_FILES_BASE_DIR);
            Path configElectionEventPath =
                configPath.resolve(electionEventId);
            String ballotBoxId = votingCardSet
                .getJsonObject(
                    JsonConstants.JSON_ATTRIBUTE_NAME_BALLOT_BOX)
                .getString(JsonConstants.JSON_ATTRIBUTE_NAME_ID);
            String ballotBoxAsJson = ballotBoxRepository.find(ballotBoxId);
            JsonObject ballotBox =
                JsonUtils.getJsonObject(ballotBoxAsJson);
            String ballotId = ballotBox
                .getJsonObject(JsonConstants.JSON_ATTRIBUTE_NAME_BALLOT)
                .getString(JsonConstants.JSON_ATTRIBUTE_NAME_ID);
            String electoralAuthorityId = ballotBox
                .getJsonObject(
                    JsonConstants.JSON_ATTRIBUTE_NAME_ELECTORAL_AUTHORITY)
                .getString(JsonConstants.JSON_ATTRIBUTE_NAME_ID);
            JsonObject electionEvent = JsonUtils.getJsonObject(
                electionEventRepository.find(electionEventId));
            Path destinationBallotFilePath = pathResolver
                .resolve(ConfigConstants.CONFIG_FILES_BASE_DIR)
                .resolve(electionEventId)
                .resolve(ConfigConstants.CONFIG_DIR_NAME_ONLINE)
                .resolve(
                    ConfigConstants.CONFIG_DIR_NAME_ELECTIONINFORMATION)
                .resolve(ConfigConstants.CONFIG_DIR_NAME_BALLOTS)
                .resolve(ballotId)
                .resolve(ConfigConstants.CONFIG_FILE_NAME_BALLOT_JSON);

            List<String> choiceCodeEncryptionKey =
                getChoiceCodesEncryptionKey(votingCardSet);
            validateChoiceCodesEncryptionKey(electionEventId,
                verificationCardSetId, choiceCodeEncryptionKey);
            
            String primeEncryptionPrivateKeyJson = 
                    electionEventService.getPrimeEncryptionPrivateKey(electionEventId).toJson();

            CreateVotingCardSetInput createVotingCardSetInput =
                new CreateVotingCardSetInput();
            createVotingCardSetInput.setStart(ballotBox
                .getString(JsonConstants.JSON_ATTRIBUTE_NAME_DATE_FROM));
            createVotingCardSetInput
                .setElectoralAuthorityID(electoralAuthorityId);
            createVotingCardSetInput.setEnd(ballotBox
                .getString(JsonConstants.JSON_ATTRIBUTE_NAME_DATE_TO));
            createVotingCardSetInput.setValidityPeriod(electionEvent
                .getJsonObject(JsonConstants.JSON_ATTRIBUTE_NAME_SETTINGS)
                .getInt(
                    JsonConstants.JSON_ATTRIBUTE_CERTIFICATES_VALIDITY));
            createVotingCardSetInput
                .setBasePath(configElectionEventPath.toString());
            createVotingCardSetInput.setBallotBoxID(ballotBoxId);
            createVotingCardSetInput.setBallotID(ballotId);
            createVotingCardSetInput
                .setBallotPath(destinationBallotFilePath.toString());
            createVotingCardSetInput.setEeID(electionEventId);
            createVotingCardSetInput.setNumberVotingCards(votingCardSet
                .getInt(JSON_PARAM_NAME_NR_OF_VC_TO_GENERATE));
            createVotingCardSetInput
                .setVerificationCardSetID(verificationCardSetId);
            createVotingCardSetInput.setVotingCardSetID(id);

            // INCLUDE ALIAS INSIDE THE OBJECT...
            createVotingCardSetInput.setVotingCardSetAlias(votingCardSet
                .getString(JsonConstants.JSON_ATTRIBUTE_NAME_ALIAS, ""));

            createVotingCardSetInput.setKeyForProtectingKeystorePassword(
                getPublicKeyForProtectingKeystorePassword());
            createVotingCardSetInput
                .setChoiceCodesEncryptionKey(choiceCodeEncryptionKey);
            
            createVotingCardSetInput.setPrimeEncryptionPrivateKeyJson(primeEncryptionPrivateKeyJson);
            
            createVotingCardSetInput
                .setPlatformRootCACertificate(PemUtils.certificateToPem(platformRootCAService.load()));
            
            createVotingCardSetInput.setCreateVotingCardSetCertificateProperties(getCertificateProperties());
            
            final ResponseEntity<StartVotingCardGenerationJobResponse> startJobResponse;
            try {
                startJobResponse = sendStartJobRequest(tenantId,
                    electionEventId, createVotingCardSetInput);
            } catch (Exception e) {
                LOGGER.error(
                    "Failed to send start voting card generation job request.",
                    e);
                result.setSuccessful(false);
                return result;
            }

            if (didJobStart(startJobResponse)) {
                StartVotingCardGenerationJobResponse jobResponse =
                    startJobResponse.getBody();
                final String jobId = jobResponse.getJobId();
                final Future<VotingCardGenerationJobStatus> future =
                    registerJobForStatusProgressUpdate(jobId,
                        startJobResponse);

                // wait for job to complete to update the verification card set
                // id and make it possible
                // to sign the voting card set.
                waitForJobCompletion(id, jobId, future);
                result.setResult(jobId);
                result.setSuccessful(true);

            } else {
                LOGGER.error(
                    "Voting card generation job failed to start. Server response: {}",
                    startJobResponse);
                result.setSuccessful(false);
            }

        } catch (DatabaseException e) {
            LOGGER.error("Error storing in database", e);
            result.setSuccessful(false);
        } catch (Exception e) {
            LOGGER.error("Error processing request", e);
            result.setSuccessful(false);
        }
        return result;
    }


    private CreateVotingCardSetCertificatePropertiesContainer getCertificateProperties() throws ConfigurationException, IOException {

        LOGGER.info("Obtaining certificate properties from the following paths:");
        LOGGER.info(" " + credentialSignCertificateProperties);
        LOGGER.info(" " + credentialAuthCertificateProperties);
        LOGGER.info(" " + votingCardSetCertificateProperties);
        LOGGER.info(" " + verificationCardSetCertificateProperties);

        CreateVotingCardSetCertificatePropertiesContainer createVotingCardSetCertificateProperties =
            new CreateVotingCardSetCertificatePropertiesContainer();

        Properties loadedCredentialSignCertificateProperties =
            getCertificateParameters(credentialSignCertificateProperties);
        Properties loadedCredentialAuthCertificateProperties =
            getCertificateParameters(credentialAuthCertificateProperties);
        Properties loadedVotingCardSetCertificateProperties =
            getCertificateParameters(votingCardSetCertificateProperties);
        Properties loadedVerificationCardSetCertificateProperties =
            getCertificateParameters(verificationCardSetCertificateProperties);

        createVotingCardSetCertificateProperties
            .setCredentialSignCertificateProperties(loadedCredentialSignCertificateProperties);
        createVotingCardSetCertificateProperties
            .setCredentialAuthCertificateProperties(loadedCredentialAuthCertificateProperties);
        createVotingCardSetCertificateProperties
            .setVotingCardSetCertificateProperties(loadedVotingCardSetCertificateProperties);
        createVotingCardSetCertificateProperties
            .setVerificationCardSetCertificateProperties(loadedVerificationCardSetCertificateProperties);

        LOGGER.info("Obtained certificate properties");

        return createVotingCardSetCertificateProperties;
    }
    
    private Properties getCertificateParameters(String path) throws IOException, ConfigurationException {

        final Properties props = new Properties();
        props.load(new FileInputStream(path));
        return props;
    }

    private void validateChoiceCodesEncryptionKey(String electionEventId,
            String verificationCardId, List<String> keys) {
        try {
            X509Certificate rootCACertificate =
                platformRootCAService.load();
            Decoder decoder = Base64.getDecoder();
            for (String string : keys) {
                JsonObject object = JsonUtils.getJsonObject(string);
                ElGamalPublicKey key = ElGamalPublicKey
                    .fromJson(object.getString("publicKey"));
                byte[] signature =
                    decoder.decode(object.getString("signature"));
                X509Certificate signingCertificate =
                    (X509Certificate) PemUtils.certificateFromPem(
                        object.getString("signerCertificate"));
                X509Certificate nodeCACertificate =
                    (X509Certificate) PemUtils.certificateFromPem(
                        object.getString("nodeCACertificate"));
                X509Certificate[] chain = {signingCertificate,
                        nodeCACertificate, rootCACertificate };
                keySignatureValidator
                    .checkChoiceCodesEncryptionKeySignature(signature,
                        chain, key, electionEventId, verificationCardId);
            }
        } catch (SignatureException | GeneralCryptoLibException | CertificateManagementException e) {
            throw new IllegalStateException(
                "Invalid choice codes encryption keys.", e);
        }
    }

    private List<String> getChoiceCodesEncryptionKey(
            JsonObject votingCardSet) {
        String arrayString = votingCardSet.getString(
            JsonConstants.JSON_ATTRIBUTE_CHOICE_CODES_ENCRYPTION_KEY);
        JsonArray array = JsonUtils.getJsonArray(arrayString);
        List<String> result = new ArrayList<>(array.size());
        for (JsonValue value : array) {
            result.add(((JsonString) value).getString());
        }
        return result;
    }

    private void waitForJobCompletion(final String votingCardSetId,
            final String jobId,
            final Future<VotingCardGenerationJobStatus> future) {
        jobCompletionExecutor.submit(() -> {
            final VotingCardGenerationJobStatus votingCardGenerationJobStatus;
            try {
                votingCardGenerationJobStatus = future.get();
                if (JobStatus.COMPLETED
                    .equals(votingCardGenerationJobStatus.getStatus())) {
                    // update the status of the voting card set
                    configurationEntityStatusService.update(
                        Status.GENERATED.name(), votingCardSetId,
                        votingCardSetRepository);
                    LOGGER.info(
                        "VotingCardSet generation job '{}' has completed successfully. Final status: {}",
                        jobId, votingCardGenerationJobStatus);
                } else {
                    LOGGER.error(
                        "Voting card generation job failed to complete. Response: {}",
                        votingCardGenerationJobStatus);
                }
            } catch (InterruptedException e) {
                LOGGER.error(
                    "We got interrupted while waiting for job completion.");
                Thread.currentThread().isInterrupted();
            } catch (ExecutionException e) {
                LOGGER.error(
                    "Unexpected exception while waiting for job completion.",
                    e);
            }
        });
    }

    private Future<VotingCardGenerationJobStatus> registerJobForStatusProgressUpdate(
            final String jobId, final ResponseEntity<?> response) {
        URI statusLocation = response.getHeaders().getLocation();
        // shouldn't be null, but...
        if (statusLocation != null) {
            return  progressManagerService.registerJob(jobId, statusLocation);
        } else {
			final String error = String.format("Failed to get location header for status update for job '%s'", jobId);
            LOGGER.error(error);
            throw new VotingCardSetDataGeneratorServiceException(error);
        }
    }

	private ResponseEntity<StartVotingCardGenerationJobResponse> sendStartJobRequest(final String tenantId,
			final String electionEventId, final CreateVotingCardSetInput input) {

        final String targetUrl = configServiceBaseUrl + BASE_JOBS_URL_PATH;
        try {
			return restClient.postForEntity(targetUrl, input, StartVotingCardGenerationJobResponse.class, tenantId,
					electionEventId);
        } catch (RestClientException e) {
			LOGGER.error("Error performing post request to endpoint '{}' : '{}'", targetUrl, e);
			throw new VotingCardSetDataGeneratorServiceException(
					"Error performing post request to endpoint " + targetUrl, e);
        }
    }

    private boolean didJobStart(final ResponseEntity<?> response) {
        return HttpStatus.CREATED.value() == response.getStatusCode()
            .value();
    }

    private String getPublicKeyForProtectingKeystorePassword()
            throws IOException {
        return systemTenantPublicKeyLoader.load(tenantId, "VV", Type.ENCRYPTION);
    }
}
