/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2021 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jboss.installer.postinstall;

import org.apache.commons.io.FileUtils;
import org.jboss.installer.core.InstallationData;
import org.jboss.installer.core.InstallationFailedException;
import org.jboss.installer.core.LanguageUtils;
import org.jboss.installer.postinstall.server.DomainServer;
import org.jboss.installer.postinstall.server.EmbeddedServer;
import org.jboss.installer.postinstall.server.ServerOperationException;
import org.jboss.installer.postinstall.server.StandaloneServer;
import org.jboss.installer.postinstall.task.impl.TaskMapperImpl;
import org.jboss.installer.screens.ProcessingScreen;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

import static org.jboss.installer.core.LoggerUtils.taskLog;

public class PostInstallTaskRunnerImpl implements PostInstallTaskRunner {

    private static final String POSTINSTALL_SERVER_START = "postinstall.server.start";
    private static final String POSTINSTALL_SERVER_STARTED = "postinstall.server.started";
    private static final String JBOSS_CLI_CONFIG_PROPERTY = "jboss.cli.config";
    private static final String CLEANUP_EXCEPTION_KEY = "post.install.cleanup.exception";
    private static final String CLEANUP_MSG_KEY = "post.install.task.cleanup";
    private final StandaloneServer standaloneServer;
    private final DomainServer domainServer;
    private final PropertyChangeListener listener;
    private LanguageUtils langUtils;
    private final TaskMapper taskMapper;

    public PostInstallTaskRunnerImpl(StandaloneServer standaloneServer, DomainServer domainServer,
                                     LanguageUtils langUtils, PropertyChangeListener listener) {
        this(standaloneServer, domainServer, langUtils, listener, new TaskMapperImpl());
    }

    public PostInstallTaskRunnerImpl(Path serverPath, LanguageUtils langUtils, PropertyChangeListener listener) {
        this(new StandaloneServer(serverPath), new DomainServer(serverPath), langUtils, listener, new TaskMapperImpl());
    }

    PostInstallTaskRunnerImpl(StandaloneServer standaloneServer, DomainServer domainServer,
                              LanguageUtils langUtils, PropertyChangeListener listener,
                              TaskMapper taskMapper) {
        this.standaloneServer = standaloneServer;
        this.domainServer = domainServer;
        this.listener = listener;
        this.langUtils = langUtils;
        this.taskMapper = taskMapper;
    }

    @Override
    public void close() {
        this.standaloneServer.close();
        this.domainServer.close();
    }

