/*
 * 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.commons.test;

import com.scytl.cryptolib.api.primitives.PrimitivesServiceAPI;
import com.scytl.cryptolib.derivation.cryptoapi.CryptoAPIKDFDeriver;
import com.scytl.cryptolib.primitives.service.PrimitivesServiceFactoryHelper;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.time.ZonedDateTime;

import org.apache.commons.pool2.impl.GenericObjectPoolConfig;

import com.scytl.cryptolib.api.asymmetric.AsymmetricServiceAPI;
import com.scytl.cryptolib.api.certificates.CertificatesServiceAPI;
import com.scytl.cryptolib.api.exceptions.CryptoLibException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.api.services.ServiceFactory;
import com.scytl.cryptolib.asymmetric.service.AsymmetricServiceFactoryHelper;
import com.scytl.cryptolib.certificates.cryptoapi.CryptoAPIX509Certificate;
import com.scytl.cryptolib.certificates.service.CertificatesServiceFactoryHelper;
import com.scytl.products.ov.commons.beans.validation.CryptographicOperationException;
import com.scytl.products.ov.commons.crypto.InputDataFormatterService;
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.X509CertificateGenerator;
import com.scytl.products.ov.commons.crypto.impl.InputDataFormatterStringServiceImpl;

/**
 * Mix of cryptographic operations useful for tests
 */
public class CryptoUtils {

    private static final CertificateDataBuilder certificateDataBuilder = new CertificateDataBuilder();

    private static final InputDataFormatterService inputDataFormatterService =
        new InputDataFormatterStringServiceImpl();

    private static final String COUNTRY = "ES";

    private static final String SCYTL = "Scytl";

    private static AsymmetricServiceAPI asymmetricService;

    private static CertificatesServiceAPI certificatesService;

    private static PrimitivesServiceAPI primitivesService;

    private static X509CertificateGenerator certificateGenerator;
    
    private CryptoUtils() {
    }
    
    static {
        GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
        genericObjectPoolConfig.setMaxTotal(50);
        genericObjectPoolConfig.setMaxIdle(50);
        ServiceFactory<CertificatesServiceAPI> certificatesServiceAPIServiceFactory =
            CertificatesServiceFactoryHelper.getFactoryOfThreadSafeServices(genericObjectPoolConfig);
        ServiceFactory<AsymmetricServiceAPI> asymmetricServiceAPIServiceFactory =
            AsymmetricServiceFactoryHelper.getFactoryOfThreadSafeServices(genericObjectPoolConfig);
        ServiceFactory<PrimitivesServiceAPI> primitivesServiceAPIServiceFactory =
                PrimitivesServiceFactoryHelper.getFactoryOfThreadSafeServices(genericObjectPoolConfig);
        try {
            certificatesService = certificatesServiceAPIServiceFactory.create();
        } catch (GeneralCryptoLibException e) {
            throw new CryptoLibException("Exception while trying to create CertificatesService", e);
        }
        try {
            asymmetricService = asymmetricServiceAPIServiceFactory.create();
        } catch (GeneralCryptoLibException e) {
            throw new CryptoLibException("Exception while trying to create AsymmetricService", e);
        }
        try {
            primitivesService = primitivesServiceAPIServiceFactory.create();
        } catch (GeneralCryptoLibException e) {
            throw new CryptoLibException("Exception while trying to create PrimitivesService", e);
        }

        certificateGenerator = new X509CertificateGenerator(certificatesService, certificateDataBuilder);
    }

    /**
     * Class for storing generated certificates
     */
    public static class CryptoMaterialContainer {
        
    	private KeyPair rootCaKeyPair;
        private KeyPair subjectKeyPair;
        private String rootCACertificate;
        private String subjectCertificate;

        CryptoMaterialContainer(KeyPair rootCaKeyPair, KeyPair subjectKeyPair, String rootCACertificate,
                String subjectCertificate) {
            this.rootCaKeyPair = rootCaKeyPair;
            this.subjectKeyPair = subjectKeyPair;
            this.rootCACertificate = rootCACertificate;
            this.subjectCertificate = subjectCertificate;
        }
        
        public KeyPair getRootCaKeyPair() {
            return rootCaKeyPair;
        }

        public KeyPair getSubjectKeyPair() {
            return subjectKeyPair;
        }

        public String getRootCACertificate() {
            return rootCACertificate;
        }

