/*
 * 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.config.commands.electionevent;

import java.io.IOException;
import java.math.BigInteger;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.certificates.cryptoapi.CryptoAPIX509Certificate;
import com.scytl.cryptolib.certificates.factory.CryptoX509Certificate;
import com.scytl.cryptolib.elgamal.bean.ElGamalEncryptionParameters;
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.LoggingWriter;
import com.scytl.products.ov.commons.beans.EncryptionParameters;
import com.scytl.products.ov.config.commands.electionevent.datapacks.beans.ElGamalCredentialDataPack;
import com.scytl.products.ov.config.commands.electionevent.datapacks.beans.ElectionCredentialDataPack;
import com.scytl.products.ov.config.commands.electionevent.datapacks.beans.ElectionInputDataPack;
import com.scytl.products.ov.config.commands.electionevent.datapacks.generators.ElGamalCredentialDataPackGenerator;
import com.scytl.products.ov.config.commands.electionevent.datapacks.generators.ElectionCredentialDataPackGenerator;
import com.scytl.products.ov.config.commands.progress.ProgressManager_old;
import com.scytl.products.ov.config.exceptions.CreateElectionEventException;
import com.scytl.products.ov.config.exceptions.specific.GenerateElectionEventCAException;
import com.scytl.products.ov.config.logevents.ConfigGeneratorLogEvents;
import com.scytl.products.ov.config.logs.Loggable;
import com.scytl.products.ov.constants.Constants;
import com.scytl.products.ov.datapacks.beans.CredentialProperties;
import com.scytl.products.ov.utils.EncryptionParametersLoader;

/**
 * The class that generates all the stuff (certs, keystores, etc.) on the create election event command and return it
 * using a CreateElectionEventOutput class.
 */
public class CreateElectionEventGenerator {

    private static final Logger LOG = LoggerFactory.getLogger("std");

    private final ElectionCredentialDataPackGenerator electionCredentialDataPackGenerator;

    private final ElGamalCredentialDataPackGenerator elGamalCredentialDataPackGenerator;

    private final EncryptionParametersLoader encryptionParametersLoader;

    @Loggable
    private LoggingWriter logWriter;

    private final ProgressManager_old votersProgressManager;

    /**
     * Constructor.
     * 
     * @param electionCredentialDataPackGenerator
     * @param elGamalCredentialDataPackGenerator
     * @param encryptionParametersLoader
     * @param votersProgressManager
     */
    public CreateElectionEventGenerator(final ElectionCredentialDataPackGenerator electionCredentialDataPackGenerator,
            ElGamalCredentialDataPackGenerator elGamalCredentialDataPackGenerator,
            EncryptionParametersLoader encryptionParametersLoader, ProgressManager_old votersProgressManager) {
        this.electionCredentialDataPackGenerator = electionCredentialDataPackGenerator;
        this.elGamalCredentialDataPackGenerator = elGamalCredentialDataPackGenerator;
        this.encryptionParametersLoader = encryptionParametersLoader;
        this.votersProgressManager = votersProgressManager;
    }

