/*
 * 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.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
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.certificates.cryptoapi.CryptoAPIX509Certificate;
import com.scytl.cryptolib.primitives.primes.utils.PemUtils;
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.products.ov.config.commons.exception.GenerateCommandException;
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.KeyStorePersister;
import com.scytl.products.ov.utils.PasswordCleaner;
import com.scytl.products.ov.utils.PasswordReaderUtils;

public class GenerateCommand implements Command {

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

    private static final String ROOT_CA = "RootCA";

    private final PasswordCleaner passwordCleaner = new PasswordCleaner();

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

        final String outputPathString = params2values.get(ActionParams.OUTPUT_FOLDER.getName());
        String platformName = params2values.get(ActionParams.PLATFORM_NAME.getName());

        platformName = joinIfComposedName(platformName);

        LOG.info("Generating keys...");

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

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

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

        CryptoAPIX509Certificate platformRootCACert =
            generateRootCertificate(keyPairForSigning, platformRoomCredentialProperties, platformName);

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

        char[] password = PasswordReaderUtils.readPasswordFromConsoleAndConfirm();

        CryptoAPIScytlKeyStore platformRootKeyStore =
            createKeyStore(keyPairForSigning, platformRoomCredentialProperties, platformRootCACert, password);

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

        Path outputPath = Paths.get(outputPathString);
        FileUtils.createOutputDirectory(outputPath);

        CertificatePersister.saveCertificate(platformRootCACert,
            Paths.get(outputPathString, platformName + ROOT_CA + Constants.PEM));
        KeyStorePersister.saveKeyStore(platformRootKeyStore,
            Paths.get(outputPathString, platformName + ROOT_CA + Constants.SKS), password);

        LOG.info("Deleting password from memory...");

        passwordCleaner.clean(password);

        LOG.info("Process finished successfully");
    }

    private String joinIfComposedName(String platformName) {
        return platformName.replace(" ", "_");
    }

    private CryptoAPIScytlKeyStore createKeyStore(final KeyPair keyPairForSigning,
            final CredentialProperties credentialProperties, final CryptoAPIX509Certificate platformRootCACert,
            final char[] password) {

        CryptoScytlKeyStoreWithPBKDF platformRootKeyStore = null;
        try {
            final ScytlKeyStoreService storesService = new ScytlKeyStoreService();
            final CryptoScytlKeyStoreWithPBKDF keyStore = (CryptoScytlKeyStoreWithPBKDF) storesService.createKeyStore();
            platformRootKeyStore = setPrivateKeyToKeystore(keyStore,
                credentialProperties.getAlias().get(CertificatePropertiesConstants.PRIVATE_KEY_ALIAS),
                keyPairForSigning.getPrivate(), password, platformRootCACert);
        } catch (GeneralCryptoLibException e) {
            throw new GenerateCommandException("An error occurred while creating the platform root keystore", e);
        }

        return platformRootKeyStore;
    }

    private CryptoAPIX509Certificate generateRootCertificate(final KeyPair keyPairForSigning,
            final CredentialProperties credentialProperties, final String platformName) {
        final PublicKey publicKey = keyPairForSigning.getPublic();
        PrivateKey parentPrivateKey = keyPairForSigning.getPrivate();

        X509CertificateGenerator certificateGenerator = CryptoUtils.createCertificateGenerator();

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

        final CertificateParameters certificateParameters = new CertificateParameters();

        certificateParameters.setType(credentialProperties.getCredentialType());

        certificateParameters
            .setUserSubjectCn(props.getProperty(CertificatePropertiesConstants.SUBJECT_COMMON_NAME_PROPERTY_NAME)
                .replace("${platformName}", platformName));
        certificateParameters.setUserSubjectOrgUnit(
            props.getProperty(CertificatePropertiesConstants.SUBJECT_ORGANIZATIONAL_UNIT_PROPERTY_NAME));
        certificateParameters
            .setUserSubjectOrg(props.getProperty(CertificatePropertiesConstants.SUBJECT_ORGANIZATION_PROPERTY_NAME));
        certificateParameters
            .setUserSubjectCountry(props.getProperty(CertificatePropertiesConstants.SUBJECT_COUNTRY_PROPERTY_NAME));

        certificateParameters
            .setUserIssuerCn(props.getProperty(CertificatePropertiesConstants.SUBJECT_COMMON_NAME_PROPERTY_NAME)
                .replace("${platformName}", platformName));
        certificateParameters.setUserIssuerOrgUnit(
            props.getProperty(CertificatePropertiesConstants.SUBJECT_ORGANIZATIONAL_UNIT_PROPERTY_NAME));
        certificateParameters
            .setUserIssuerOrg(props.getProperty(CertificatePropertiesConstants.SUBJECT_ORGANIZATION_PROPERTY_NAME));
        certificateParameters
            .setUserIssuerCountry(props.getProperty(CertificatePropertiesConstants.SUBJECT_COUNTRY_PROPERTY_NAME));

        String start = props.getProperty(CertificatePropertiesConstants.START_DATE);
        String end = props.getProperty(CertificatePropertiesConstants.END_DATE);
        ZonedDateTime notBefore = ZonedDateTime.ofInstant(Instant.parse(start), ZoneOffset.UTC);
        ZonedDateTime notAfter = ZonedDateTime.ofInstant(Instant.parse(end), ZoneOffset.UTC);
        if (!notBefore.isBefore(notAfter)) {
            throw new GenerateCommandException("The given \"start\" date should be strictly before than the \"end\" date");
        }
        certificateParameters.setUserNotBefore(notBefore);
        certificateParameters.setUserNotAfter(notAfter);

        CryptoAPIX509Certificate platformRootCACert = null;
        try {
            platformRootCACert = certificateGenerator.generate(certificateParameters, publicKey, parentPrivateKey);
        } catch (ConfigurationException | GeneralCryptoLibException e) {
            throw new GenerateCommandException("An error occurred while creating the platform root certificate", e);
        }

        return platformRootCACert;
    }

    private CryptoScytlKeyStoreWithPBKDF setPrivateKeyToKeystore(final CryptoScytlKeyStoreWithPBKDF keystore,
            final String alias, final PrivateKey privateKey, final char[] keyStorePassword,
            final CryptoAPIX509Certificate... certs) throws GeneralCryptoLibException {

        final Certificate[] chain = new Certificate[certs.length];

        for (int i = 0; i < certs.length; i++) {
            chain[i] = PemUtils.certificateFromPem(new String(certs[i].getPemEncoded()));
        }

        keystore.setPrivateKeyEntry(alias, privateKey, keyStorePassword, chain);

        return keystore;
    }
}
