/*
 * 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.ei.ui.ws.rs.platformdata;

import com.scytl.cryptolib.scytl.keystore.cryptoapi.CryptoAPIScytlKeyStore;
import com.scytl.products.ov.commons.beans.domain.model.tenant.TenantInstallationData;
import com.scytl.products.ov.commons.test.CryptoUtils;
import com.scytl.products.ov.ei.ui.ws.rs.config.SystemPropertiesLoader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Base64;
import java.util.Date;
import java.util.Properties;

import javax.security.auth.x500.X500Principal;

import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest;
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.api.stores.bean.KeyStoreType;
import com.scytl.cryptolib.certificates.bean.X509DistinguishedName;
import com.scytl.cryptolib.certificates.cryptoapi.CryptoAPIX509Certificate;
import com.scytl.cryptolib.certificates.service.CertificatesService;
import com.scytl.cryptolib.primitives.primes.utils.PemUtils;
import com.scytl.cryptolib.scytl.keystore.factory.CryptoScytlKeyStoreWithPBKDF;
import com.scytl.cryptolib.scytl.keystore.service.ScytlKeyStoreService;
import com.scytl.cryptolib.stores.service.StoresService;
import com.scytl.products.ov.commons.beans.domain.model.platform.PlatformInstallationData;
import com.scytl.products.ov.commons.beans.utils.PasswordEncrypter;
import com.scytl.products.ov.commons.crypto.configuration.CertificateDataBuilder;
import com.scytl.products.ov.commons.crypto.configuration.CertificateParameters;
import com.scytl.products.ov.commons.crypto.configuration.CredentialProperties;
import com.scytl.products.ov.commons.crypto.configuration.X509CertificateGenerator;

public class TestPlatformDataGenerator {

    private static final String SIGNING_KEYSTORE_NAME = "test_log_signing.p12";

    private static final String ENCRYPTION_KEYSTORE_NAME = "test_log_encryption.p12";

    private static final String OUTPUT_FOLDER_NAME = "target";

    private KeyPair tenantKeyPair;

    private PrivateKey platformRootPrivateKey;

    private CryptoAPIX509Certificate platformRootCACert;

    private CryptoAPIX509Certificate tenantCert;

    private CredentialPropertiesProvider credPropsProvider;

    private CryptoAPIScytlKeyStore tenantKeystore;

    private String platformName;

    private String tenantId;

    private char[] tenantPassword;

    private String keystorePassphrase;

    private PlatformInstallationData platformInstallationData;

    private boolean dataGenerated;

    private TenantInstallationData tenantInstallationData;

    public TestPlatformDataGenerator(String tenantId, String platformName, String tenantPasswordString) {
        this.platformName = platformName;
        this.tenantId = tenantId;
        this.tenantPassword = tenantPasswordString.toCharArray();
        this.keystorePassphrase = tenantPasswordString;

    }

    /**
     * Generates the platform certificates for the corresponding context.
     */
    public void generate()
            throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException,
            GeneralCryptoLibException, OperatorCreationException {
        createPlatformRootData();
        createTenantKeystore();
        dataGenerated = true;
    }

    /**
     * Gets the tenant keystore encoded B64.
     *
     * @return the encoded tenant keystore
     * @throws GeneralCryptoLibException
     *             if something fails when exporting the keystore
     */
    public String getTenantKeystoreAsB64String() throws GeneralCryptoLibException {
        if (!dataGenerated) {
            throw new IllegalStateException("Tenant data not generated. Call generate() method first");
        }
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        tenantKeystore.store(out, tenantPassword);
        byte[] keystoreBytes = out.toByteArray();
        return Base64.getEncoder().encodeToString(keystoreBytes);
    }

    /**
     * Gets the tenant keystore in JSON format.
     *
     * @return the tenant keystore as JSON.
     * @throws GeneralCryptoLibException
     *             if something fails when exporting the keystore.
     */
    public String getTenantKeystoreAsJSON() throws GeneralCryptoLibException {
        if (!dataGenerated) {
            throw new IllegalStateException("Tenant data not generated. Call generate() method first");
        }
        final ScytlKeyStoreService storesService = new ScytlKeyStoreService();
        final CryptoScytlKeyStoreWithPBKDF keyStore = (CryptoScytlKeyStoreWithPBKDF) storesService.createKeyStore();
        final Certificate[] chainEncryption = {tenantCert.getCertificate(), platformRootCACert.getCertificate() };
        keyStore.setPrivateKeyEntry("privatekey", tenantKeyPair.getPrivate(), tenantPassword, chainEncryption);
        byte[] keyStoreJSONBytes = keyStore.toJSON(tenantPassword).getBytes(StandardCharsets.UTF_8);

        return Base64.getEncoder().encodeToString(keyStoreJSONBytes);
    }

    /**
     * Encrypts the tenant password with the tenant public key.
     * 
     * @return the encrypted password
     * @throws GeneralCryptoLibException
     *             if something fails when encrypting.
     */
    public String getEncryptedTenantKeystorePassword() throws GeneralCryptoLibException {
        if (!dataGenerated) {
            throw new IllegalStateException("Tenant data not generated. Call generate() method first");
        }
        PasswordEncrypter passwordEncrypter = new PasswordEncrypter(CryptoUtils.getAsymmetricService());
        String publicKey = PemUtils.publicKeyToPem(tenantKeyPair.getPublic());
        String encryptedPassword =
            passwordEncrypter.encryptPasswordIfEncryptionKeyAvailable(keystorePassphrase, publicKey);
        return encryptedPassword;
    }

    public PlatformInstallationData getPlatformInstallationData() {
        return platformInstallationData;
    }

    public TenantInstallationData getTenantInstallationData() {
        return tenantInstallationData;
    }

    private void createPlatformRootData()
            throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {

        KeyPair keyPairForSign = generateKeyPairOfType(CertificateParameters.Type.SIGN);

        credPropsProvider = new CredentialPropertiesProvider();

        final CredentialProperties platformRoomCredentialProperties =
            credPropsProvider.getPlatformRootCredentialPropertiesFromClassPath();

        platformRootCACert = generateRootCertificate(keyPairForSign, platformRoomCredentialProperties, platformName);

        String platformRootCACertPem = new String(platformRootCACert.getPemEncoded(), StandardCharsets.UTF_8);

        platformRootPrivateKey = keyPairForSign.getPrivate();

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

        CredentialProperties signingCredentialPropertiesFromClassPath =
            credPropsProvider.getServicesLoggingSigningCredentialPropertiesFromClassPath();
        CryptoAPIX509Certificate serviceSignerCertificate = generateCertificate(keyPairForSigning,
            platformRootPrivateKey, keystorePassphrase, getPropertiesFrom(signingCredentialPropertiesFromClassPath),
            signingCredentialPropertiesFromClassPath.getCredentialType(), platformRootCACert);

        CredentialProperties encryptionCredentialPropertiesFromClassPath =
            credPropsProvider.getServicesLoggingEncryptionCredentialPropertiesFromClassPath();
        CryptoAPIX509Certificate serviceEncryptionCertificate = generateCertificate(keyPairForEncryption,
            platformRootPrivateKey, keystorePassphrase, getPropertiesFrom(encryptionCredentialPropertiesFromClassPath),
            encryptionCredentialPropertiesFromClassPath.getCredentialType(), platformRootCACert);

        final KeyStore keyStoreSigning = createP12(keyPairForSigning, signingCredentialPropertiesFromClassPath,
            serviceSignerCertificate, keystorePassphrase, tenantPassword, platformRootCACert.getCertificate());
        final KeyStore keyStoreEncryption = createP12(keyPairForEncryption, encryptionCredentialPropertiesFromClassPath,
            serviceEncryptionCertificate, keystorePassphrase, tenantPassword, platformRootCACert.getCertificate());

        Path signingKeystorePath = Paths.get(OUTPUT_FOLDER_NAME, SIGNING_KEYSTORE_NAME);
        saveKeyStore(keyStoreSigning, signingKeystorePath, tenantPassword);
        Path encryptionKeystorePath = Paths.get(OUTPUT_FOLDER_NAME, ENCRYPTION_KEYSTORE_NAME);
        saveKeyStore(keyStoreEncryption, encryptionKeystorePath, tenantPassword);

        File file = Paths.get(SystemPropertiesLoader.ABSOLUTEPATH).resolve("logging_" + platformName + ".properties").toFile();
        Properties p = new Properties();
        p.setProperty(platformName + "_log_encryption", keystorePassphrase);
        p.setProperty(platformName + "_log_signing", keystorePassphrase);
        try (FileOutputStream fos = new FileOutputStream(file)) {
            p.store(fos, "");
        }

        File tenantFile =
            Paths.get(SystemPropertiesLoader.ABSOLUTEPATH).resolve("tenant_" + platformName + "_" + tenantId + ".properties").toFile();

        p.setProperty(tenantId + "_" + platformName, keystorePassphrase);
        try (FileOutputStream fos = new FileOutputStream(tenantFile)) {
            p.store(fos, "");
        }
        platformInstallationData = new PlatformInstallationData();
        platformInstallationData.setPlatformRootCaPEM(platformRootCACertPem);
        platformInstallationData.setLoggingEncryptionKeystoreBase64(getFileContentsBase64(encryptionKeystorePath));
        platformInstallationData.setLoggingSigningKeystoreBase64(getFileContentsBase64(signingKeystorePath));
    }

    private void createTenantKeystore()
            throws OperatorCreationException, IOException, CertificateException, NoSuchAlgorithmException,
            KeyStoreException, GeneralCryptoLibException {

        // load credentialproperties
        final CredentialProperties tenantCredentialProperties =
            credPropsProvider.getTenantCredentialPropertiesFromClassPath();

        // Generate and sign CSR (#PKCS10)
        tenantKeyPair = generateKeyPairOfType(CertificateParameters.Type.ENCRYPTION);
        JcaPKCS10CertificationRequestBuilder p10Builder = new JcaPKCS10CertificationRequestBuilder(
            new X500Principal("CN=Tenant " + tenantId + " CA, OU=tenant" + tenantId + ", O=test, C=ES"),
            tenantKeyPair.getPublic());
        JcaContentSignerBuilder csBuilder = new JcaContentSignerBuilder("SHA256withRSA");
        ContentSigner signer = csBuilder.build(tenantKeyPair.getPrivate());
        JcaPKCS10CertificationRequest csr = new JcaPKCS10CertificationRequest(p10Builder.build(signer));

        // Issue tenant certificate
        CertificateRequestSigner certificateRequestSigner = new CertificateRequestSigner();
        CSRSigningInputProperties csrSingingPropertyInputs =
            certificateRequestSigner.getCsrSingingInputProperties(tenantCredentialProperties);
        tenantCert = certificateRequestSigner.signCSR(platformRootCACert.getCertificate(), platformRootPrivateKey, csr,
            csrSingingPropertyInputs);

        // Build tenant keystore
        final ScytlKeyStoreService storesService = new ScytlKeyStoreService();
        tenantKeystore = storesService.createKeyStore();
        final Certificate[] chainEncryption = {tenantCert.getCertificate(), platformRootCACert.getCertificate() };
        String privateKeyAlias =
            tenantCredentialProperties.getAlias().get(CertificatePropertiesConstants.PRIVATE_KEY_ALIAS);

        tenantKeystore.setPrivateKeyEntry(privateKeyAlias, tenantKeyPair.getPrivate(), tenantPassword, chainEncryption);

        tenantInstallationData = new TenantInstallationData();
        tenantInstallationData.setEncodedData(new String(tenantCert.getPemEncoded(), StandardCharsets.UTF_8));
    }

    private void saveKeyStore(final KeyStore keyStore, final Path path, final char[] keyStorePassword)
            throws IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException {

        try (final OutputStream out = Files.newOutputStream(path)) {
            keyStore.store(out, keyStorePassword);
        }
    }

    private String getFileContentsBase64(Path keystorePath) {

        byte[] keystoreBytes;
        try {
            keystoreBytes = Files.readAllBytes(keystorePath);
        } catch (IOException e) {
            throw new RuntimeException("Error while trying to obtain a logging keystore for the service", e);
        }

        return new String(Base64.getEncoder().encode(keystoreBytes));
    }

    private KeyStore createP12(final KeyPair keyPair, final CredentialProperties credentialProperties,
            final CryptoAPIX509Certificate serviceCert, final String subjectName, final char[] password,
            final X509Certificate issuerCA) {

        KeyStore keyStore;
        try {
            StoresService storesService = new StoresService();
            keyStore = storesService.createKeyStore(KeyStoreType.PKCS12);
        } catch (GeneralCryptoLibException e) {
            throw new RuntimeException("An error occurred while preparing the p12 of " + subjectName, e);
        }

        final Certificate[] chain = {serviceCert.getCertificate(), issuerCA };

        try {
            keyStore.setKeyEntry(credentialProperties.getAlias().get(CertificatePropertiesConstants.PRIVATE_KEY_ALIAS),
                keyPair.getPrivate(), password, chain);
        } catch (KeyStoreException e) {
            throw new RuntimeException("An error occurred while creating the p12 of " + subjectName, e);
        }

        return keyStore;
    }

    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 RuntimeException("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 = createCertificateGenerator();

        final CertificateParameters certificateParameters = new CertificateParameters();

        certificateParameters.setType(credentialType);

        certificateParameters
            .setUserSubjectCn(properties.getProperty(CertificatePropertiesConstants.SUBJECT_COMMON_NAME_PROPERTY_NAME)
                .replace("${serviceName}", 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 RuntimeException("The validity period of the certificate is empty");
        }

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

        return platformRootCACert;
    }

    private KeyPair generateKeyPairOfType(CertificateParameters.Type certificateType) {

        switch (certificateType) {
        case SIGN:
            return CryptoUtils.getKeyPairForSigning();
        case ENCRYPTION:
            return CryptoUtils.getKeyPairForEncryption();
        default:
            throw new RuntimeException("The provided certificate type is invalid");
        }
    }

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

        X509CertificateGenerator certificateGenerator = createCertificateGenerator();

        final Properties props = readProperties(credentialProperties);

        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 RuntimeException("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 (GeneralCryptoLibException e) {
            throw new RuntimeException("An error occurred while creating the platform root certificate", e);
        }

        return platformRootCACert;
    }

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

    private X509CertificateGenerator createCertificateGenerator() {
        CertificatesService certificatesService;
        try {
            certificatesService = new CertificatesService();
        } catch (GeneralCryptoLibException e) {
            throw new IllegalStateException("Failed to create certificate service.", e);
        }
        CertificateDataBuilder certificateDataBuilder = new CertificateDataBuilder();
        return new X509CertificateGenerator(certificatesService, certificateDataBuilder);
    }
}
