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

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.PrivateKey;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.api.primitives.PrimitivesServiceAPI;
import com.scytl.cryptolib.primitives.service.PrimitivesService;
import com.scytl.cryptolib.securerandom.cryptoapi.CryptoAPIRandomString;
import com.scytl.products.ov.commons.beans.domain.model.tenant.TenantInstallationData;
import com.scytl.products.ov.commons.infrastructure.exception.OvCommonsInfrastructureException;
import com.scytl.products.ov.commons.infrastructure.remote.client.NodeIdentifier;
import com.scytl.products.ov.commons.infrastructure.remote.client.RestClientConnectionManager;
import com.scytl.products.ov.commons.infrastructure.remote.client.RetrofitConsumer;
import com.scytl.products.ov.commons.infrastructure.remote.client.RetrofitException;
import com.scytl.products.ov.config.commons.ConfigPlatformRootKeyLoader;
import com.scytl.products.ov.config.commons.exception.ConfigPlatformRootException;
import com.scytl.products.ov.config.commons.exception.InstallTenantCommandException;
import com.scytl.products.ov.config.install.InstallTenantClient;

import okhttp3.ResponseBody;
import retrofit2.Retrofit;

public class InstallTenantCommand implements Command {

    private static final int TRACKING_ID_LENGTH = 16;

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

    private static final NodeIdentifier THIS_NODE = NodeIdentifier.CONFIG_PLATFORM_ROOT;

    private static final String PATH_OF_PLATFORM_INSTALLATION_PROPERTIES_FILE = "tenantInstallation.properties";

    /**
     * @see com.scytl.products.ov.config.cli.Command#execute(java.util.Map)
     */
    @Override
    public void execute(final Map<String, String> params2values) {

        LOG.info("Starting the tenant installation process");

        final String tenantID = params2values.get(ActionParams.TENANT_ID.getName());
        final String tenantCertPath = params2values.get(ActionParams.TENANT_CERT_PATH.getName());
        final String systemKeystoresFolder = params2values.get(ActionParams.SYSTEM_KEYSTORES_FOLDER.getName());
        final String password = params2values.get(ActionParams.PASSWORD.getName());

        LOG.info("tenantID: " + tenantID);
        LOG.info("tenantCertPath: " + tenantCertPath);
        LOG.info("systemKeystoresFolder: " + systemKeystoresFolder);

        Path pathTenantCert = Paths.get(tenantCertPath).toAbsolutePath();
        LOG.info("Path of tenant certificate: " + pathTenantCert.toString());

        String tenantCertAsPem;
        try {
            tenantCertAsPem = getTenantCertAsPem(pathTenantCert);
        } catch (IOException e) {
			throw new InstallTenantCommandException("Error while trying to obtain the tenant certificate in PEM format",
					e);
        }
        ConfigPlatformRootKeyLoader configPlatformRootKeyLoader = ConfigPlatformRootKeyLoader.getInstance();

        PrivateKey privateKey = null;
        try {
            privateKey = configPlatformRootKeyLoader.getPrivateKeyFromKeystore(password);
        } catch (ConfigPlatformRootException e) {
            throw new InstallTenantCommandException(
                "Error while trying to obtain the authentication key for configuring the installation", e);
        }
        if (installTenantOnAllContexts(tenantID, tenantCertAsPem, systemKeystoresFolder, privateKey)) {
            LOG.info("Overall install tenant result: SUCCESS");
        } else {
            LOG.error("Overall install tenant result: FAILED");
            LOG.error("Troubleshooting tips:");
            LOG.error("Is the server running?");
            LOG.error("Does the relevant properties file have the correct values? (such as port number)");
            LOG.error("Could the data already exist in the database?");
        }
    }

    private String getTenantCertAsPem(final Path path) throws IOException {

        try (FileInputStream fisTargetFile = new FileInputStream(path.toFile())) {
            return IOUtils.toString(fisTargetFile, "UTF-8");
        }
    }

