/*
 * 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 com.scytl.products.ov.config.io.ServicesInput;
import com.scytl.products.ov.datapacks.beans.CertificateParameters.Type;
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.EnumMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import java.util.Set;
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.cryptolib.scytl.keystore.factory.CryptoScytlKeyStoreWithPBKDF;
import com.scytl.cryptolib.scytl.keystore.service.ScytlKeyStoreService;
import com.scytl.cryptolib.securerandom.cryptoapi.CryptoAPIRandomString;
import com.scytl.products.ov.config.commons.exception.GenerateSystemKeysCommandException;
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 GenerateSystemKeysCommand implements Command {

    public static final int RANDOM_STRING_LENGTH = 26;

    public static final String SEPARATOR = "_";

    public static final String TENANT_NAME = "${tenantName}";

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

    public static final String ENCRYPTION = "_log_encryption";

    public static final String SIGNING = "_log_signing";

    public static final String P12 = ".p12";

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

    private final PrimitivesServiceAPI primitivesServiceAPI;

    private final PasswordCleaner passwordCleaner = new PasswordCleaner();

    private final ScytlKeyStoreService sksService;

    private static final CredentialPropertiesProvider credPropsProvider = new CredentialPropertiesProvider();

	private static final Map<CertificateParameters.Type, CredentialProperties> credentialPropertiesMap = new EnumMap<CertificateParameters.Type, CredentialProperties>(
			CertificateParameters.Type.class);

	private static final Map<CertificateParameters.Type, Properties> subjectPropertiesMap = new EnumMap<CertificateParameters.Type, Properties>(
			CertificateParameters.Type.class);

    GenerateSystemKeysCommand(PrimitivesServiceAPI primitivesService,
            ScytlKeyStoreService sksService) {
        primitivesServiceAPI = primitivesService;
        this.sksService = sksService;
    }

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

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

        final Path servicesPropertiesPath;
        try {
            servicesPropertiesPath = Paths.get(
                Paths.get(params2values.get(ActionParams.SERVICES_NAMES_PATH.getName())).toFile().getCanonicalPath());
        } catch (IOException e) {
            throw new GenerateSystemKeysCommandException("Could not get the absolute path of the servicesNames", e);
        }
        String tenantName = params2values.get(ActionParams.TENANT_NAME.getName());
        final Path outputPath = Paths.get(params2values.get(ActionParams.OUTPUT_FOLDER.getName()));
        FileUtils.createOutputDirectory(outputPath);

        char[] password = PasswordReaderUtils.readPasswordFromConsole();
        FileUtils.createOutputDirectory(outputPath);

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

        final CredentialProperties platformRootCredentialProperties =
            credPropsProvider.getPlatformRootCredentialPropertiesFromClassPath();

        PrivateKey platformRootPrivateKey =
            getPlatformRootPrivateKey(platformRootKeyStore, platformRootCredentialProperties, password);
        X509Certificate platformRootCA =
            (X509Certificate) getPlatformRootCA(platformRootKeyStore, platformRootCredentialProperties);

        passwordCleaner.clean(password);

        ServicesInput servicesInput = getServicesNameFromJson(servicesPropertiesPath);
        createSystemKeyStores(platformRootCA, platformRootPrivateKey,
                servicesInput.getServiceCertificateParameters(), tenantName, outputPath.toString());

    }

    /**
     * Create the keystores for all the system keys.
     *
     * @param platformRootCACert the platform CA certificate
     * @param platformRootPrivateKey the platform CA private key
     * @param serviceCertificateParameters a map with service names and their certificate's
     *         purposes
     * @param tenantName the name of the tenant
     * @param outputFolder the output folder as a string
     */
    private void createSystemKeyStores(final X509Certificate platformRootCACert,
            final PrivateKey platformRootPrivateKey,
            final Map<String, Set<Type>> serviceCertificateParameters, final String tenantName,
            final String outputFolder) {

        final CryptoAPIRandomString cryptoRandomString =
                primitivesServiceAPI.get32CharAlphabetCryptoRandomString();
        Map<String, char[]> keystorePasswords = new HashMap<>();

        // For each service...
        serviceCertificateParameters.forEach((serviceName, certificateParameters) -> {
            try {
                // ... create a Scytl keystore ...
                final CryptoScytlKeyStoreWithPBKDF keystore = (CryptoScytlKeyStoreWithPBKDF) sksService.createKeyStore();
                // ... create a password for the keystore ...
                char[] keystorePassword =
                        cryptoRandomString.nextRandom(RANDOM_STRING_LENGTH).toCharArray();
                // ... put away the SKS password ...
                keystorePasswords.put(serviceName, keystorePassword);

                // ... create a certificate of each of the required kinds ...
                certificateParameters.forEach(purpose -> {
                    LOG.info("Generating the certificates and keystore of {} for {}...",
                            serviceName, purpose.name());
                    KeyPair keyPair = CryptoUtils.generateKeyPairOfType(purpose);
                    CryptoAPIX509Certificate serviceCertificate =
                            generateSystemCertificate(keyPair, platformRootPrivateKey, serviceName,
                                    getSubjectProperties(purpose), purpose, platformRootCACert,
                                    tenantName);
                    // ... add the certificate chain up to the platform root CA ...
                    final Certificate[] certificateChain =
                            {serviceCertificate.getCertificate(), platformRootCACert};

                    try {
                        // ... place the private key in the keystore ...
                        keystore.setPrivateKeyEntry(getCredentialProperties(purpose).getAlias()
                                        .get(CertificatePropertiesConstants.PRIVATE_KEY_ALIAS),
                                keyPair.getPrivate(), keystorePassword, certificateChain);
                    } catch (GeneralCryptoLibException e) {
                        throw new GenerateSystemKeysCommandException("Could not store the private key in the SKS", e);
                    }
                    // ... and persist the new certificate.
                    CertificatePersister.saveCertificate(serviceCertificate, Paths.get(outputFolder,
                            tenantName + SEPARATOR + serviceName + SEPARATOR + purpose.name()
                                    + Constants.PEM));
                });

                // Persist the keystore.
                KeyStorePersister.saveKeyStore(keystore,
                        Paths.get(outputFolder, tenantName + SEPARATOR + serviceName + Constants.SKS),
                        keystorePassword);
            } catch (GeneralCryptoLibException e) {
                throw new GenerateSystemKeysCommandException(e);
            }
        });

        LOG.info(
                "The generated passwords for the services are the following, please take note of them carefully...");
        keystorePasswords.entrySet().stream().forEach(x -> System.out.println(
                tenantName + SEPARATOR + x.getKey() + Constants.EQUAL + String
                        .valueOf(x.getValue())));

    }

    /**
     * Get a certificate's subject properties.
     *
     * @param purpose the purpose the certificate is for
     * @return the properties
     */
    private Properties getSubjectProperties(Type purpose) {
        if (!subjectPropertiesMap.containsKey(purpose)) {
            // No properties for this purpose yet, load them.
            Properties subjectProperties = new Properties();
            try (InputStream input = Thread.currentThread().getContextClassLoader()
                    .getResourceAsStream(getCredentialProperties(purpose).getPropertiesFile())) {
                subjectProperties.load(input);
                // Add to cache.
                subjectPropertiesMap.put(purpose, subjectProperties);
            } catch (IOException e) {
                throw new GenerateSystemKeysCommandException(
                        "An error occurred while loading the system services certificate properties",
                        e);
            }
        }

        return subjectPropertiesMap.get(purpose);
    }

    /**
     * Get a certificate's credential properties.
     *
     * @param purpose the purpose the certificate is for
     * @return the properties
     */
    public static CredentialProperties getCredentialProperties(Type purpose) {
        if (!credentialPropertiesMap.containsKey(purpose)) {
            // No properties for this purpose yet, add to cache.
            credentialPropertiesMap.put(purpose,
                    credPropsProvider.getSystemCredentialPropertiesFromClassPath(purpose));
        }

        return credentialPropertiesMap.get(purpose);
    }



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

        CryptoAPIX509Certificate cryptoIssuerCertificate = null;
        try {
            cryptoIssuerCertificate = new CryptoX509Certificate(issuerCertificate);
        } catch (GeneralCryptoLibException e) {
			throw new GenerateSystemKeysCommandException(
					"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(
            subjectProperties.getProperty(CertificatePropertiesConstants.SUBJECT_COMMON_NAME_PROPERTY_NAME)
                .replace(SERVICE_NAME, serviceName).replace(TENANT_NAME, tenantName));
        certificateParameters.setUserSubjectOrgUnit(
            subjectProperties.getProperty(CertificatePropertiesConstants.SUBJECT_ORGANIZATIONAL_UNIT_PROPERTY_NAME));
        certificateParameters.setUserSubjectOrg(
            subjectProperties.getProperty(CertificatePropertiesConstants.SUBJECT_ORGANIZATION_PROPERTY_NAME));
        certificateParameters.setUserSubjectCountry(
            subjectProperties.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 = subjectProperties.getProperty(CertificatePropertiesConstants.START_DATE);
        String end = subjectProperties.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 GenerateSystemKeysCommandException(
					"the system tenant \"end\" sholud be after the system tenant \"start\"");
		}
		if (Date.from(notBefore.toInstant()).before(issuerCertificate.getNotBefore())) {
			throw new GenerateSystemKeysCommandException(
					"The system tenant \"start\" time should be strictly after the root \"start\" time");
		}
		if (Date.from(notAfter.toInstant()).after(issuerCertificate.getNotAfter())) {
			throw new GenerateSystemKeysCommandException(
					"The system tenant \"end\" time should be strictly before the root \"end\" time");
		}

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

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

        return systemEncryptionCert;
    }

    private ServicesInput getServicesNameFromJson(final Path pathToJson) {

        ConfigObjectMapper configObjectMapper = new ConfigObjectMapper();
        ServicesInput servicesInput = null;
        try {
            servicesInput = configObjectMapper.fromJSONFileToJava(pathToJson.toFile(), ServicesInput.class);
        } catch (IOException e) {
            throw new GenerateSystemKeysCommandException("An error occurred while loading the services file", e);
        }

        return servicesInput;
    }

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

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

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

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

        return privateKey;
    }

    private Certificate getPlatformRootCA(final CryptoAPIScytlKeyStore platformRootKeyStore,
            final CredentialProperties properties) {

        Certificate[] certificateChain = null;

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

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

        return certificateChain[0];
    }

}
