/*
 * 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.logging.config;

import static java.nio.file.Files.deleteIfExists;
import static java.nio.file.Files.newBufferedReader;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStore.PasswordProtection;
import java.security.KeyStore.PrivateKeyEntry;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Enumeration;
import java.util.Properties;

import javax.security.auth.DestroyFailedException;

import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.api.stores.StoresServiceAPI;
import com.scytl.cryptolib.api.stores.bean.KeyStoreType;
import com.scytl.cryptolib.certificates.bean.X509CertificateType;
import com.scytl.cryptolib.stores.service.StoresService;
import com.scytl.products.ov.commons.beans.domain.model.platform.LoggingInitializationState;
import com.scytl.products.ov.commons.beans.exceptions.SecureLoggerInitialitzerException;
import com.scytl.products.ov.commons.logging.domain.model.LoggingKeystoreEntity;
import com.scytl.products.ov.commons.logging.domain.model.LoggingKeystoreRepository;
import com.scytl.products.ov.commons.logging.producers.SecureLoggerProducer;

/**
 * Class that can be used to initialize the secure logging system.
 * <P>
 * Note: two public methods are provided:
 * <ol>
 * <li>Initialize using data that has already been stored in the DB and is
 * accessible now.</li>
 * <li>Initialize using data that is received at this moment.</li>
 * </ol>
 */
public final class SecureLoggerInitializer {

	private static final String DB_ENCRYPTION_KEY_TYPE = X509CertificateType.ENCRYPT.name();

	private static final String DB_SIGNING_KEY_TYPE = X509CertificateType.SIGN.name();

	private static final String NAME_OF_PROPERTY_THAT_SPECIFIES_LOGGING_PW_DIRECTORY = "loggingpasswordsdirectory";

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

    private final String context;

    private final String encryptionPasswordsPropertiesKey;

    private final String signingPasswordsPropertiesKey;

    private final LoggingInitializationState loggingInitializationState;

    private final LoggingKeystoreRepository loggingKeystoreRepository;

    public SecureLoggerInitializer(
            final LoggingInitializationState loggingInitializationState,
            final LoggingKeystoreRepository loggingKeystoreRepository,
            final String context,
            final String encryptionPasswordsPropertiesKey,
            final String signingPasswordsPropertiesKey) {

		this.loggingInitializationState = loggingInitializationState;
		this.loggingKeystoreRepository = loggingKeystoreRepository;
		this.context = context;
		this.encryptionPasswordsPropertiesKey = encryptionPasswordsPropertiesKey;
		this.signingPasswordsPropertiesKey = signingPasswordsPropertiesKey;
    }

    /**
     * Check if the required data exists in the database, and if it does, then
     * initialize the secure logging system using that data.
     * <P>
     * Note: even if the logging system is not initialized, this method will
     * attempt to delete any logging keystore password files that it finds in
     * the directory defined by the property "loggingpasswordsdirectory".
     *
     * @return true if the logging has been initialized, false otherwise.
     */
    public boolean initializeIfDataExistsInDB() {

        if (loggingKeystoreRepository
            .checkIfKeystoreExists(DB_ENCRYPTION_KEY_TYPE)
            && loggingKeystoreRepository
                .checkIfKeystoreExists(DB_SIGNING_KEY_TYPE)) {

            LOG.info(context
                + " - logging data does exist in DB, will attempt to initialize logging");

            initializeFromDB();
            return true;
        } else {

            LOG.info(context
                + " - logging data does not exist in DB, will not attempt to initialize logging");

            // even though we have not been able to init the logging, we still
            // try to clean up
            deletedFileFromLoggingPasswordsDirectory();

            return false;
        }
    }

    /**
     * Initialize the secure logging system using the received keystores.
     * <P>
     * Note: even if the logging system is not initialized, this method will
     * attempt to delete any logging keystore password files that it finds in
     * the directory defined by the property "loggingpasswordsdirectory".
     *
     * @param encryptionKeystoreBase64
     *            the encryption keystore in base64.
     * @param signingKeystoreBase64
     *            the signing keystore in base64.
     * @return true if the logging has been initialized, false otherwise.
     */
    public void initializeFromUploadData(
            final String encryptionKeystoreBase64,
            final String signingKeystoreBase64) {
        LOG.info(
            context + " - attempting to init logging from uploaded data");
        Properties properties = loadAndDeleteLoggingProperties();
        String signingPassword =
            properties.getProperty(signingPasswordsPropertiesKey);
        PrivateKey signingPrivateKey =
            getSigningPrivateKey(signingKeystoreBase64, signingPassword);
        String encryptionPassword =
            properties.getProperty(encryptionPasswordsPropertiesKey);
        PrivateKeyEntry encryptionKeyEntry = getEncryptionKeyEntry(
            encryptionKeystoreBase64, encryptionPassword);
        PrivateKey encryptionPrivateKey =
            encryptionKeyEntry.getPrivateKey();
        PublicKey encryptionPublicKey =
            encryptionKeyEntry.getCertificate().getPublicKey();
        LOG.info(context + " - logging keys reconstructed");
        configureSecureLogger(signingPrivateKey, encryptionPrivateKey,
            encryptionPublicKey);
        LOG.info(context + " - configured secure logger");
        configureAuditLogger(signingPrivateKey, encryptionPrivateKey,
            encryptionPublicKey);
        LOG.info(context + " - configured secure audit logger");
    }

