/*
 * 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.tenant.service;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Properties;

import javax.security.auth.x500.X500Principal;

import org.apache.commons.configuration.ConfigurationException;
import org.bouncycastle.operator.ContentVerifierProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.bouncycastle.pkcs.PKCSException;
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.certificates.cryptoapi.CryptoAPIX509Certificate;
import com.scytl.cryptolib.certificates.service.CertificatesService;
import com.scytl.products.ov.config.tenant.CertificatePropertiesConstants;
import com.scytl.products.ov.config.tenant.exception.CertificateServiceException;
import com.scytl.products.ov.csr.CSRGenerator;
import com.scytl.products.ov.datapacks.beans.CertificateParameters;
import com.scytl.products.ov.datapacks.beans.CredentialProperties;
import com.scytl.products.ov.datapacks.generators.CertificateDataBuilder;
import com.scytl.products.ov.datapacks.generators.X509CertificateGenerator;
import com.scytl.products.ov.utils.PemUtils;

@Service
public class CertificateService {

    @Autowired
    KeyPairService keypairService;

    public CryptoAPIX509Certificate generateCertificate(final KeyPair keyPairForSigning,
            final CredentialProperties credentialProperties, final String tenantName) {
        final PublicKey publicKey = keyPairForSigning.getPublic();
        PrivateKey parentPrivateKey = keyPairForSigning.getPrivate();

        X509CertificateGenerator certificateGenerator = createCertificateGenerator();

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

        final CertificateParameters certificateParameters = new CertificateParameters();

        certificateParameters.setType(credentialProperties.getCredentialType());

        String commonName = props.getProperty(CertificatePropertiesConstants.SUBJECT_COMMON_NAME_PROPERTY_NAME);
        commonName = commonName.replace("${tenant}", tenantName) + " Temp";
        certificateParameters.setUserSubjectCn(commonName);
        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(commonName);
        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);

        compareCertificateDates(notBefore, notAfter);

        certificateParameters.setUserNotBefore(notBefore);
        certificateParameters.setUserNotAfter(notAfter);

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

        return platformRootCACert;
    }

    private X509CertificateGenerator createCertificateGenerator() {
        CertificatesService certificatesService;
        try {
            certificatesService = new CertificatesService();
        } catch (GeneralCryptoLibException e) {
			throw new CertificateServiceException(
					"Exception while trying to create a CertificatesService: " + e.getMessage(), e);
        }
        CertificateDataBuilder certificateDataBuilder = new CertificateDataBuilder();

        return new X509CertificateGenerator(certificatesService, certificateDataBuilder);
    }

    /**
     * Stores the content of CSR in the given path
     * 
     * @param csr
     *            - certificate to be stored
     * @param path
     *            - path where the certificate will be stored
     */
    public void saveCSR(final PKCS10CertificationRequest csr, final Path path) {

        try (final PrintWriter writer = new PrintWriter(path.toFile())) {
            String data = PemUtils.csrToPem(csr);
            writer.write(data);
            writer.close();
        } catch (IOException e) {
            throw new CertificateServiceException("An error occurred storing the CSR file", e);
        }

    }

    public PKCS10CertificationRequest createCSR(PublicKey publicKey, PrivateKey privateKey,
            final CredentialProperties credentialProperties, String tenantName) {

        try {
            if (!keypairService.validateKeyPair(publicKey, privateKey)) {
                throw new CertificateServiceException("Error validating the pair of keys");
            }
        } catch (GeneralCryptoLibException e) {
            throw new CertificateServiceException("Error validating the pair of keys", e);
        }
        final Properties props = new Properties();
        try (InputStream input = Thread.currentThread().getContextClassLoader()
            .getResourceAsStream(credentialProperties.getPropertiesFile())) {
            props.load(input);
        } catch (IOException e) {
            throw new CertificateServiceException("Error reading the credential properties", e);
        }

        String subjectCountry = props.getProperty(CertificatePropertiesConstants.SUBJECT_COUNTRY_PROPERTY_NAME);
        String subjectOrganization =
            props.getProperty(CertificatePropertiesConstants.SUBJECT_ORGANIZATION_PROPERTY_NAME);
        String subjectOrganizationUnit =
            props.getProperty(CertificatePropertiesConstants.SUBJECT_ORGANIZATIONAL_UNIT_PROPERTY_NAME);
        String subjectCommonName = props.getProperty(CertificatePropertiesConstants.SUBJECT_COMMON_NAME_PROPERTY_NAME);
        subjectCommonName = subjectCommonName.replace("${tenant}", tenantName);
        X500Principal subject = new X500Principal("C=" + subjectCountry + ", O=" + subjectOrganization + ", OU="
            + subjectOrganizationUnit + ", CN=" + subjectCommonName);

        PKCS10CertificationRequest result = null;
        try {
            CSRGenerator generator = new CSRGenerator();
            result = generator.generate(publicKey, privateKey, subject);
        } catch (OperatorCreationException e) {
            throw new CertificateServiceException("An error occurred during the CSR generation", e);
        }
        return result;
    }

    public JcaPKCS10CertificationRequest getCSRFromPEM(final Path tenantCSRPath) {
        JcaPKCS10CertificationRequest csr = null;
        try {
            csr = PemUtils.csrFromPem(new String(Files.readAllBytes(tenantCSRPath), StandardCharsets.UTF_8));
        } catch (IOException e) {
            throw new CertificateServiceException("CSR could not be loaded", e);
        }

        PublicKey tenantPublicKey = null;
        try {
            tenantPublicKey = csr.getPublicKey();
        } catch (InvalidKeyException | NoSuchAlgorithmException e) {
            throw new CertificateServiceException("CSR could not get the public key from the CSR", e);
        }

        try {
            final ContentVerifierProvider verifierProvider =
                new JcaContentVerifierProviderBuilder().build(tenantPublicKey);

            if (!csr.isSignatureValid(verifierProvider)) {
                throw new CertificateServiceException("CSR was tampered and the signature could not be verified");
            }

        } catch (OperatorCreationException | PKCSException e) {
            throw new CertificateServiceException("An error occurred while verifying the signature of the CSR", e);
        }
        return csr;
    }

    private void compareCertificateDates(ZonedDateTime startDate, ZonedDateTime endDate) {

        if (!startDate.isBefore(endDate))
            throw new CertificateServiceException("The provided end date of the certificate must be after the initial date");

    }

}