        public String getSubjectCertificate() {
            return subjectCertificate;
        }
    }

    /**
     * Obtains a key pair for signing purposes
     *
     * @return The keypair for signing purposes
     */
    public static KeyPair getKeyPairForSigning() {
        return asymmetricService.getKeyPairForSigning();
    }

    /**
     * Obtains a key pair for encryption purposes
     *
     * @return The keypair for encryption purposes
     */
    public static KeyPair getKeyPairForEncryption() {
        return asymmetricService.getKeyPairForEncryption();
    }

    /**
     * Creates an X509 certificate with provided params
     *
     * @param commonName
     * @param certificateType
     * @param keyPair
     * @return The generated certificate
     * @throws IOException
     * @throws GeneralCryptoLibException
     */
    public static CryptoAPIX509Certificate createCryptoAPIx509Certificate(final String commonName,
            CertificateParameters.Type certificateType, KeyPair keyPair) throws IOException, GeneralCryptoLibException {
        final CertificateParameters certificateParameters = createCertificateParameters(commonName, certificateType);
        return certificateGenerator.generate(certificateParameters, keyPair.getPublic(), keyPair.getPrivate());
    }

    /**
     * Generates a root CA certificate and keypair and issues a certificate. All
     * the content is stored in a CryptoMaterialContainer instance.
     *
     * @param commonName
     * @return The cryptoMaterialContainer instance with the generated
     *         certificates and key pairs
     * @throws IOException
     * @throws GeneralCryptoLibException
     */
    public static CryptoMaterialContainer createRootAndChildCertificates(final String commonName)
            throws IOException, GeneralCryptoLibException {
        final KeyPair rootCAKeyPair = getKeyPairForSigning();
        final KeyPair subjectKeyPair = getKeyPairForSigning();
        final CertificateParameters certificateParametersCA =
            createCertificateParameters(commonName, CertificateParameters.Type.ROOT);
        final CertificateParameters certificateParameters =
            createCertificateParameters(commonName, CertificateParameters.Type.SIGN);
        final CryptoAPIX509Certificate rootCACertificate = certificateGenerator.generate(certificateParametersCA,
            rootCAKeyPair.getPublic(), rootCAKeyPair.getPrivate());
        final CryptoAPIX509Certificate subjectCertificate = certificateGenerator.generate(certificateParameters,
            subjectKeyPair.getPublic(), rootCAKeyPair.getPrivate());
        final String rootCACertificatePem = new String(rootCACertificate.getPemEncoded(), StandardCharsets.UTF_8);
        final String subjectCertificatePem = new String(subjectCertificate.getPemEncoded(), StandardCharsets.UTF_8);
        return new CryptoMaterialContainer(rootCAKeyPair, subjectKeyPair, rootCACertificatePem, subjectCertificatePem);
    }

    /**
     * Concatenates and signs the data with provided private key
     *
     * @param privateKey
     * @param data
     * @return The signed data
     * @throws CryptographicOperationException
     */
    public static byte[] sign(PrivateKey privateKey, String... data) throws CryptographicOperationException {
        byte[] signatureInput = inputDataFormatterService.concatenate(data);
        try {
            return asymmetricService.sign(privateKey, signatureInput);
        } catch (GeneralCryptoLibException e) {
            throw new CryptographicOperationException("Error performing a signature:", e);
        }
    }

    public static CryptoAPIKDFDeriver getKDFDeriver() throws GeneralCryptoLibException {
        return primitivesService.getKDFDeriver();
    }

    public static AsymmetricServiceAPI getAsymmetricService() {
        return asymmetricService;
    }

    private static CertificateParameters createCertificateParameters(String commonName,
                                                                     CertificateParameters.Type certificateType) {
        ZonedDateTime now = ZonedDateTime.now();
        CertificateParameters params = new CertificateParameters();
        params.setType(certificateType);
        params.setUserIssuerCn(commonName);
        params.setUserIssuerCountry(COUNTRY);
        params.setUserIssuerOrg(SCYTL);
        params.setUserIssuerOrgUnit(SCYTL);
        params.setUserNotAfter(now.plusYears(1));
        params.setUserNotBefore(now);
        params.setUserSubjectCn(commonName);
        params.setUserSubjectCountry(COUNTRY);
        params.setUserSubjectOrg(SCYTL);
        params.setUserSubjectOrgUnit(SCYTL);
        return params;
    }
}
