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

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Properties;

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

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.api.primitives.PrimitivesServiceAPI;
import com.scytl.cryptolib.certificates.bean.X509DistinguishedName;
import com.scytl.cryptolib.certificates.cryptoapi.CryptoAPIX509Certificate;
import com.scytl.cryptolib.certificates.factory.CryptoX509Certificate;
import com.scytl.cryptolib.scytl.keystore.cryptoapi.CryptoAPIScytlKeyStore;
import com.scytl.products.ov.config.commons.exception.GenerateControlComponentNodeCertsAndKeysException;
import com.scytl.products.ov.config.io.ControlComponentNodes;
import com.scytl.products.ov.config.io.CredentialPropertiesProvider;
import com.scytl.products.ov.config.utils.CryptoUtils;
import com.scytl.products.ov.config.utils.FileUtils;
import com.scytl.products.ov.constants.CertificatePropertiesConstants;
import com.scytl.products.ov.constants.Constants;
import com.scytl.products.ov.datapacks.beans.CertificateParameters;
import com.scytl.products.ov.datapacks.beans.CredentialProperties;
import com.scytl.products.ov.datapacks.generators.X509CertificateGenerator;
import com.scytl.products.ov.utils.CertificatePersister;
import com.scytl.products.ov.utils.ConfigObjectMapper;
import com.scytl.products.ov.utils.KeyStoreLoader;
import com.scytl.products.ov.utils.KeyStorePersister;
import com.scytl.products.ov.utils.PasswordCleaner;
import com.scytl.products.ov.utils.PasswordReaderUtils;

public class GenerateControlComponentNodeCertsAndKeys implements Command {

    public static final int RANDOM_STRING_LENGTH = 26;

    public static final String ENCRYPTION = "_log_encryption";

    public static final String SIGNING = "_log_signing";

    public static final String P12_SUFFIX = ".p12";

    public static final String SERVICE_NAME = "${serviceName}";

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

    private final PasswordCleaner passwordCleaner;

    private final PrimitivesServiceAPI primitivesServiceAPI;

    public GenerateControlComponentNodeCertsAndKeys(PrimitivesServiceAPI primitivesService) {

        this.passwordCleaner = new PasswordCleaner();
        this.primitivesServiceAPI = primitivesService;
    }

    @Override
    public void execute(Map<String, String> params2values) {

        LOG.info("Going to generate node certificates and keys...");

        final Path platformRootKeyStorePath;
        try {
            platformRootKeyStorePath = Paths
                .get(Paths.get(params2values.get(ActionParams.KEYSTORE_PATH.getName())).toFile().getCanonicalPath());
        } catch (IOException e) {
			throw new GenerateControlComponentNodeCertsAndKeysException(
					"Could not get the absolute path of the keystore", e);
        }

        final String outputPathString = params2values.get(ActionParams.OUTPUT_FOLDER.getName());
        Path outputPath = Paths.get(outputPathString);
        FileUtils.createOutputDirectory(outputPath);

        LOG.info("Will try to open Platform Root CA keystore...");

        char[] platformRootKeyStorePassword = PasswordReaderUtils.readPasswordFromConsole();

        CredentialPropertiesProvider credPropsProvider = new CredentialPropertiesProvider();
        final CredentialProperties platformRootCredentialProperties =
            credPropsProvider.getPlatformRootCredentialPropertiesFromClassPath();

        CryptoAPIScytlKeyStore platformRootKeyStore =
            KeyStoreLoader.loadPlatformRootKeyStore(platformRootKeyStorePath, platformRootKeyStorePassword);

        PrivateKey platformRootPrivateKey = getPlatformRootPrivateKey(platformRootKeyStore,
            platformRootCredentialProperties, platformRootKeyStorePassword);

        passwordCleaner.clean(platformRootKeyStorePassword);

        LOG.info("Obtained platform Root CA privatekey.");

        Certificate[] certificateChain = getCertificateChain(platformRootKeyStore, platformRootCredentialProperties);

        LOG.info("Obtained platform Root CA certificate.");

        List<String> nodeNames = getListOfNodeNames(params2values).getNodes();

        for (String nodeName : nodeNames) {

            try {

				createAllKeysAndCertsForOneNode(certificateChain, platformRootPrivateKey, nodeName, outputPathString);

            } catch (GeneralCryptoLibException | IOException e) {
                LOG.error("Generation of CCN data failed: " + e.getMessage());
				throw new GenerateControlComponentNodeCertsAndKeysException(
						"Could not generate the CCN certificates and keys", e);
            }
        }
    }

