/*
 * 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 com.scytl.products.ov.config.io.ServiceNamesInput;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.PrivateKey;
import java.util.List;
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.InstallTenantPerServiceCommandException;
import com.scytl.products.ov.config.install.InstallTenantClient;
import com.scytl.products.ov.utils.ConfigObjectMapper;

import okhttp3.ResponseBody;
import retrofit2.Retrofit;

/**
 * Tenant Installation command that will allow to install a tenant regarding the
 * provided list of parameters. The former implementation directly performed the
 * tenant activation using a hardcoded set of services.
 */
public class InstallTenantPerServiceCommand 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_TENANT_INSTALLATION_PROPERTIES_FILE = "tenantInstallationPerService.properties";

    private static final String CERTIFICATE_REGISTRY = "CR";

    private static final String CONTEXT_URL = "CONTEXT_URL";

    /**
     * Retrieve data from files, and then attempt to send that data to the
     * appropriate endpoints.
     * <P>
     * This method obtains a list of services from a configuration file. Each of
     * these services should have an endpoint for receiving incoming tenant
     * installation data.
     * <P>
     * For each of the services defined in the services configuration file, this
     * method attempts to read the tenant installation data needed by that
     * service and it constructs the URL of the service endpoint, based on a URL
     * from a properties file, and from the service name.
     *
     * @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());
        final String servicesNamesPath = params2values.get(ActionParams.SERVICES_NAMES_PATH.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 InstallTenantPerServiceCommandException(
					"Error while trying to obtain the tenant certificate in PEM format", e);
        }

        final Path servicesPropertiesPath;
        try {
            servicesPropertiesPath = Paths.get(Paths.get(servicesNamesPath).toFile().getCanonicalPath());
        } catch (IOException e) {
			throw new InstallTenantPerServiceCommandException(
					"Error while trying to obtain path of services properties file", e);
        }

        ConfigPlatformRootKeyLoader configPlatformRootKeyLoader = ConfigPlatformRootKeyLoader.getInstance();

        PrivateKey privateKey = null;
        try {
            privateKey = configPlatformRootKeyLoader.getPrivateKeyFromKeystore(password);
        } catch (ConfigPlatformRootException e) {
            throw new InstallTenantPerServiceCommandException(
                "Error while trying to obtain the authentication key for configuring the installation", e);
        }

        ServiceNamesInput servicesInput = getServicesNameFromJson(servicesPropertiesPath);

        if (installTenantOnAllContexts(tenantID, tenantCertAsPem, systemKeystoresFolder,
            servicesInput.getServiceNames(), 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 List<String> serviceNames, final PrivateKey requestSigningKey) {

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

        boolean result = true;

        for (String serviceName : serviceNames) {

            final TenantInstallationData tenantInstallationData =
                buildTenantInstallation(tenantCertAsPem, serviceName, systemKeystoresFolder);

            boolean thisResult =
                sendTenantInstallation(tenantID, tenantInstallationData, serviceName, requestSigningKey);

            if (thisResult) {
                LOG.info(String.format("Uploaded tenant %s to service %s ", tenantID, serviceName));
            } else {
                LOG.error(String.format("Failed to upload tenant %s to service %s", tenantID, serviceName));
            }

            result = result && thisResult;
        }

        return result;

    }

    private boolean sendTenantInstallation(final String tenantID, final TenantInstallationData tenantInstallationData,
            final String serviceName, final PrivateKey requestSigningKey) {
        try {

            InstallTenantClient client = createClient(serviceName, requestSigningKey);

            String trackingId = generateTrackingId();

            LOG.info(String.format("Sending data for tenantID: %s, to service: %s", tenantID, serviceName));
			if (serviceName.equals(CERTIFICATE_REGISTRY)) {
				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 404:
                    LOG.error("The parent certificate does not exist in the repository", e);
                    break;
                case 412:
                    LOG.error("Error validating the certificate " + e.getErrorBody(), e);
                    break;

                case 409:
                    LOG.error("The certificate or keystore already existed in the repository", e);

                    break;
                default:
                    LOG.error("An error occurred trying to upload the certificate or keystore", e);
                    break;
                }

            }
            return false;

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

    private TenantInstallationData buildTenantInstallation(final String tenantCertAsPem, final String serviceName,
            final String systemKeystoresFolder) {
        TenantInstallationData tenantInstallationData = new TenantInstallationData();

        if (serviceName.equals(CERTIFICATE_REGISTRY)) {
            tenantInstallationData.setEncodedData(tenantCertAsPem);
        } else {
            String fileEnding = "_" + serviceName + ".sks";
            String keystoreBase64 = getFileBase64(systemKeystoresFolder, fileEnding);
            tenantInstallationData.setEncodedData(keystoreBase64);
        }
        return tenantInstallationData;
    }

    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 InstallTenantPerServiceCommandException(
							"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 InstallTenantPerServiceCommandException("Error while trying to obtain a system keystore");
    }

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

        final Properties props = getProperties(PATH_OF_TENANT_INSTALLATION_PROPERTIES_FILE);

        String contextSpecificURL = props.getProperty(CONTEXT_URL).replace("XX", serviceName).toLowerCase();

        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 InstallTenantPerServiceCommandException(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 InstallTenantPerServiceCommandException(
					"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);
    }

    private ServiceNamesInput getServicesNameFromJson(final Path pathToJson) {

        ConfigObjectMapper configObjectMapper = new ConfigObjectMapper();
        ServiceNamesInput serviceNamesInput = null;
        try {
            serviceNamesInput = configObjectMapper.fromJSONFileToJava(pathToJson.toFile(), ServiceNamesInput.class);
        } catch (IOException e) {
            throw new InstallTenantPerServiceCommandException("An error occurred while loading the services file", e);
        }

        return serviceNamesInput;
    }
}
