/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2022 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.task;

import com.google.auto.service.AutoService;
import org.jboss.installer.core.InstallationData;
import org.jboss.installer.core.LoggerUtils;
import org.jboss.installer.postinstall.PostInstallTask;
import org.jboss.installer.postinstall.SimplePostInstallTask;
import org.jboss.installer.postinstall.TaskPrinter;
import org.jboss.installer.postinstall.server.DomainServer;
import org.jboss.installer.screens.ComponentSelectionScreen;

import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;

@AutoService(PostInstallTask.class)
public class Ipv6EnabledTask implements SimplePostInstallTask {
    private static final String TASK_NAME_KEY = "post_install.task.enable_ipv6.name";

    private static final String IPv4_LOCALHOST = "127.0.0.1";
    private static final String LOCALHOST_TEXT = "localhost";
    private static final String PREFER_IPV4_PROPERTY = "java.net.preferIPv4Stack";
    private static final String PREFER_IPV6_PROPERTY = "java.net.preferIPv6Addresses";
    private static final String ENABLE_IPV6_JVM_PROPERTY = String.format("-D%s=false -D%s=true", PREFER_IPV4_PROPERTY, PREFER_IPV6_PROPERTY);
    private static final String ENABLE_IPV4_JVM_PROPERTY = String.format("-D%s=true", PREFER_IPV4_PROPERTY);
    public static final String[] REQUIRED_CONFIGS = new String[]{
            "standalone.conf", "standalone.conf.bat", "standalone.conf.ps1",
            "domain.conf", "domain.conf.bat", "domain.conf.ps1"
    };
    public static final String[] OPTIONAL_CONFIGS = new String[]{
            "appclient.conf", "appclient.conf.bat", "appclient.conf.ps1"
    };

    @Override
    public String getName() {
        return TASK_NAME_KEY;
    }

    @Override
    public String getSerializationName() {
        return "enable-ipv6";
    }

    @Override
    public Class<? extends InstallationData.PostInstallConfig> getConfigClass() {
        return DatasourceTask.Config.class;
    }

    @Override
    public boolean applyToInstallation(InstallationData idata, TaskPrinter printer) {
        final Path serverPath = idata.getTargetFolder();

        printer.print("tasks.enable_ipv6.started");

        try {
            processStandaloneConfigs(serverPath, printer);
            processDomainConfigs(serverPath, printer);
            processAppclientConfig(serverPath, printer);

            replacePreferredStackInScriptConfigs(serverPath, printer, isAppclientInstalled(idata));

            printer.print("tasks.enable_ipv6.file", "jboss-cli.bat");
            addPreferredStackWindowsCliScript(serverPath);
            printer.print("tasks.enable_ipv6.file", "jboss-cli.sh");
            addPreferredStackUnixCliScript(serverPath);
            printer.print("tasks.enable_ipv6.file", "jboss-cli.ps1");
            addPreferredStackPowershellCliScript(serverPath);
            printer.print("tasks.enable_ipv6.finished");
            return true;
        } catch (IOException e) {
            e.printStackTrace();
            printer.print("tasks.enable_ipv6.failed");
            printer.print(e);
            return false;
        } catch (ConfigFileNotFoundException e) {
            printer.print("tasks.enable_ipv6.missing_file", e.getMissingPath().toString());
            printer.print(e);
            return false;
        }
    }

    private static boolean isAppclientInstalled(InstallationData idata) {
        return !idata.getExcludedPackages().contains(ComponentSelectionScreen.APPCLIENT_PACKAGE);
    }

    private void addPreferredStackWindowsCliScript(Path serverPath) {
        final Path configFilePath = getPath(serverPath, "jboss-cli.bat");
        final String prefix = "set \"JAVA_OPTS";
        final String enableIpv6Property = "set \"JAVA_OPTS=!JAVA_OPTS! " + ENABLE_IPV6_JVM_PROPERTY;

        addPreferredStackOption(configFilePath, prefix, enableIpv6Property);
    }

    private void addPreferredStackPowershellCliScript(Path serverPath) {
        final Path configFilePath = getPath(serverPath, "jboss-cli.ps1");
        final String prefix = "$SERVER_OPTS = ";
        final String enableIpv6Property = "$JAVA_OPTS += '" + ENABLE_IPV6_JVM_PROPERTY +"'";

        addPreferredStackOption(configFilePath, prefix, enableIpv6Property);
    }

    private void addPreferredStackUnixCliScript(Path serverPath) {
        final Path configFilePath = getPath(serverPath, "jboss-cli.sh");
        final String prefix = "JAVA_OPTS=";
        final String enableIpv6Property = "JAVA_OPTS=\"$JAVA_OPTS " + ENABLE_IPV6_JVM_PROPERTY + "\"";

        addPreferredStackOption(configFilePath, prefix, enableIpv6Property);
    }