    private boolean installTenantOnAllContexts(final String tenantID, final String tenantCertAsPem,
            final String systemKeystoresFolder, final PrivateKey requestSigningKey) {

        LOG.info("Beginning process of installing tenant to all the services");

        boolean result = true;

        // install tenant on AU
        boolean thisResult = sendKeystoreToService(tenantID, systemKeystoresFolder, "AU", requestSigningKey);
        if (thisResult) {
            LOG.info("Uploaded tenant " + tenantID + " to service AU");
        } else {
            LOG.error("Failed to upload tenant " + tenantID + " to service AU");
        }
        result = result && thisResult;

        // install tenant on EI
        thisResult = sendKeystoreToService(tenantID, systemKeystoresFolder, "EI", requestSigningKey);
        if (thisResult) {
            LOG.info("Uploaded tenant " + tenantID + " to service EI");
        } else {
            LOG.error("Failed to upload tenant " + tenantID + " to service EI");
        }
        result = result && thisResult;

        // install tenant on VV
        thisResult = sendKeystoreToService(tenantID, systemKeystoresFolder, "VV", requestSigningKey);
        if (thisResult) {
            LOG.info("Uploaded tenant " + tenantID + " to service VV");
        } else {
            LOG.error("Failed to upload tenant " + tenantID + " to service VV");
        }
        result = result && thisResult;

        // install tenant on CR
        thisResult = sendCertificateToCertificateRegistry(tenantID, tenantCertAsPem, requestSigningKey);
        if (thisResult) {
            LOG.info("Uploaded tenant " + tenantID + " to service CR");
        } else {
            LOG.error("Failed to upload tenant " + tenantID + " to service CR");
        }
        result = result && thisResult;

        return result;
    }

    private boolean sendKeystoreToService(final String tenantID, final String systemKeystoresFolder,
            final String service, final PrivateKey requestSigningKey) {
        try {

            // Note: this method assumes that a file will exist in specified
            // systems keystores folder that matches the
            // pattern: "<tenant_name>_<service>.sks"
            TenantInstallationData tenantInstallationData = new TenantInstallationData();
            String fileEnding = "_" + service + ".sks";
            String keystoreBase64 = getFileBase64(systemKeystoresFolder, fileEnding);
            tenantInstallationData.setEncodedData(keystoreBase64);
            InstallTenantClient client = createClient(service, requestSigningKey);
            LOG.info("Sending data for tenantID: " + tenantID + ", to service: " + service);
            String trackingId = generateTrackingId();
			if ("cr".equalsIgnoreCase(service)) {
				try (ResponseBody responseBody = RetrofitConsumer
						.processResponse(client.postToCrContext(trackingId, tenantID, tenantInstallationData))) {					
					//This block is intentionally left blank for the use of Closeable 
				}
			} else {
				try (ResponseBody responseBody = RetrofitConsumer
						.processResponse(client.postToContext(trackingId, tenantID, tenantInstallationData))) {
					//This block is intentionally left blank for the use of Closeable 
				}
			}
        } catch (RetrofitException e) {
            // in case of connection error (invalid host, etc..)
            if (e.getErrorBody() != null && e.getErrorBody().contentLength() == 0) {
                LOG.error("Network failure sending data to service.", e);
            } else {
                switch (e.getHttpCode()) {
                case 409:
                    LOG.error("The keystore already existed in the repository.", e);
                    break;
                default:
                    LOG.error("An error occurred trying to upload the keystore.", e);
                    break;
                }

            }
            return false;

        } catch (GeneralCryptoLibException e) {
            LOG.error("Failed to generate TrackingId", e);
            return false;
        }
        return true;
    }