    private void createAllKeysAndCertsForOneNode(Certificate[] certificateChain, PrivateKey platformRootPrivateKey,
            String nodeName, String outputPathString)
            throws GeneralCryptoLibException, IOException {

        LOG.info("---- Generating data for " + nodeName + " ----");

        KeyStore keyStore = CryptoUtils.createEmptyKeystore();

        char[] keystorePassword =
            primitivesServiceAPI.get32CharAlphabetCryptoRandomString().nextRandom(RANDOM_STRING_LENGTH).toCharArray();

		generateCA(certificateChain, platformRootPrivateKey, nodeName, keyStore, keystorePassword, outputPathString);

        KeyStorePersister.saveKeyStore(keyStore, Paths.get(outputPathString, nodeName + P12_SUFFIX), keystorePassword);

        System.out.println(String.format("%s=%s", nodeName, String.valueOf(keystorePassword)));

        passwordCleaner.clean(keystorePassword);
    }

	private void generateCA(Certificate[] certificateChain, PrivateKey platformRootPrivateKey, String name,
			KeyStore keyStore, char[] keystorePassword, String outputPathString) throws GeneralCryptoLibException {

        LOG.info("Generating CA for " + name);

        KeyPair keyPairForSigning = CryptoUtils.generateKeyPairOfType(CertificateParameters.Type.SIGN);

        LOG.info("Generating the certificate...");

        CredentialPropertiesProvider credPropsProvider = new CredentialPropertiesProvider();

		final CredentialProperties controlComponentNodeCredentialProperties = credPropsProvider
				.getControlComponentNodePropertiesFromClassPath();

        final Properties properties = getPropertiesFrom(controlComponentNodeCredentialProperties);

        X509Certificate issuerCertificate = (X509Certificate) certificateChain[0];
        CryptoAPIX509Certificate controlComponentNodeCACert = generateCertificate(keyPairForSigning,
            platformRootPrivateKey, name, properties, CertificateParameters.Type.INTERMEDIATE, issuerCertificate);

        LOG.info("Adding key to the keystore...");

        CryptoUtils.addToKeystore(keyStore, keyPairForSigning, controlComponentNodeCredentialProperties,
            controlComponentNodeCACert, name, keystorePassword, certificateChain);

        LOG.info("Persisting the certificate...");

        CertificatePersister.saveCertificate(controlComponentNodeCACert,
            Paths.get(outputPathString, name + "_CA" + Constants.PEM));

        LOG.info("Finished generation of CA for " + name);
    }

    private CryptoAPIX509Certificate generateCertificate(final KeyPair keyPair, final PrivateKey issuerPrivateKey,
            final String serviceName, final Properties properties, final CertificateParameters.Type credentialType,
            final X509Certificate issuerCertificate) {

        CryptoAPIX509Certificate cryptoIssuerCertificate;
        try {
            cryptoIssuerCertificate = new CryptoX509Certificate(issuerCertificate);
        } catch (GeneralCryptoLibException e) {
			throw new GenerateControlComponentNodeCertsAndKeysException(
					"An error occurred while loading platform root certificate properties", e);
        }

        final PublicKey publicKey = keyPair.getPublic();

        X509CertificateGenerator certificateGenerator = CryptoUtils.createCertificateGenerator();

        final CertificateParameters certificateParameters = new CertificateParameters();

        certificateParameters.setType(credentialType);

        certificateParameters
            .setUserSubjectCn(properties.getProperty(CertificatePropertiesConstants.SUBJECT_COMMON_NAME_PROPERTY_NAME)
                .replace(SERVICE_NAME, serviceName));
        certificateParameters.setUserSubjectOrgUnit(
            properties.getProperty(CertificatePropertiesConstants.SUBJECT_ORGANIZATIONAL_UNIT_PROPERTY_NAME));
        certificateParameters.setUserSubjectOrg(
            properties.getProperty(CertificatePropertiesConstants.SUBJECT_ORGANIZATION_PROPERTY_NAME));
        certificateParameters.setUserSubjectCountry(
            properties.getProperty(CertificatePropertiesConstants.SUBJECT_COUNTRY_PROPERTY_NAME));

        X509DistinguishedName issuerDn = cryptoIssuerCertificate.getIssuerDn();
        certificateParameters.setUserIssuerCn(issuerDn.getCommonName());
        certificateParameters.setUserIssuerOrgUnit(issuerDn.getOrganizationalUnit());
        certificateParameters.setUserIssuerOrg(issuerDn.getOrganization());
        certificateParameters.setUserIssuerCountry(issuerDn.getCountry());

        String start = properties.getProperty(CertificatePropertiesConstants.START_DATE);
        String end = properties.getProperty(CertificatePropertiesConstants.END_DATE);
        ZonedDateTime notBefore = ZonedDateTime.ofInstant(Instant.parse(start), ZoneOffset.UTC);
        ZonedDateTime notAfter = ZonedDateTime.ofInstant(Instant.parse(end), ZoneOffset.UTC);

        if (notAfter.isBefore(notBefore)) {
			throw new GenerateControlComponentNodeCertsAndKeysException(
					"The validity period of the certificate is empty");
        }

        if (Date.from(notBefore.toInstant()).before(cryptoIssuerCertificate.getNotBefore())) {
			throw new GenerateControlComponentNodeCertsAndKeysException(
					"The tenant \"start\" time should be strictly after the root \"start\" time");
        }
        if (Date.from(notAfter.toInstant()).after(cryptoIssuerCertificate.getNotAfter())) {
			throw new GenerateControlComponentNodeCertsAndKeysException(
					"The tenant \"end\" time should be strictly before the root \"end\" time");
        }

        certificateParameters.setUserNotBefore(notBefore);
        certificateParameters.setUserNotAfter(notAfter);

        CryptoAPIX509Certificate platformRootCACert = null;
        try {
            platformRootCACert = certificateGenerator.generate(certificateParameters, publicKey, issuerPrivateKey);
        } catch (ConfigurationException | GeneralCryptoLibException e) {
			throw new GenerateControlComponentNodeCertsAndKeysException(
					"An error occurred while creating the certificate of service " + serviceName, e);
        }

        return platformRootCACert;
    }