    public void execute(InstallationData installationData, PropertyChangeSupport propertyChangeSupport) throws InstallationFailedException {
        propertyChangeSupport.addPropertyChangeListener(listener);
        taskLog.info("Starting processing post install tasks");
        List<SimplePostInstallTaskImpl> simpleTasks = new ArrayList<>();
        List<CliPostInstallTaskImpl> cliTasks = new ArrayList<>();

        // sort installation data into Simple and Cli tasks
        for (PostInstallTask postInstallTask : installationData.getPostInstallTasks()) {
            PostInstallTaskImpl taskImpl = taskMapper.map(postInstallTask);
            if (taskImpl == null) {
                throw new IllegalStateException("Unable to find the implementation of task " + postInstallTask.getName());
            }
            // inject langUtils into the tasks if needed
            taskImpl.setLanguageUtils(langUtils);

            if (taskImpl instanceof SimplePostInstallTaskImpl) {
                simpleTasks.add((SimplePostInstallTaskImpl) taskImpl);
            } else if (taskImpl instanceof CliPostInstallTaskImpl) {
                cliTasks.add((CliPostInstallTaskImpl) taskImpl);
            } else {
                // assert we don't have unexpected tasks
                throw new IllegalStateException("Unknown task type");
            }
        }

        final TaskPrinter printer = new TaskPrinter(propertyChangeSupport, langUtils);

        // execute tasks without running server
        taskLog.info("Processing SimpleTasks");
        for (SimplePostInstallTaskImpl simpleTask : simpleTasks) {
            propertyChangeSupport.firePropertyChange(POST_INSTALL_TASK_NAME_PROPERTY, "", langUtils.getString(simpleTask.getName()));
            boolean success = simpleTask.applyToInstallation(installationData, printer);
            // don't run any other tasks if one fails
            if (!success) {
                throw new InstallationFailedException("Failed to apply configuration changes", "generic.error.configuration_error");
            }
        }

        if (!cliTasks.isEmpty()) {
            taskLog.info("Processing CliTasks on standalone servers");
            if (standaloneServer != null) {
                // apply all tasks to each standalone configuration
                for (String configuration : standaloneServer.availableConfigurations()) {
                    boolean success = applyCliOperationsOnConfiguration((t) -> t.applyToStandalone(installationData, standaloneServer, printer),
                            cliTasks, standaloneServer, configuration, propertyChangeSupport);
                    // don't run any other configurations if one fails
                    if (!success) {
                        throw new InstallationFailedException("Failed to apply configuration changes", "generic.error.configuration_error");
                    }
                }
            }
            if (domainServer != null) {
                // apply all tasks to each domain configuration
                for (String configuration : domainServer.availableConfigurations()) {
                    boolean success = applyCliOperationsOnConfiguration((t) -> t.applyToDomain(installationData, domainServer, printer),
                            cliTasks, domainServer, configuration, propertyChangeSupport);
                    // don't run any other configurations if one fails
                    if (!success) {
                        throw new InstallationFailedException("Failed to apply configuration changes", "generic.error.configuration_error");
                    }
                }
            }
        }

        // cleanup
        try {
            if (standaloneServer != null) {
                for (Path p : standaloneServer.getTemporaryPaths()) {
                    printer.print(CLEANUP_MSG_KEY, p.toString());
                    FileUtils.deleteDirectory(p.toFile());
                }
            }
            if (domainServer != null) {
                for (Path p : domainServer.getTemporaryPaths()) {
                    printer.print(CLEANUP_MSG_KEY, p.toString());
                    FileUtils.deleteDirectory(p.toFile());
                }
            }
        } catch (IOException e) {
            throw new InstallationFailedException("Failed to remove temporary files", CLEANUP_EXCEPTION_KEY, e);
        }
    }

    private boolean applyCliOperationsOnConfiguration(Function<CliPostInstallTaskImpl, Boolean> taskCallback, List<CliPostInstallTaskImpl> cliTasks,
                                                      EmbeddedServer server, String configuration, PropertyChangeSupport propertyChangeSupport) throws InstallationFailedException {
        boolean resetCliProperty = false;
        try {
            if (System.getProperty(JBOSS_CLI_CONFIG_PROPERTY) == null && server.getJBossCliXml().isPresent()) {
                    System.setProperty(JBOSS_CLI_CONFIG_PROPERTY, server.getJBossCliXml().toString());
                    resetCliProperty = true;
            }
            taskLog.debugf("Starting server %s", configuration);
            propertyChangeSupport.firePropertyChange(POST_INSTALL_TASK_LOG_ENTRY, "",
                    new ProcessingScreen.TaskLogEntry(POSTINSTALL_SERVER_START, configuration));
            server.start(configuration);
            propertyChangeSupport.firePropertyChange(POST_INSTALL_TASK_LOG_ENTRY, "",
                    new ProcessingScreen.TaskLogEntry(POSTINSTALL_SERVER_STARTED, configuration));
            // execute tasks requiring CLI access
            for (CliPostInstallTaskImpl cliTask : cliTasks) {
                propertyChangeSupport.firePropertyChange(POST_INSTALL_TASK_NAME_PROPERTY, "", langUtils.getString(cliTask.getName(), configuration));
                boolean success = taskCallback.apply(cliTask);
                // don't run any more tasks if one fails
                if (!success) {
                    return false;
                }
            }
            return true;
        } catch (ServerOperationException e) {
            throw new InstallationFailedException("Failed to apply configuration changes", "generic.error.configuration_error", e);
        } finally {
            if (resetCliProperty) {
                System.clearProperty(JBOSS_CLI_CONFIG_PROPERTY);
            }
            tryShutdown(server);
        }
    }

    private void tryShutdown(EmbeddedServer server) throws InstallationFailedException {
        try {
            taskLog.debug("Shutting down server");
            server.shutdown();
        } catch (ServerOperationException e) {
            throw new InstallationFailedException("Failed to apply configuration changes", "generic.error.configuration_error", e);
        }
    }
}
