/*
 * 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.cr.ui.ws.rs.operation.certificate;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URISyntaxException;
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.asymmetric.service.AsymmetricService;
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.stores.service.StoresService;
import com.scytl.products.ov.commons.beans.domain.model.platform.PlatformInstallationData;
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;
import com.scytl.products.ov.cr.ui.ws.rs.operation.SystemPropertiesLoader;

public class CryptoCreator {

    private static final String KEYSTORE_PASSPHRASE = "test";

    private PrivateKey platformRootPrivateKey;

    private CryptoAPIX509Certificate platformRootCACert;

    private final CredentialPropertiesProvider credPropsProvider = new CredentialPropertiesProvider();

    public String createTenantInstallationData(int tenantId)
            throws IOException, URISyntaxException, OperatorCreationException {
        final CredentialProperties tenantCredentialProperties =
            credPropsProvider.getTenantCredentialPropertiesFromClassPath();
        KeyPair pair = generateKeyPairOfType(CertificateParameters.Type.SIGN);
        JcaPKCS10CertificationRequestBuilder p10Builder = new JcaPKCS10CertificationRequestBuilder(
            new X500Principal("CN=Tenant " + tenantId + " CA, OU=tenant" + tenantId + ", O=bddtest, C=ES"),
            pair.getPublic());
        JcaContentSignerBuilder csBuilder = new JcaContentSignerBuilder("SHA256withRSA");
        ContentSigner signer = csBuilder.build(pair.getPrivate());
        JcaPKCS10CertificationRequest csr = new JcaPKCS10CertificationRequest(p10Builder.build(signer));

        CertificateRequestSigner certificateRequestSigner = new CertificateRequestSigner();
        CSRSigningInputProperties csrSingingPropertyInputs =
            certificateRequestSigner.getCsrSingingInputProperties(tenantCredentialProperties);
        CryptoAPIX509Certificate tenantCert = certificateRequestSigner.signCSR(platformRootCACert.getCertificate(),
            platformRootPrivateKey, csr, csrSingingPropertyInputs);
        return new String(tenantCert.getPemEncoded(), StandardCharsets.UTF_8);
    }

    public String createElectionCertificate(String certName) throws GeneralCryptoLibException {

        KeyPair electionCertKP = generateKeyPairOfType(CertificateParameters.Type.SIGN);
        CredentialProperties signingCredentialPropertiesFromClassPath =
            credPropsProvider.getServicesLoggingSigningCredentialPropertiesFromClassPath();
        CryptoAPIX509Certificate generateCertificate = generateCertificate(electionCertKP, platformRootPrivateKey,
            certName, getPropertiesFrom(signingCredentialPropertiesFromClassPath), CertificateParameters.Type.SIGN,
            platformRootCACert);
        return PemUtils.certificateToPem(generateCertificate.getCertificate());
    }

    public PlatformInstallationData createPlatformInstallationData()
            throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
        KeyPair keyPairForSign = generateKeyPairOfType(CertificateParameters.Type.SIGN);

        final CredentialProperties platformRoomCredentialProperties =
            credPropsProvider.getPlatformRootCredentialPropertiesFromClassPath();

        platformRootCACert = generateRootCertificate(keyPairForSign, platformRoomCredentialProperties, "CR");

        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, KEYSTORE_PASSPHRASE, getPropertiesFrom(signingCredentialPropertiesFromClassPath),
            signingCredentialPropertiesFromClassPath.getCredentialType(), platformRootCACert);

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

        char[] keystorePasswordSigning = KEYSTORE_PASSPHRASE.toCharArray();
        char[] keystorePasswordEncryption = KEYSTORE_PASSPHRASE.toCharArray();

        final KeyStore keyStoreSigning =
            createP12(keyPairForSigning, signingCredentialPropertiesFromClassPath, serviceSignerCertificate,
                KEYSTORE_PASSPHRASE, keystorePasswordSigning, platformRootCACert.getCertificate());
        final KeyStore keyStoreEncryption =
            createP12(keyPairForEncryption, encryptionCredentialPropertiesFromClassPath, serviceEncryptionCertificate,
                KEYSTORE_PASSPHRASE, keystorePasswordEncryption, platformRootCACert.getCertificate());

        Path signingKeystorePath = Paths.get("target", "test_log_signing.p12");
        saveKeyStore(keyStoreSigning, signingKeystorePath, keystorePasswordSigning);
        Path encryptionKeystorePath = Paths.get("target", "test_log_encryption.p12");
        saveKeyStore(keyStoreEncryption, encryptionKeystorePath, keystorePasswordEncryption);

        File file = Paths.get(SystemPropertiesLoader.ABSOLUTEPATH).resolve("logging_CR.properties").toFile();
        Properties p = new Properties();
        p.setProperty("CR_log_encryption", KEYSTORE_PASSPHRASE);
        p.setProperty("CR_log_signing", KEYSTORE_PASSPHRASE);
        try (FileOutputStream fos = new FileOutputStream(file)) {
            p.store(fos, "");
        }

        PlatformInstallationData pid = new PlatformInstallationData();
        pid.setPlatformRootCaPEM(platformRootCACertPem);
        pid.setLoggingEncryptionKeystoreBase64(getFileContentsBase64(encryptionKeystorePath));
        pid.setLoggingSigningKeystoreBase64(getFileContentsBase64(signingKeystorePath));
        return pid;
    }

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

        String keystoreBase64;
        try {
            keystoreBase64 = getFileBytesAsBase64(keystorePath);
        } catch (IOException e) {
            throw new RuntimeException("Error while trying to obtain a logging keystore for the service", e);
        }

        return keystoreBase64;
    }

    private String getFileBytesAsBase64(final Path pathOfFile) throws IOException {

        byte[] keyStoreBytes = Files.readAllBytes(pathOfFile);

        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) {
        AsymmetricService asymmetricService;
        try {
            asymmetricService = new AsymmetricService();
        } catch (GeneralCryptoLibException e) {
            throw new IllegalStateException("Failed to create asymmetric service.", e);
        }
        switch (certificateType) {
        case SIGN:
            return asymmetricService.getKeyPairForSigning();
        case ENCRYPTION:
            return asymmetricService.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);
    }

}