    private ControlComponentNodes getListOfNodeNames(Map<String, String> params2values) {

        final Path ccNodeListFilePath;
        try {
            ccNodeListFilePath =
                Paths.get(Paths.get(params2values.get(ActionParams.CONTROL_COMPONENTS_NODES_PATH.getName())).toFile()
                    .getCanonicalPath());
        } catch (IOException e) {
			throw new GenerateControlComponentNodeCertsAndKeysException(
					"Could not get the absolute path of the control components node list file", e);
        }

        ConfigObjectMapper configObjectMapper = new ConfigObjectMapper();
        ControlComponentNodes controlComponentNodes;
        try {
            controlComponentNodes =
                configObjectMapper.fromJSONFileToJava(ccNodeListFilePath.toFile(), ControlComponentNodes.class);
        } catch (IOException e) {
			throw new GenerateControlComponentNodeCertsAndKeysException(
					"An error occurred while loading the control component nodes list file", e);
        }

        return controlComponentNodes;
    }

    private Properties getPropertiesFrom(final CredentialProperties credentialPropertiesServicesSigner) {

        final Properties subjectPropertiesSigner = new Properties();
        try (InputStream input = Thread.currentThread().getContextClassLoader()
            .getResourceAsStream(credentialPropertiesServicesSigner.getPropertiesFile())) {
            subjectPropertiesSigner.load(input);
        } catch (IOException e) {
			throw new GenerateControlComponentNodeCertsAndKeysException(
					"An error occurred while loading the certificate properties", e);
        }
        return subjectPropertiesSigner;
    }

    private PrivateKey getPlatformRootPrivateKey(final CryptoAPIScytlKeyStore platformRootKeyStore,
            final CredentialProperties properties, final char[] password) {

        KeyStore.PasswordProtection passwordProtection = new KeyStore.PasswordProtection(password);
        PrivateKey privateKey;

        String alias = properties.getAlias().get(CertificatePropertiesConstants.PRIVATE_KEY_ALIAS);

        try {
            privateKey = platformRootKeyStore.getPrivateKeyEntry(alias, passwordProtection.getPassword());
        } catch (GeneralCryptoLibException e) {
			throw new GenerateControlComponentNodeCertsAndKeysException("The private key could not be retrieved", e);
        }

        return privateKey;
    }

    private Certificate[] getCertificateChain(final CryptoAPIScytlKeyStore platformRootKeyStore,
            final CredentialProperties properties) {

        Certificate[] certificateChain;

        String alias = properties.getAlias().get(CertificatePropertiesConstants.PRIVATE_KEY_ALIAS);

        try {
            certificateChain = platformRootKeyStore.getCertificateChain(alias);
        } catch (GeneralCryptoLibException e) {
			throw new GenerateControlComponentNodeCertsAndKeysException("The certificate chain could not be retrieved",
					e);
        }

        return certificateChain;
    }
}