    /**
     * Generates all the datapacks (certs, keystores) defined on the ElectionInputDataPack (inside
     * CreateElectionEventParametersHolder) and pass them using CreateElectionEventOutput class.
     *
     * @param sharedData
     * @return
     * @throws ConfigurationException
     * @throws GeneralCryptoLibException
     * @throws IOException
     */
    public CreateElectionEventOutput generate(final CreateElectionEventParametersHolder sharedData)
            throws ConfigurationException, GeneralCryptoLibException, IOException {

        final CreateElectionEventOutput createElectionEventOutput = new CreateElectionEventOutput();

        final ElectionInputDataPack electionInputDataPack = sharedData.getInputDataPack();

        final Map<String, CredentialProperties> configProperties =
            sharedData.getConfigurationInput().getConfigProperties();

        final List<CredentialProperties> sortedCredentialProperties = new ArrayList<>(configProperties.values());
        sortedCredentialProperties.sort((c1, c2) -> c1.getCredentialType().compareTo(c2.getCredentialType()));

        votersProgressManager.initProgress(sortedCredentialProperties.size(), sharedData.getInputDataPack().getEeid());

        for (final CredentialProperties current : sortedCredentialProperties) {

            try {
                LOG.info("Creating data pack for " + current.getName());

                electionInputDataPack.setCredentialProperties(current);
                if (StringUtils.isNotEmpty(current.getParentName())) {
                    electionInputDataPack.setParentKeyPair(
                        createElectionEventOutput.getDataPackMap().get(current.getParentName()).getKeyPair());
                }
                CryptoAPIX509Certificate[] parentCertificates = getParentCertificatesChain(current.getParentName(),
                    createElectionEventOutput.getDataPackMap(), configProperties);

                Properties certificateProperties =
                    sharedData.getCertificatePropertiesInput().getNameToCertificateProperties().get(current.getName());

                final ElectionCredentialDataPack dataPack = electionCredentialDataPackGenerator.generate(
                    electionInputDataPack, electionInputDataPack.getReplacementsHolder(), current.getName(),
                    Constants.EMPTY, certificateProperties, parentCertificates);
                createElectionEventOutput.getDataPackMap().put(current.getName(), dataPack);

                LOG.info("Data pack for " + current.getName() + " was successfully created");

                if (current.getName().equals(Constants.CONFIGURATION_ELECTION_CA_JSON_TAG)) {
                    // log success - CA certificate successfully generated and stored
                    logWriter.log(Level.INFO,
                        new LogContent.LogContentBuilder()
                            .logEvent(ConfigGeneratorLogEvents.GENEECA_SUCCESS_CA_CERTIFICATE_GENERATED)
                            .electionEvent(sharedData.getInputDataPack().getEeid()).user("adminID")
                            .additionalInfo("cert_cn", dataPack.getCertificate().getSubjectDn().getCommonName())
                            .additionalInfo("cert_sn", dataPack.getCertificate().getSerialNumber().toString())
                            .createLogInfo());
                }

            } catch (Exception e) {

                if (current.getName().equals(Constants.CONFIGURATION_ELECTION_CA_JSON_TAG)) {
                    // log error - Error generating the CA certificate
                    String commonName = "";
                    ElectionCredentialDataPack dataPack =
                        createElectionEventOutput.getDataPackMap().get(current.getName());
                    if (dataPack != null) {
                        commonName = dataPack.getCertificate().getSubjectDn().getCommonName();
                    }

                    logWriter.log(Level.ERROR,
                        new LogContent.LogContentBuilder()
                            .logEvent(ConfigGeneratorLogEvents.GENEECA_ERROR_GENERATING_CA_CERTIFICATE)
                            .electionEvent(sharedData.getInputDataPack().getEeid()).user("adminID")
                            .additionalInfo("cert_cn", commonName).additionalInfo("err_desc", e.getMessage())
                            .createLogInfo());
                    throw new GenerateElectionEventCAException(e);
                }

                throw new CreateElectionEventException(e);
            } finally {
                votersProgressManager.inc(sharedData.getInputDataPack().getEeid());
            }
        }

        final CredentialProperties authTokenSigner = sharedData.getConfigurationInput().getAuthTokenSigner();

        LOG.info("Creating data pack for the " + authTokenSigner.getName());

        electionInputDataPack.setCredentialProperties(authTokenSigner);

        electionInputDataPack.setParentKeyPair(
            createElectionEventOutput.getDataPackMap().get(authTokenSigner.getParentName()).getKeyPair());

        CryptoAPIX509Certificate[] parentCertificates = getParentCertificatesChain(authTokenSigner.getParentName(),
            createElectionEventOutput.getDataPackMap(), configProperties);

        String keyForProtectingKeystorePassword = sharedData.getKeyForProtectingKeystorePassword();

        Properties certificateProperties =
            sharedData.getCertificatePropertiesInput().getAuthTokenSignerCertificateProperties();

        createElectionEventOutput.setAuthTokenSigner(electionCredentialDataPackGenerator.generate(electionInputDataPack,
            electionInputDataPack.getReplacementsHolder(), Constants.CONFIGURATION_AUTH_TOKEN_SIGNER_JSON_TAG,
            keyForProtectingKeystorePassword, certificateProperties, parentCertificates));

        LOG.info("Data pack for " + authTokenSigner.getName() + " was successfully created");

        CredentialProperties primeEncryption = sharedData.getConfigurationInput().getPrimeEncryption();

        LOG.info("Creating data pack for the " + primeEncryption.getName());

        ElGamalEncryptionParameters encryptionParameters = getElGamalEncryptionParameters(sharedData);

        ElGamalCredentialDataPack primeEncryptionDataPack =
            elGamalCredentialDataPackGenerator.generate(primeEncryption, encryptionParameters, 1);

        createElectionEventOutput.setPrimeEncryption(primeEncryptionDataPack);

        LOG.info("Data pack for " + primeEncryption.getName() + " was successfully created");

        return createElectionEventOutput;

    }

    private CryptoAPIX509Certificate[] getParentCertificatesChain(final String parentName,
            final Map<String, ElectionCredentialDataPack> dataPackMap,
            final Map<String, CredentialProperties> credentialPropertiesMap) {

        List<CryptoAPIX509Certificate> parentCertificates = new ArrayList<>();
        if (parentName != null) {
            String currentParent = parentName;
            while (currentParent != null) {
                parentCertificates.add(dataPackMap.get(currentParent).getCertificate());
                currentParent = credentialPropertiesMap.get(currentParent).getParentName();
            }
        }
        return parentCertificates.toArray(new CryptoX509Certificate[parentCertificates.size()]);
    }

    private ElGamalEncryptionParameters getElGamalEncryptionParameters(CreateElectionEventParametersHolder sharedData)
            throws GeneralCryptoLibException, IOException {
        Path folder = sharedData.getElectionFolder();
        Path file = folder.resolve(Constants.ENCRYPTION_PARAMS_FILENAME);
        EncryptionParameters parameters = encryptionParametersLoader.load(file);
        BigInteger p = new BigInteger(parameters.getP());
        BigInteger q = new BigInteger(parameters.getQ());
        BigInteger g = new BigInteger(parameters.getG());
        return new ElGamalEncryptionParameters(p, q, g);
    }
}
