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

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

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.certificates.bean.X509CertificateType;
import com.scytl.cryptolib.scytl.keystore.cryptoapi.CryptoAPIScytlKeyStore;
import com.scytl.products.ov.commons.application.TenantActivatorKeystoreException;
import com.scytl.products.ov.commons.application.TenantActivatorPasswordException;
import com.scytl.products.ov.commons.beans.domain.model.tenant.TenantActivationData;
import com.scytl.products.ov.commons.beans.domain.model.tenant.TenantSystemKeys;
import com.scytl.products.ov.commons.beans.utils.KeyStoreLoader;
import com.scytl.products.ov.commons.domain.model.tenant.TenantKeystoreEntity;
import com.scytl.products.ov.commons.domain.model.tenant.TenantKeystoreRepository;
import java.security.cert.CertificateException;

/**
 * Class that can be used for activating a tenant in a service.
 */
public final class TenantActivator {

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

    private static final String NAME_OF_PROPERTY_THAT_SPECIFIES_TENANT_PW_DIRECTORY = "tenantpasswordsdirectory";

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

    private final TenantKeystoreRepository tenantKeystoreRepository;

    private final TenantSystemKeys tenantSystemKeys;

    private final String serviceName;

    private final TenantActivatorTools tenantActivatorTools = new TenantActivatorTools();

    /**
     * Instantiates a new TenantActivator.
     *
     * @param tenantKeystoreRepo
     *            the repository that contains tenant keystores.
     * @param tenantSysKeys
     *            the tenant system keys.
     * @param serviceName
     *            the service name.
     */
    public TenantActivator(final TenantKeystoreRepository tenantKeystoreRepo, final TenantSystemKeys tenantSysKeys,
            final String serviceName) {

        tenantKeystoreRepository = tenantKeystoreRepo;
        tenantSystemKeys = tenantSysKeys;
        this.serviceName = serviceName;
    }

    /**
     * Activate a tenant using received keystore.
     * <P>
     * This method could be called when a tenant system keystore for the tenant with the specified tenantID has been
     * received. This method will attempt to find the corresponding keystore password in a file that should exist in the
     * directory specified by the property "tenantpasswordsdirectory".
     * <P>
     * This method will delete tenant system keystore password files that exist in the directory defined by the property
     * "tenantpasswordsdirectory".
     *
     * @param tenantID
     *            the tenant id.
     * @param keystore
     *            the keystore.
     * @return true if successful, false otherwise.
     */
    public boolean activateUsingReceivedKeystore(final String tenantID, final String keystore) {

        LOG.info(serviceName + " - activating tenantID: " + tenantID);

        char[] password;
        try {
            password = tenantActivatorTools.getPasswordFromFile(getTenantPasswordsDirectoryProperty(), tenantID,
                serviceName);
        } catch (IOException e) {
            String errorMsg = serviceName + " - failed to get tenant password from file for tenant: " + tenantID;
            LOG.error(errorMsg, e);
            return false;
        }

        LOG.info(serviceName + " - obtained tenant keystore password for tenant: " + tenantID);

        boolean result = populateCacheFromKeystore(tenantID, keystore, password);

        deleteKeystorePasswordFileRelatedToThisTenant(tenantID);

        return result;
    }

    /**
     * Activate tenants from DB and files.
     * <P>
     * This method could be called at the moment that the application has been started up. It searches for tenant
     * keystore passwords in files and the actual keystores in the DB. It will attempt to activate all of the tenants
     * for which it can find passwords and keystores.
     * <P>
     * This method will delete tenant system keystore password files that exist in the directory defined by the property
     * "tenantpasswordsdirectory".
     *
     * @return a list of tenants that have been successfully activated.
     */
    public List<String> activateTenantsFromDbAndFiles() {

        LOG.info(serviceName + " - attempting to activate tenants from DB and files");

        List<String> listSuccessfullyActivatedTenants = new ArrayList<>();

        List<TenantActivationData> tenantData;
        try {
            tenantData = tenantActivatorTools.getListTenantsFromPasswordFiles(getTenantPasswordsDirectoryProperty(),
                serviceName);
        } catch (IOException e) {
            String errorMsg =
                serviceName + " - failed to construct list of tenants from tenant keystore password files";
            LOG.error(errorMsg, e);
            return listSuccessfullyActivatedTenants;
        }

        int numTenants = tenantData.size();
        LOG.info(serviceName + " - number of tenants found in keystore password files: " + numTenants);
        if (numTenants < 1) {
            return listSuccessfullyActivatedTenants;
        }

        for (TenantActivationData tenantActivationData : tenantData) {

            String tenantID = tenantActivationData.getTenantID();

            LOG.info(serviceName + " - processing tenant: " + tenantID);

            if (tenantKeystoreRepository.checkIfKeystoreExists(tenantID, KEYTYPE)) {

                LOG.info(serviceName + " - keystore for this tenant exists in DB, will attempt to activate from DB");

                if (activateFromDB(tenantActivationData)) {
                    listSuccessfullyActivatedTenants.add(tenantID);
                } else {
                    LOG.error(serviceName + " - failed to activate tenant: " + tenantID);
                }
            } else {

                LOG.warn(serviceName + " - keystore for this tenant not found in DB");
            }

            deleteKeystorePasswordFileRelatedToThisTenant(tenantActivationData.getTenantID());
        }

        LOG.info(serviceName + " - number of tenants activated: " + listSuccessfullyActivatedTenants.size());

        return listSuccessfullyActivatedTenants;
    }

