/*
 * 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.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.securerandom.cryptoapi.CryptoAPIRandomString;
import com.scytl.products.ov.config.commons.exception.GenerateLoggingKeysCommandException;
import com.scytl.products.ov.config.io.CredentialPropertiesProvider;
import com.scytl.products.ov.config.io.ServicesInput;
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 GenerateLoggingKeysCommand implements Command {

    public static final int RANDOM_STRING_LENGTH = 26;

    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 IServiceNamesProvider<ServicesInput> serviceNamesProvider;

    private final PrimitivesServiceAPI primitivesServiceAPI;

    private final PasswordCleaner passwordCleaner = new PasswordCleaner();

    public GenerateLoggingKeysCommand(PrimitivesServiceAPI primitivesService) {

        primitivesServiceAPI = primitivesService;
    }

    /**
     * Sets new serviceNamesProvider.
     *
     * @param serviceNamesProvider
     *            New value of serviceNamesProvider.
     */
    public void setServiceNamesProvider(final IServiceNamesProvider<ServicesInput> serviceNamesProvider) {
        this.serviceNamesProvider = serviceNamesProvider;
    }

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

        setServiceNamesProvider(new JsonServiceNamesProvider());

        final Path platformRootKeyStorePath;
        try {
            platformRootKeyStorePath = Paths
                .get(Paths.get(params2values.get(ActionParams.KEYSTORE_PATH.getName())).toFile().getCanonicalPath());
        } catch (IOException e) {
            throw new GenerateLoggingKeysCommandException("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 GenerateLoggingKeysCommandException("Could not get the absolute path of the servicesNames", e);
        }

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

        LOG.info("Opening platform keystore...");

        CredentialPropertiesProvider credPropsProvider = new CredentialPropertiesProvider();

        ServicesInput servicesInput = getServicesNameFromJson(servicesPropertiesPath);

        char[] password = PasswordReaderUtils.readPasswordFromConsole();

        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);

        try {

            final CredentialProperties credentialPropertiesServicesSigner =
                credPropsProvider.getServicesLoggingSigningCredentialPropertiesFromClassPath();
            final CredentialProperties credentialPropertiesServicesEncryption =
                credPropsProvider.getServicesLoggingEncryptionCredentialPropertiesFromClassPath();

            createServicesLoggingKeys(credentialPropertiesServicesSigner, credentialPropertiesServicesEncryption,
                platformRootCA, platformRootPrivateKey, serviceNamesProvider.getServiceNames(
                            servicesInput),
                outputPathString);

        } catch (GeneralCryptoLibException e) {
            throw new GenerateLoggingKeysCommandException("Could not generate the logging keys", e);
        }
    }

    private void createServicesLoggingKeys(final CredentialProperties credentialPropertiesServicesSigner,
            final CredentialProperties credentialPropertiesServicesEncryption, final X509Certificate platformRootCACert,
            final PrivateKey platformRootPrivateKey, final Set<String> serviceNames, final String output)
            throws GeneralCryptoLibException {

        final CryptoAPIRandomString cryptoRandomString = primitivesServiceAPI.get32CharAlphabetCryptoRandomString();

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

        Map<String, char[]> keystorePasswords = new HashMap<>();

        final Properties subjectPropertiesSigner = getPropertiesFrom(credentialPropertiesServicesSigner);
        final Properties subjectPropertiesEnc = getPropertiesFrom(credentialPropertiesServicesEncryption);

        for (String serviceName : serviceNames) {

            LOG.info("Generating the certificates and keystore of " + serviceName + "...");

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

            CryptoAPIX509Certificate serviceSignerCertificate =
                generateCertificate(keyPairForSigning, platformRootPrivateKey, serviceName, subjectPropertiesSigner,
                    credentialPropertiesServicesSigner.getCredentialType(), cryptoIssuerCertificate);

            CryptoAPIX509Certificate serviceEncryptionCertificate =
                generateCertificate(keyPairForEncryption, platformRootPrivateKey, serviceName, subjectPropertiesEnc,
                    credentialPropertiesServicesEncryption.getCredentialType(), cryptoIssuerCertificate);

            char[] keystorePasswordSigning = cryptoRandomString.nextRandom(RANDOM_STRING_LENGTH).toCharArray();
            char[] keystorePasswordEncryption = cryptoRandomString.nextRandom(RANDOM_STRING_LENGTH).toCharArray();

            Certificate[] chain = new Certificate[] {platformRootCACert };
            final KeyStore keyStoreSigning =
                CryptoUtils.createP12AndAddKey(keyPairForSigning, credentialPropertiesServicesSigner,
                    serviceSignerCertificate, serviceName, keystorePasswordSigning, chain);
            final KeyStore keyStoreEncryption =
                CryptoUtils.createP12AndAddKey(keyPairForEncryption, credentialPropertiesServicesEncryption,
                    serviceEncryptionCertificate, serviceName, keystorePasswordEncryption, chain);

            keystorePasswords.put(serviceName + SIGNING, keystorePasswordSigning);
            keystorePasswords.put(serviceName + ENCRYPTION, keystorePasswordEncryption);

            KeyStorePersister.saveKeyStore(keyStoreSigning, Paths.get(output, serviceName + SIGNING + P12),
                keystorePasswordSigning);
            KeyStorePersister.saveKeyStore(keyStoreEncryption, Paths.get(output, serviceName + ENCRYPTION + P12),
                keystorePasswordEncryption);

            CertificatePersister.saveCertificate(serviceSignerCertificate,
                Paths.get(output, serviceName + SIGNING + Constants.PEM));

            CertificatePersister.saveCertificate(serviceEncryptionCertificate,
                Paths.get(output, serviceName + ENCRYPTION + Constants.PEM));

        }

        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(x.getKey() + Constants.EQUAL + String.valueOf(x.getValue())));
    }

    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 GenerateLoggingKeysCommandException("An error occurred while loading the certificate properties",
					e);
        }
        return subjectPropertiesSigner;
    }

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

        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 = issuerCertificate.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 GenerateLoggingKeysCommandException("The validity period of the certificate is empty");
		}

		if (Date.from(notBefore.toInstant()).before(issuerCertificate.getNotBefore())) {
			throw new GenerateLoggingKeysCommandException(
					"The tenant \"start\" time should be strictly after the root \"start\" time");
		}
		if (Date.from(notAfter.toInstant()).after(issuerCertificate.getNotAfter())) {
			throw new GenerateLoggingKeysCommandException(
					"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 GenerateLoggingKeysCommandException("An error occurred while creating the certificate of service " + serviceName, e);
        }

        return platformRootCACert;
    }

    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 GenerateLoggingKeysCommandException("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 GenerateLoggingKeysCommandException("The certificate chain could not be retrieved", e);
        }

        return certificateChain[0];
    }

    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 GenerateLoggingKeysCommandException("An error occurred while loading the services file", e);
        }

        return servicesInput;
    }
}