    private Properties loadAndDeleteLoggingProperties() {
        Properties properties = new Properties();
        Path file = getLoggingPropertiesFile();
        LOG.info(context + " - logging keystores passwords file path: "
                + file.toString());
        try {
            try {
                try (Reader reader =
                    newBufferedReader(file, StandardCharsets.UTF_8)) {
                    properties.load(reader);
                }
            } finally {
                deleteIfExists(file);
            }
        } catch (IOException e) {
            throw new IllegalStateException(
                "Failed to load and to delete logging properties.", e);
        }
        return properties;
    }

    private Path getLoggingPropertiesFile() {
		String directory = System.getProperty(NAME_OF_PROPERTY_THAT_SPECIFIES_LOGGING_PW_DIRECTORY);
        LOG.info(context + " - logging passwords directory property: "
            + directory);
        String file = "logging_" + context + ".properties";
        return Paths.get(directory, file).toAbsolutePath();
    }

    private PrivateKey getSigningPrivateKey(String keyStoreBase64,
            String password) {
        return getPrivateKeyEntry(keyStoreBase64, password)
            .getPrivateKey();
    }

    private PrivateKeyEntry getEncryptionKeyEntry(String keyStoreBase64,
            String password) {
        return getPrivateKeyEntry(keyStoreBase64, password);
    }

    private PrivateKeyEntry getPrivateKeyEntry(String keyStoreBase64,
            String password) {
        KeyStore store = loadKeyStore(keyStoreBase64, password);
        return getPrivateKeyEntry(store, password);
    }

    private PrivateKeyEntry getPrivateKeyEntry(KeyStore store,
            String password) {
        try {
            Enumeration<String> aliases = store.aliases();
            while (aliases.hasMoreElements()) {
                String alias = aliases.nextElement();
                if (store.entryInstanceOf(alias, PrivateKeyEntry.class)) {
                    PasswordProtection protection =
                        new PasswordProtection(password.toCharArray());
                    try {
                        return (PrivateKeyEntry) store.getEntry(alias,
                            protection);
                    } finally {
                        protection.destroy();
                    }
                }
            }
        } catch (GeneralSecurityException | DestroyFailedException e) {
            throw new IllegalStateException(
                "Failed to get private key entry.", e);
        }
        throw new IllegalStateException(
            "Failed to get a private key entry.");
    }

    private void configureSecureLogger(PrivateKey signingPrivateKey,
            PrivateKey encryptionPrivateKey,
            PublicKey encryptionPublicKey) {
        new SecureLoggerConfig().configure(
            SecureLoggerProducer.LOGGER_NAME_SECURE_LOGGER,
            signingPrivateKey, encryptionPrivateKey, encryptionPublicKey);
    }

    private void configureAuditLogger(PrivateKey signingPrivateKey,
            PrivateKey encryptionPrivateKey,
            PublicKey encryptionPublicKey) {
        new AuditLoggerConfig().configure(
            SecureLoggerProducer.LOGGER_NAME_AUDIT_LOGGER,
            signingPrivateKey, encryptionPrivateKey, encryptionPublicKey);
    }

    private void deletedFileFromLoggingPasswordsDirectory() {

		String loggingPasswordFilesDirectory = System.getProperty(NAME_OF_PROPERTY_THAT_SPECIFIES_LOGGING_PW_DIRECTORY);
        LOG.info(context + " - cleaning up logging passwords directory: "
            + loggingPasswordFilesDirectory);

        String loggingPasswordFileName =
            "logging_" + context + ".properties";
        Path passwordsFilePath = Paths.get(loggingPasswordFilesDirectory,
            loggingPasswordFileName);
        deleteFileIfExists(passwordsFilePath);
    }

    private void initializeFromDB() {

        LOG.info(
            context + " - attempting to initialize logging from DB...");

        if (loggingInitializationState.getInitialized()) {
            String errorMsg =
                "Logging already initialized. Request being ignored";
            LOG.info(errorMsg);
        }

        LoggingKeystoreEntity loggingKeystoreEntity =
            loggingKeystoreRepository
                .getLoggingKeystoreByType(DB_ENCRYPTION_KEY_TYPE);
        String encryptionKey = loggingKeystoreEntity.getKeystoreContent();

        LoggingKeystoreEntity signingKeystoreEntity =
            loggingKeystoreRepository
                .getLoggingKeystoreByType(DB_SIGNING_KEY_TYPE);
        String signingKey = signingKeystoreEntity.getKeystoreContent();

        initializeFromUploadData(encryptionKey, signingKey);
        LOG.info(context
            + " - successfully initialized logging using keystores from DB");
    }

    private KeyStore loadKeyStore(final String keystoreBase64,
            final String password) {

        StoresServiceAPI storesService;
        try {
            storesService = new StoresService();
        } catch (GeneralCryptoLibException e) {
			throw new SecureLoggerInitialitzerException("Exception while trying to create StoresService", e);
        }
        byte[] keystoreAsBytes = Base64.decodeBase64(keystoreBase64);
        KeyStore loggingEncryptionKeystore = null;

        try {
            loggingEncryptionKeystore =
                storesService.loadKeyStore(KeyStoreType.PKCS12,
                    new ByteArrayInputStream(keystoreAsBytes),
                    password.toCharArray());
        } catch (GeneralCryptoLibException e) {
			throw new SecureLoggerInitialitzerException("Error trying to load keystore, error: " + e.getMessage(), e);
        }

        return loggingEncryptionKeystore;
    }

    private boolean deleteFileIfExists(final Path pathOfFileToDelete) {

        if (!Files.exists(pathOfFileToDelete)) {
            return true;
        }

        try {
            Files.delete(pathOfFileToDelete);
        } catch (IOException e) {
			LOG.warn(context + " - non-fatal error - failed to delete file: "
					+ pathOfFileToDelete.toAbsolutePath().toString(), e);
            return false;
        }
        return true;
    }
}