    /**
     * Activate a particular tenant using keystores from the DB, and using the passwords contained within the received
     * tenant data.
     * <P>
     * This method can be called when a particular tenant should be activated without restarting the system.
     *
     * @param tenantActivationData
     *            the tenant activation data.
     * @return true if successful, false otherwise.
     */
    public boolean activateFromDB(final TenantActivationData tenantActivationData) {

        String tenantID = tenantActivationData.getTenantID();

        LOG.info(serviceName + " - activating tenantID: " + tenantID);

        TenantKeystoreEntity tenantKeystoreEntity = getTenantKeystoreEntity(tenantID);

        LOG.info(serviceName + " - obtained tenant keystore entity from DB");

		return populateCacheFromKeystore(tenantID, tenantKeystoreEntity.getKeystoreContent(),
				tenantActivationData.getSystemKeystorePassword().toCharArray());
    }

    private void deleteKeystorePasswordFileRelatedToThisTenant(final String tenantID) {

        if (tenantActivatorTools.attemptToDeletePasswordsFiles(getTenantPasswordsDirectoryProperty(), serviceName,
            tenantID)) {
            LOG.info(serviceName + " - deleted tenant keystore password file related to tenant: " + tenantID);
        } else {
            LOG.warn(serviceName
                + " - non-fatal issue - failed to delete tenant keystore password file related to tenant: " + tenantID);
        }
    }

    /**
     * Get the relevant contents of a keystore and place them in memory for
     * convenient access.
     *
     * @param tenantID
     *            the identifier of the tenant the keystore belongs to
     * @param keystoreContents
     *            the Base64-encoded contents of the keystore
     * @param password
     *            the password to open the keystore
     * @return
     * @throws GeneralCryptoLibException
     */
    private boolean populateCacheFromKeystore(final String tenantID, final String keystoreContents,
            final char[] password) {

        // Open the keystore.
        CryptoAPIScytlKeyStore tenantKeystore = KeyStoreLoader
            .loadKeyStoreFromStream(new ByteArrayInputStream(Base64.decodeBase64(keystoreContents)), password);

        LOG.info(serviceName + " - obtained tenant keystore");

        // Cache the tenant's private keys.
       tenantSystemKeys.setInitialized(tenantID, tenantActivatorTools.getPrivateKeys(tenantKeystore, password));

        Arrays.fill(password, '\u0000');
        LOG.info(serviceName + " - obtained tenant private key");

        boolean result;
        try {
            // Cache the tenant's certificate chains.
            tenantSystemKeys.addCertificateChains(tenantID,
                tenantActivatorTools.getCertificateChains(tenantKeystore));

            LOG.info(serviceName + " - tenant: " + tenantID + " activated successfully");
            result = true;
        } catch (GeneralCryptoLibException | CertificateException e) {
            LOG.error("The tenant certificate chains could not be retrieved", e);
            result = false;
        }

        return result;
    }

    private String getTenantPasswordsDirectoryProperty() {

        String passwordsFilePath = System.getProperty(NAME_OF_PROPERTY_THAT_SPECIFIES_TENANT_PW_DIRECTORY);

        if (passwordsFilePath == null) {
            String errorMsg = serviceName + " - failed to obtain passwords files directory property";
            LOG.error(errorMsg);
            throw new TenantActivatorPasswordException(errorMsg);
        }

        return passwordsFilePath;
    }

    private TenantKeystoreEntity getTenantKeystoreEntity(final String tenantID) {

        LOG.info(serviceName + " - searching in DB for: " + tenantID + ", " + KEYTYPE);
        TenantKeystoreEntity entity = null;
        try {
            entity = tenantKeystoreRepository.getByTenantAndType(tenantID, KEYTYPE);
        } catch (Exception e) {
            throw new TenantActivatorKeystoreException(serviceName + " - failed to get privatekey from keystore", e);
        }

        return entity;
    }
}