    private boolean sendCertificateToCertificateRegistry(final String tenantID, final String tenantCertAsPem,
            final PrivateKey requestSigningKey) {

        try {
            TenantInstallationData tenantInstallationData = new TenantInstallationData();
            tenantInstallationData.setEncodedData(tenantCertAsPem);
            InstallTenantClient clientCR = createClient("CR", requestSigningKey);
            LOG.info("Sending data for tenantID: " + tenantID + ", to service: CR");
            String trackingId = generateTrackingId();
            
			try (ResponseBody responseBody = RetrofitConsumer
					.processResponse(clientCR.postToCrContext(trackingId, tenantID, tenantInstallationData))) {
				//This block is intentionally left blank for the use of Closeable
			}
            
        } catch (RetrofitException e) {
            // in case of connection error (invalid host, etc..)
            if (e.getErrorBody() != null && e.getErrorBody().contentLength() == 0) {
                LOG.error("Network failure sending data to service.", e);
            } else {
                switch (e.getHttpCode()) {
                case 404:
                    LOG.error("The parent certificate does not exist in the repository", e);
                    break;
                case 412:
                    try {
                        LOG.error("Error validating the certificate "
                            + IOUtils.toString(e.getErrorBody().byteStream(), StandardCharsets.UTF_8));
                    } catch (IOException ioE) {
                        LOG.error("Error validating the certificate ", ioE);
                    }
                    break;
                case 409:
                    LOG.error("The certificate already existed in the repository", e);
                    break;
                default:
                    LOG.error("An error occurred trying to upload the certificate", e);
                    break;

                }
            }
            return false;

        } catch (GeneralCryptoLibException e) {
            LOG.error("Failed to generate TrackingId", e);
            return false;
        }
        return true;
    }

    private String getFileBase64(final String systemKeystoresFolder, final String fileEnding) {

        File systemKeystoresFolderAsFile = new File(systemKeystoresFolder);
        File[] allFilesInSystemKeystoresFolder = systemKeystoresFolderAsFile.listFiles();

        for (int i = 0; i < allFilesInSystemKeystoresFolder.length; i++) {

            File fileInSystemKeystoresFolder = allFilesInSystemKeystoresFolder[i];

            if (fileInSystemKeystoresFolder.getName().contains(fileEnding)) {

                LOG.info("Found file: " + fileInSystemKeystoresFolder);

                Path keystorePath = fileInSystemKeystoresFolder.toPath();

                String keystoreBase64;
                try {
                    keystoreBase64 = com.scytl.products.ov.config.utils.FileUtils.getFileBytesAsBase64(keystorePath);
                } catch (IOException e) {
					throw new InstallTenantCommandException(
							"Error while trying to obtain a logging keystore for the service", e);
                }

                return keystoreBase64;
            }
        }

        LOG.info("Error - failed to find system keystore. Throwing exception");
        throw new InstallTenantCommandException("Error while trying to obtain a system keystore");
    }

    private InstallTenantClient createClient(final String serviceName, final PrivateKey requestSigningKey) {

        final Properties props = getProperties(PATH_OF_PLATFORM_INSTALLATION_PROPERTIES_FILE);

        final String propertykey = serviceName + "_URL";
        final String contextSpecificURL = props.getProperty(propertykey);

        LOG.info("Sending tenant installation data to endpoint: " + contextSpecificURL);

        Retrofit client;
        try {

            client = RestClientConnectionManager.getInstance()
                .getRestClientWithInterceptorAndJacksonConverter(contextSpecificURL, requestSigningKey, THIS_NODE);

        } catch (OvCommonsInfrastructureException e) {
            String errorMsg = "Error occurred while trying to obtain a REST client.";
            LOG.error(errorMsg);
            throw new InstallTenantCommandException(errorMsg, e);

        }

        return client.create(InstallTenantClient.class);

    }

    private Properties getProperties(String path) {
        final Properties props = new Properties();
        try (InputStream input = Thread.currentThread().getContextClassLoader().getResourceAsStream(path)) {
            props.load(input);
        } catch (IOException e) {
            throw new InstallTenantCommandException("An error occurred while loading the platform installation properties", e);
        }
        return props;
    }

    private String generateTrackingId() throws GeneralCryptoLibException {
        PrimitivesServiceAPI primitivesService = new PrimitivesService();
        CryptoAPIRandomString randomString = primitivesService.get32CharAlphabetCryptoRandomString();
        return randomString.nextRandom(TRACKING_ID_LENGTH);
    }
}