    private void addPreferredStackOption(Path configFilePath, String prefix, String enableIpv6Property) {
        try {
            final List<String> lines = Files.readAllLines(configFilePath);
            try (final PrintWriter writer = new PrintWriter(new FileWriter(configFilePath.toFile()))) {
                boolean firstJavaOptsFound = false;
                boolean nextBlankLineFound = false;
                for (String l : lines) {
                    if (!firstJavaOptsFound && l.startsWith(prefix)) {
                        firstJavaOptsFound = true;
                    } else if (firstJavaOptsFound && !nextBlankLineFound && l.isEmpty()) {
                        nextBlankLineFound = true;
                        writer.println(l);
                        writer.println(enableIpv6Property);
                    }
                    writer.println(l);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void replacePreferredStackInScriptConfigs(Path serverPath, TaskPrinter printer, boolean appclientIncluded) throws ConfigFileNotFoundException {
        try {
            for (String config : REQUIRED_CONFIGS) {
                final Path configFilePath = getPath(serverPath, config);
                if (Files.exists(configFilePath)) {
                    printer.print("tasks.enable_ipv6.file", config);
                    doReplace(configFilePath, ENABLE_IPV4_JVM_PROPERTY, ENABLE_IPV6_JVM_PROPERTY);
                } else {
                    throw new ConfigFileNotFoundException("Required file " + config + " was not found in the installation", configFilePath);
                }
            }

            if (appclientIncluded) {
                for (String config : OPTIONAL_CONFIGS) {
                    final Path configFilePath = getPath(serverPath, config);
                    // appclient files are optional
                    if (Files.exists(configFilePath)) {
                        printer.print("tasks.enable_ipv6.file", config);
                        doReplace(configFilePath, ENABLE_IPV4_JVM_PROPERTY, ENABLE_IPV6_JVM_PROPERTY);
                    } else {
                        throw new ConfigFileNotFoundException("Required file " + config + " was not found in the installation", configFilePath);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void processStandaloneConfigs(Path serverPath, TaskPrinter printer) throws IOException {
        final String[] configs = {"standalone.xml", "standalone-ha.xml", "standalone-full.xml", "standalone-full-ha.xml"};
        for (String config: configs) {
            printer.print("tasks.enable_ipv6.file", config);
            final Path configFilePath = getPath(serverPath, config);
            doReplace(configFilePath, IPv4_LOCALHOST, LOCALHOST_TEXT);
        }
    }

    private void processDomainConfigs(Path serverPath, TaskPrinter printer) throws IOException {
        final String[] configs = {"domain.xml", "host.xml", DomainServer.HOST_PRIMARY_XML, DomainServer.HOST_SECONDARY_XML};
        final String propertyLineXml = "<property name=\"%s\" value=\"%s\"/>";

        for (String config: configs) {
            printer.print("tasks.enable_ipv6.file", config);
            final Path configFilePath = getPath(serverPath, config);
            final List<String> lines = Files.readAllLines(configFilePath);
            try (final PrintWriter writer = new PrintWriter(new FileWriter(configFilePath.toFile()))) {
                lines.stream()
                        .map(l -> l.contains(IPv4_LOCALHOST) ? l.replace(IPv4_LOCALHOST, LOCALHOST_TEXT) : l)
                        .flatMap(l -> {
                            if (config.equals("domain.xml") && l.contains(String.format(propertyLineXml, PREFER_IPV4_PROPERTY, "true"))) {
                                return Stream.of(l.replace(String.format(propertyLineXml, PREFER_IPV4_PROPERTY, "true"),
                                                String.format(propertyLineXml, PREFER_IPV4_PROPERTY, "false")),
                                        l.replace(String.format(propertyLineXml, PREFER_IPV4_PROPERTY, "true"),
                                                String.format(propertyLineXml, PREFER_IPV6_PROPERTY, "true")));
                            }
                            return Stream.of(l);
                        })
                        .forEach(l -> writer.println(l));
            }
        }
    }

    private void processAppclientConfig(Path serverPath, TaskPrinter printer) throws IOException {
        final Path configFilePath = getPath(serverPath,"appclient.xml");
        // appclient is an optional file - check if it exists
        if (Files.exists(configFilePath)) {
            printer.print("tasks.enable_ipv6.file", "appclient.xml");
            doReplace(configFilePath, IPv4_LOCALHOST, LOCALHOST_TEXT);
        } else {
            LoggerUtils.taskLog.debug("Skipping the appclient.xml file");
        }
    }

    private Path getPath(Path serverPath, String name) {
        final Path resolvedPath;
        if (name.equals("appclient.xml")) {
            resolvedPath = serverPath.resolve("appclient").resolve("configuration").resolve("appclient.xml");
        } else if (name.contains("standalone") && name.endsWith("xml")) {
            resolvedPath = serverPath.resolve("standalone").resolve("configuration").resolve(name);
        } else if (name.endsWith("xml")) {
            resolvedPath = serverPath.resolve("domain").resolve("configuration").resolve(name);
        } else {
            resolvedPath = serverPath.resolve("bin").resolve(name);
        }

        return resolvedPath;
    }

    private void doReplace(Path configFilePath, String text, String rep) throws IOException {
        final List<String> lines = Files.readAllLines(configFilePath);
        try (final PrintWriter writer = new PrintWriter(new FileWriter(configFilePath.toFile()))) {
            lines.stream()
                    .map(l -> l.contains(text) ? l.replace(text, rep) : l)
                    .forEach(l -> writer.println(l));
        }
    }

    private static class ConfigFileNotFoundException extends Exception {

        private final Path configFilePath;

        public ConfigFileNotFoundException(String message, Path configFilePath) {
            super(message);
            this.configFilePath = configFilePath;
        }

        public Path getMissingPath() {
            return configFilePath;
        }
    }
}
