/*
 * 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.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.jboss.installer.auto.AutomaticInstallationParsingException;
import org.jboss.installer.core.FileUtils;
import org.jboss.installer.core.FlatListPostInstallConfig;
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.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.function.BiFunction;

import static org.jboss.installer.core.FileUtils.isFileEmpty;
import static org.jboss.installer.core.FileUtils.loadXmlFromFile;
import static org.jboss.installer.core.FileUtils.replaceLines;
import static org.jboss.installer.core.FileUtils.writeToFile;
import static org.jboss.installer.core.LoggerUtils.taskLog;
import static org.jboss.installer.screens.QuickstartsMavenRepositorySetupScreen.DEFAULT_MAVEN_REPOSITORY;
import static org.jboss.installer.validators.MavenSettingsPathValidator.SETTINGS_XML;

@AutoService(PostInstallTask.class)
public class QuickstartsTask implements SimplePostInstallTask {

    public static final String TASK_NAME_KEY = "post_install.task.quickstarts.name";
    public static final String PROFILES = "profiles";
    public static final String ACTIVE_PROFILES = "activeProfiles";
    public static final String JBOSS_GA = "jboss-ga";
    public static final String PROFILE_ID_EXPRESSION = "/settings/profiles/profile/id";
    protected static final String ACTIVE_PROFILE = "activeProfile";
    protected static final String PROFILE = "profile";
    protected static final String ID = "id";
    protected static final String REPOSITORIES = "repositories";
    protected static final String REPOSITORY = "repository";
    protected static final String URL = "url";
    protected static final String PLUGIN_REPOSITORIES = "pluginRepositories";
    protected static final String PLUGIN_REPOSITORY = "pluginRepository";

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

    @Override
    public String getSerializationName() {
        return "add-quickstarts";
    }

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

    @Override
    public boolean applyToInstallation(InstallationData data, TaskPrinter printer) {
        Config config = data.getConfig(QuickstartsTask.Config.class);
        try {
            printer.print("tasks.quickstarts.started");
            checkoutQuickstartsFromGithub(printer, config.getQuickstartsTargetFolder(), config.getQuickstartsGithubRepo(), config.getQuickstartsGithubBranch());
            printer.print("tasks.quickstarts.finished");

            updateSettingsXmlFile(config, printer);
        } catch (XPathExpressionException | ParserConfigurationException | TransformerException | IOException | SAXException e) {
            LoggerUtils.taskLog.error("Failed to perform operation", e);
            printer.print("tasks.quickstarts.settings.failed");
            printer.print(e);
            return false;
        }
        return true;
    }

    protected void updateSettingsXmlFile(Config config, TaskPrinter printer)
            throws XPathExpressionException, IOException, ParserConfigurationException, TransformerException, SAXException {
        File settingsFile = new File(config.getQuickstartsSettingsPath());
        if (settingsFile.isDirectory()) {
            settingsFile = new File(settingsFile, "settings.xml");
        }
        final String jbossId = getJbossId(settingsFile);
        final String defaultSettingsXml = getDefaultSettingsXml(getMavenRepositoryUrl(config), jbossId);
        if (!settingsFile.exists() || isFileEmpty(settingsFile)) {
            if (!settingsFile.getParentFile().exists()) {
                Files.createDirectories(settingsFile.getParentFile().toPath());
            }
            writeToFile(settingsFile, defaultSettingsXml);
            printer.print("tasks.quickstarts.settings.created", settingsFile.toString());
        } else {
            final String input = updateSettingsFileInput(settingsFile, defaultSettingsXml);
            if (input != null) {
                writeToFile(settingsFile, input);
            }
        }
        printer.print("tasks.quickstarts.profile.created", jbossId, settingsFile.toString());
    }

    private static URL getMavenRepositoryUrl(Config config) {
        // remove multiple trailing slashes
        final String originalValue = config.getQuickstartsRepository().replaceAll("/+$", "/");

        try {
            if (FileUtils.isUrl(originalValue)) {
                return new URL(originalValue);
            } else {
                final URL url = Path.of(originalValue).toAbsolutePath().toUri().toURL();

                // remove the trailing slash if needed to be consistent with user input
                final String urlAsString = url.toExternalForm();
                if (urlAsString.endsWith("/") && !config.quickstartsRepository.endsWith(File.separator)) {
                    return new URL(urlAsString.substring(0, urlAsString.length()-1));
                } else {
                    return url;
                }
            }
        } catch (MalformedURLException e) {
            throw new RuntimeException("The quickstart path cannot be transformed to URL", e);
        }
    }

    private String getDefaultSettingsXml(URL mavenRepo, String jbossId) throws IOException {
        InputStream stream = this.getClass().getClassLoader().getResourceAsStream(SETTINGS_XML);
        HashMap<String, String> replacements = new HashMap<>();
        replacements.put("${REPOSITORY}", mavenRepo.toExternalForm());
        replacements.put("${ID}", jbossId);

        try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))){
            return replaceLines(replacements, reader);
        }
    }

    private boolean settingsContainDefaultJBossId(File file)
            throws ParserConfigurationException, IOException, SAXException, XPathExpressionException {
        NodeList targetList;
        Document doc = loadXmlFromFile(file);
        XPathFactory xpathfactory = XPathFactory.newInstance();
        XPath xPath = xpathfactory.newXPath();
        targetList = (NodeList) xPath.compile(QuickstartsTask.PROFILE_ID_EXPRESSION).evaluate(
                doc, XPathConstants.NODESET);
        for (int i = 0; i < targetList.getLength(); i++) {
            if (targetList.item(i).getTextContent().equals(QuickstartsTask.JBOSS_GA)) {
                return true;
            }
        }
        return false;
    }

    private String updateSettingsFileInput(File settingsFile, String defaultSettingsXml)
            throws TransformerException, ParserConfigurationException, IOException, SAXException {
        Document inputDoc = FileUtils.loadXmlFromString(defaultSettingsXml);
        Document settingsDoc = FileUtils.loadXmlFromFile(settingsFile);

        if (!isRepositoryActive(settingsDoc, DEFAULT_MAVEN_REPOSITORY)) {
            appendInputNodeToSettingsDoc(settingsDoc, PROFILES, inputDoc);
            appendInputNodeToSettingsDoc(settingsDoc, ACTIVE_PROFILES, inputDoc);
            return getXmlDocAsString(settingsDoc);
        } else {
            return null;
        }
    }

    private void appendInputNodeToSettingsDoc(Document settingsDoc, String nodeName, Document inputDoc) {
        Node originalNode = settingsDoc.getElementsByTagName(nodeName).item(0);
        Node nodeToAppend = inputDoc.getElementsByTagName(nodeName).item(0);
        if (originalNode == null) {
            originalNode = settingsDoc.getDocumentElement();
            originalNode.appendChild(settingsDoc.importNode(nodeToAppend.cloneNode(true), true));
            return;
        }
        nodeToAppend = nodeToAppend.getFirstChild().getNextSibling();
        originalNode.appendChild(settingsDoc.importNode(nodeToAppend.cloneNode(true), true));
    }

    private String getXmlDocAsString(Document doc) throws TransformerException {
        TransformerFactory tf = TransformerFactory.newInstance();
        tf.setAttribute("indent-number", 4);
        // need to apply the XSLT to remove additional empty spaces
        final InputStream xslt = this.getClass().getClassLoader().getResourceAsStream("remove_whitespaces.xslt");
        Transformer t = tf.newTransformer(new StreamSource(xslt));
        t.setOutputProperty(OutputKeys.INDENT, "yes");
        StringWriter sw = new StringWriter();
        t.transform(new DOMSource(doc), new StreamResult(sw));
        return sw.toString();
    }

    private String getJbossId(File settingsFile) throws XPathExpressionException, IOException, ParserConfigurationException, SAXException {
        if (settingsFile.exists() && !isFileEmpty(settingsFile)
                && settingsContainDefaultJBossId(settingsFile)) {
            return JBOSS_GA + "-" + Math.abs(new Random().nextInt());
        }
        return JBOSS_GA;
    }

    private void checkoutQuickstartsFromGithub(TaskPrinter printer, String quickStartsBaseTarget, String quickStartsGithubRepo,
                                               String quickStartsGithubBranch) throws IOException {
        try {
            taskLog.info("Cloning QuickStarts from repository " + quickStartsGithubRepo + " Branch " + quickStartsGithubBranch + " into " + quickStartsBaseTarget);
            printer.print("tasks.quickstarts.details", quickStartsGithubRepo, quickStartsGithubBranch, quickStartsBaseTarget);
            final Path checkoutDirectory = Paths.get(quickStartsBaseTarget);
            Git.cloneRepository()
                    .setURI(quickStartsGithubRepo)
                    .setDirectory(checkoutDirectory.toFile())
                    .setBranch(quickStartsGithubBranch)
                    .setCloneAllBranches(false)
                    .call()
                    .close();
            taskLog.info("Cloning Completed.");
            org.apache.commons.io.FileUtils.deleteQuietly(checkoutDirectory.resolve(".git").toFile());
        } catch (JGitInternalException | GitAPIException e) {
            taskLog.error(e.getMessage(), e);
            throw new IOException("Failed to clone from repository " + quickStartsGithubRepo + " Branch " + quickStartsGithubBranch + " due to " + e.getMessage());
        }
    }

    /**
     * Find the missing urls based on the profiles you want to add.
     *
     * @param doc  Your xml document
     * @param expectedUrl Url to look for
     * @return List of urls that are missing from the settings.xml file
     */
    private static boolean isRepositoryActive(Document doc, String expectedUrl) {
        //Get all active profiles
        final ArrayList<String> activeProfiles = findActiveProfiles(doc);

        //Get all profiles
        final NodeList profileList = doc.getElementsByTagName(PROFILE);
        for (int i = 0; i < profileList.getLength(); i++) {
            final Element p = (Element) profileList.item(i);
            final String profile = p.getElementsByTagName(ID).item(0).getTextContent();

            if (profileProvidesRepo(expectedUrl, p) &&
                    profileProvidesPluginRepo(expectedUrl, p, activeProfiles, profile) &&
                    activeProfiles.contains(profile)) {
                return true;
            }
        }
        return false;
    }

    private static boolean profileProvidesPluginRepo(String expectedUrl, Element p, ArrayList<String> activeProfiles, String profile) {
        final NodeList pluginRepoList = p.getElementsByTagName(PLUGIN_REPOSITORIES);
        final Element pluginRepoElement = ((Element) pluginRepoList.item(0));
        NodeList pluginList = (pluginRepoElement != null) ? pluginRepoElement.getElementsByTagName(PLUGIN_REPOSITORY) : null;

        //If the plugin repository contains the same url of interest remove url of interest from the urls List
        //It's possible for p.getElementsByTagName(...) to return null, so we can't immediately do .item(0) on it
        if (pluginList != null) {
            for (int j = 0; j < pluginList.getLength(); j++) {
                Element pluginElem = (Element) pluginList.item(j);
                String pluginRepo = pluginElem.getElementsByTagName(URL).item(0).getTextContent();
                if (expectedUrl.equals(pluginRepo)) {
                    return true;
                }
            }
        }
        return false;
    }

    private static boolean profileProvidesRepo(String expectedUrl, Element p) {
        final NodeList reposList = p.getElementsByTagName(REPOSITORIES);
        final Element repoElement = (Element) reposList.item(0);
        final NodeList repoList = (repoElement != null) ? repoElement.getElementsByTagName(REPOSITORY) : null;

        //Save the repo url if its a url you are looking for
        if (repoList != null) {
            for (int j = 0; j < repoList.getLength(); j++) {
                Element repoElem = (Element) repoList.item(j);
                String repo = repoElem.getElementsByTagName(URL).item(0).getTextContent();
                if (expectedUrl.equals(repo)) {
                    return true;
                }
            }
        }
        return false;
    }

    private static ArrayList<String> findActiveProfiles(Document doc) {
        final ArrayList<String> activeProfiles = new ArrayList<>();
        final NodeList activeProfileList = doc.getElementsByTagName(ACTIVE_PROFILES);

        for (int i = 0; i < activeProfileList.getLength(); i++) {
            final Element p = (Element) activeProfileList.item(i);
            final NodeList activeList = p.getElementsByTagName(ACTIVE_PROFILE);

            for (int j = 0; j < activeList.getLength(); j++) {
                final Element a = (Element) activeList.item(j);
                activeProfiles.add(a.getTextContent());
            }
        }
        return activeProfiles;
    }

    public static class Config extends FlatListPostInstallConfig {

        private String quickstartsTargetFolder;
        private String quickstartsRepository;
        private String quickstartsSettingsPath;
        private String quickstartsGithubRepo;
        private String quickstartsGithubBranch;

        public String getQuickstartsTargetFolder() {
            return quickstartsTargetFolder;
        }

        public void setQuickstartsTargetFolder(String quickstartsTargetFolder) {
            this.quickstartsTargetFolder = quickstartsTargetFolder;
        }

        public String getQuickstartsRepository() {
            return quickstartsRepository;
        }

        public void setQuickstartsRepository(String quickstartsRepository) {
            this.quickstartsRepository = quickstartsRepository;
        }

        public String getQuickstartsSettingsPath() {
            return quickstartsSettingsPath;
        }

        public void setQuickstartsSettingsPath(String quickstartsSettingsPath) {
            this.quickstartsSettingsPath = quickstartsSettingsPath;
        }

        public String getQuickstartsGithubRepo() {
            return quickstartsGithubRepo;
        }

        public void setQuickstartsGithubRepo(String quickstartsGithubRepo) {
            this.quickstartsGithubRepo = quickstartsGithubRepo;
        }

        public String getQuickstartsGithubBranch() {
            return quickstartsGithubBranch;
        }

        public void setQuickstartsGithubBranch(String quickstartsGithubBranch) {
            this.quickstartsGithubBranch = quickstartsGithubBranch;
        }

        @Override
        protected Map<String, String> listAttributes() {
            final HashMap<String, String> attrs = new HashMap<>();
            attrs.put("quickstartsTargetFolder", quickstartsTargetFolder);
            attrs.put("quickstartsRepository", quickstartsRepository);
            attrs.put("quickstartsSettingsPath", quickstartsSettingsPath);
            attrs.put("quickstartsGithubRepo", quickstartsGithubRepo);
            attrs.put("quickstartsGithubBranch", quickstartsGithubBranch);
            return attrs;
        }

        @Override
        protected Set<String> listVariables() {
            return Collections.emptySet();
        }

        @Override
        protected void acceptAttributes(Map<String, String> attributes, BiFunction<String, String, String> variableResolver) throws AutomaticInstallationParsingException {
            quickstartsRepository = attributes.getOrDefault("quickstartsRepository", null);
            quickstartsTargetFolder = attributes.getOrDefault("quickstartsTargetFolder", null);
            quickstartsSettingsPath = attributes.getOrDefault("quickstartsSettingsPath", null);
            quickstartsGithubRepo = attributes.getOrDefault("quickstartsGithubRepo", null);
            quickstartsGithubBranch = attributes.getOrDefault("quickstartsGithubBranch", null);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Config config = (Config) o;
            return Objects.equals(quickstartsTargetFolder, config.quickstartsTargetFolder) && Objects.equals(quickstartsRepository, config.quickstartsRepository) && Objects.equals(quickstartsSettingsPath, config.quickstartsSettingsPath) && Objects.equals(quickstartsGithubRepo, config.quickstartsGithubRepo) && Objects.equals(quickstartsGithubBranch, config.quickstartsGithubBranch);
        }

        @Override
        public int hashCode() {
            return Objects.hash(quickstartsTargetFolder, quickstartsRepository, quickstartsSettingsPath, quickstartsGithubRepo, quickstartsGithubBranch);
        }

        @Override
        public String toString() {
            return "Config{" +
                    "quickstartsTargetFolder='" + quickstartsTargetFolder + '\'' +
                    ", quickstartsRepository='" + quickstartsRepository + '\'' +
                    ", quickstartsSettingsPath='" + quickstartsSettingsPath + '\'' +
                    ", quickstartsGithubRepo='" + quickstartsGithubRepo + '\'' +
                    ", quickstartsGithubBranch='" + quickstartsGithubBranch + '\'' +
                    '}';
        }
    }
}