/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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.auto;

import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
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.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringReader;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;

import org.aesh.readline.Prompt;
import org.aesh.readline.Readline;
import org.aesh.readline.ReadlineBuilder;
import org.aesh.readline.tty.terminal.TerminalConnection;
import org.aesh.terminal.tty.Signal;
import org.apache.commons.io.FileUtils;
import org.jboss.installer.core.InstallationData;
import org.jboss.installer.core.LanguageUtils;
import org.jboss.installer.core.LoggerUtils;
import org.jboss.installer.core.ValidationResult;
import org.jboss.installer.postinstall.PostInstallTask;
import org.jboss.installer.validators.PasswordSecurityValidator;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import static org.apache.commons.io.IOUtils.resourceToURL;
import static org.jboss.installer.core.LoggerUtils.taskLog;

public class InstallationDataSerializer implements AutoCloseable {

    public static final String INSTALLATION_DATA_TAG = "installation-data";
    public static final String POST_INSTALL_CONFIGS_TAG = "post-install-configs";
    public static final String ADMIN_USER_TAG = "admin-user";
    public static final String TARGET_DIRECTORY_TAG = "target-directory";
    public static final String POST_INSTALL_TASK_TAG = "post-install-task";
    public static final String POST_INSTALL_CONFIG_TAG = "post-install-config";
    public static final String USERNAME_ATTRIBUTE = "username";
    public static final String PASSWORD_ATTRIBUTE = "adminPassword";
    public static final String MAVEN_REPOSITORIES_TAG = "maven-repositories";
    public static final String MAVEN_REPOSITORY_TAG = "maven-repository";
    public static final String REPOSITORY_ID_ATTRIBUTE = "repository-id";
    public static final String REPOSITORY_URL_ATTRIBUTE = "repository-url";
    public static final String PREFIX = "";
    public static final String NS = "urn:jboss:installer:record:1.0.0";
    public static final String NAME_ATTRIBUTE = "name";
    public static final String POST_INSTALL_TASKS_TAG = "post-install-tasks";
    public static final String SELECTED_PACKAGES_TAG = "selected-packages";
    public static final String EXCLUDED_PACKAGES_TAG = "excluded-packages";
    public static final String PACKAGE_TAG = "package";
    public static final String NAME_ATTR = "name";
    public static final String VALIDATION_ERROR = "automated_installation.confirmation_password_match";
    public static final String YES = "automated_installation.password.continue.yes";
    public static final String NO = "automated_installation.password.continue.no";
    public static final String TRY_AGAIN = "automated_installation.password.try.again";
    private final LanguageUtils langUtils;
    private final PasswordSecurityValidator passwordSecurityValidator;
    private final HashMap<String, PostInstallTask> tasks = new HashMap<>();
    private final HashMap<Class<?>, String> configNames = new HashMap<>();

    private final InputConsole console;

    public InstallationDataSerializer(LanguageUtils langUtils) throws IOException {
        this(langUtils, new AeshInputConsole());
    }

    public InstallationDataSerializer(LanguageUtils langUtils, InputConsole console) {
        this.langUtils = langUtils;
        this.console = console;
        passwordSecurityValidator = new PasswordSecurityValidator(this.langUtils);

        // discover tasks and configs
        ServiceLoader.load(PostInstallTask.class).stream()
                .forEach(t -> {
                    final PostInstallTask task = t.get();
                    final String key = task.getSerializationName();
                    tasks.put(key, task);
                    if (task.getConfigClass() != null) {
                        configNames.put(task.getConfigClass(), key);
                    }
                });
    }

    @Deprecated
    public InstallationData deserialize(Path inputFile, Optional<Path> variablesFile) throws AutomaticInstallationParsingException, SAXParseException, MalformedURLException {
        return deserialize(inputFile.toUri().toURL(), variablesFile.map(f-> {
            try {
                return f.toUri().toURL();
            } catch (MalformedURLException e) {
                throw new RuntimeException(e);
            }
        }));
    }

    public InstallationData deserialize(URL inputFileUrl, Optional<URL> variablesFile) throws AutomaticInstallationParsingException, SAXParseException{
        validate(inputFileUrl);

        Map<String, String> variablesMap = loadProperties(inputFileUrl, variablesFile);

        XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
        XMLEventReader reader = null;
        try (InputStream fis = inputFileUrl.openStream()) {
            reader = xmlInputFactory.createXMLEventReader(fis);

            final InstallationData installationData = new InstallationData();
            while (reader.hasNext()) {
                final XMLEvent xmlEvent = reader.nextEvent();
                if (xmlEvent.isStartElement()) {
                    StartElement element = xmlEvent.asStartElement();

                    switch (element.getName().getLocalPart()) {
                        case MAVEN_REPOSITORIES_TAG:
                        case INSTALLATION_DATA_TAG:
                        case POST_INSTALL_CONFIGS_TAG:
                        case POST_INSTALL_TASKS_TAG:
                            // ignore
                            break;
                        case ADMIN_USER_TAG:
                            installationData.setAdminUsername(element.getAttributeByName(new QName(USERNAME_ATTRIBUTE)).getValue());
                            installationData.setPassword(getPassword(PASSWORD_ATTRIBUTE, "Admin password", variablesMap));
                            break;
                        case TARGET_DIRECTORY_TAG:
                            installationData.setTargetFolder(Paths.get(reader.getElementText()));
                            break;
                        case MAVEN_REPOSITORY_TAG:
                            MavenRepositoryItem mavenRepositoryItem = deserializeMavenRepository(element);
                            installationData.addMavenRepository(mavenRepositoryItem.getRepoId(), new URL(mavenRepositoryItem.getRepoUrl()));
                            break;
                        case POST_INSTALL_TASK_TAG:
                            installationData.addPostInstallTask(deserializePostInstallTask(element));
                            break;
                        case POST_INSTALL_CONFIG_TAG:
                            installationData.putConfig(deserializeConfig(element, reader, variablesMap));
                            break;
                        case EXCLUDED_PACKAGES_TAG:
                            installationData.setExcludedPackages(deserializePackages(element, reader));
                            break;
                        case SELECTED_PACKAGES_TAG:
                            installationData.setSelectedPackages(deserializePackages(element, reader));
                            break;
                        default:
                            throw unexpectedElement(element);
                    }
                }
            }

            return installationData;
        } catch (XMLStreamException | IOException e) {
            throw unableToParse(e);
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (XMLStreamException e) {
                    taskLog.debug(e);
                }
            }
        }
    }

    private void validate(URL inputFile) throws AutomaticInstallationParsingException, SAXParseException {
        try {
            inputFile.openConnection().getInputStream().close();
        } catch (IOException e) {
            throw new AutomaticInstallationParsingException(langUtils.getString("console.error.automatic_installer_file_not_found",  inputFile.toExternalForm()), e);
        }
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setNamespaceAware(true);
            DocumentBuilder parser = factory.newDocumentBuilder();
            Document document = parser.parse(inputFile.toExternalForm());

            SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);

            Schema schema = schemaFactory.newSchema(resourceToURL("/schema/install-record_1.0.0.xsd"));
            Validator validator = schema.newValidator();
            validator.validate(new DOMSource(document));
        } catch (SAXParseException e) {
            throw e;
        } catch (IOException | ParserConfigurationException | SAXException e) {
            throw new AutomaticInstallationParsingException("Validation of the XML failed", e);
        }
    }

    private Map<String, String> loadProperties(URL inputFile, Optional<URL> variablesFile) throws AutomaticInstallationParsingException {
        final URL propertiesFilePath = variablesFile.orElse(org.jboss.installer.core.FileUtils.asUrl(inputFile.toExternalForm() + ".variables"));
        try {
            propertiesFilePath.openConnection().getInputStream().close();
        } catch (IOException ex) {
            if (variablesFile.isPresent()) {
                throw new AutomaticInstallationParsingException(langUtils.getString("console.error.automatic_installer_file_not_found",  variablesFile.get().toExternalForm()), ex);
            } else {
                return Collections.emptyMap();
            }
        }

        Map<String, String> variablesMap = new HashMap<>();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(propertiesFilePath.openStream()))) {
            while (reader.ready()) {
                final String s = reader.readLine();
                final int splitAt = s.indexOf('=');
                final String key = s.substring(0, splitAt);
                final String value = s.substring(splitAt + 1);
                if (!value.isEmpty()) {
                    variablesMap.put(key, value);
                }
            }
        } catch (IOException e) {
            throw new AutomaticInstallationParsingException("Failed to read installation variables", e);
        }
        return variablesMap;
    }

    public static AutomaticInstallationParsingException unexpectedElement(StartElement element) throws AutomaticInstallationParsingException {
        return new AutomaticInstallationParsingException("Unrecognized element: " + element.getName().getLocalPart());
    }

    public void serialize(InstallationData installationData, Path outputFile) throws AutomaticInstallationParsingException {
        XMLOutputFactory output = XMLOutputFactory.newInstance();
        XMLEventFactory eventFactory = XMLEventFactory.newInstance();
        XMLEventWriter writer = null;
        Set<String> variables = new HashSet<>();

        try (ByteArrayOutputStream content = new ByteArrayOutputStream()) {
            writer = output.createXMLEventWriter(content);

            writer.add(eventFactory.createStartDocument());
            writer.add(eventFactory.createStartElement(PREFIX, NS, INSTALLATION_DATA_TAG));
            writer.add(eventFactory.createAttribute("xmlns", NS));

            serializeAdminCredentials(installationData, eventFactory, writer, variables);

            serializeTargetDirectory(installationData, eventFactory, writer);

            Map<String, URL> mavenRepositoriesMap = installationData.getMavenRepositories();
            if (!mavenRepositoriesMap.isEmpty()) {
                writer.add(eventFactory.createStartElement(PREFIX, NS, MAVEN_REPOSITORIES_TAG));
                for (String repoId : mavenRepositoriesMap.keySet()) {
                    writer.add(serializeMavenRepository(eventFactory, repoId, mavenRepositoriesMap.get(repoId).toExternalForm()));
                }
                writer.add(eventFactory.createEndElement(PREFIX, NS, MAVEN_REPOSITORIES_TAG));
            }

            serializePackages(installationData, eventFactory, writer);

            if (!installationData.getPostInstallTasks().isEmpty()) {
                writer.add(eventFactory.createStartElement(PREFIX, NS, POST_INSTALL_TASKS_TAG));
                for (PostInstallTask postInstallTask : installationData.getPostInstallTasks()) {
                    writer.add(serializePostInstallTask(eventFactory, postInstallTask));
                }
                writer.add(eventFactory.createEndElement(PREFIX, NS, POST_INSTALL_TASKS_TAG));
            }

            if (!installationData.getConfigs().isEmpty()) {
                writer.add(eventFactory.createStartElement(PREFIX, NS, POST_INSTALL_CONFIGS_TAG));
                for (InstallationData.PostInstallConfig config : installationData.getConfigs().values()) {
                    serializeTaskConfig(eventFactory, writer, config, variables);
                }
                writer.add(eventFactory.createEndElement(PREFIX, NS, POST_INSTALL_CONFIGS_TAG));
            }

            // close XML tag and document
            writer.add(eventFactory.createEndElement(PREFIX, NS, INSTALLATION_DATA_TAG));
            writer.add(eventFactory.createEndDocument());

            writer.flush();

            printXmlToFile(outputFile, content);
            final Path variablesFile = outputFile.getParent().resolve(outputFile.getFileName().toString() + ".variables");
            printPropertiesToFile(variables, variablesFile);

            changeLineEndings(outputFile);
            changeLineEndings(variablesFile);
        } catch (IOException | XMLStreamException | TransformerException e) {
            throw new AutomaticInstallationParsingException("Failed to write installation record", e);
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (XMLStreamException e) {
                    // ignore
                }
            }
        }
    }

    @Override
    public void close() {
        try {
            console.close();
        } catch (IOException e) {
            System.err.println("Warning: an error when closing console session: " + e.getMessage());
            LoggerUtils.systemLog.warn("An error closing console session", e);
        }
    }

    private void changeLineEndings(Path file) throws IOException {
        final Path temp = Files.createTempFile("eap-installer-tmp", file.getFileName().toString());
        try {
            Files.copy(file, temp, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
            try (BufferedReader reader = Files.newBufferedReader(temp, StandardCharsets.UTF_8);
                 BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) {
                String line;
                while ((line = reader.readLine()) != null) {
                    writer.write(line);
                    writer.write("\n");
                }
            }
        } finally {
            FileUtils.deleteQuietly(temp.toFile());
        }
    }

    private String getPassword(String key, String name, Map<String, String> variablesMap) {
        if (variablesMap.containsKey(key)) {
            return variablesMap.get(key);
        }

        while (true) {
            console.printf("%s:%n", name);
            char[] chars = console.readPassword();
            final String password1 = new String(chars);

            console.printf("Confirm %s:%n", name);
            chars = console.readPassword();
            final String password2 = new String(chars);
            final ValidationResult securityResult = passwordSecurityValidator.passwordSecurityCheck(name, password1);
            final ValidationResult validationWarningResult = passwordSecurityValidator.passwordValidationWarning(name, password1);

            if (password1.equals(password2)) {
                if (password1.isEmpty()) {
                    console.printf(langUtils.getString("admin_creation_screen.empty_password") + ".%n");
                } else if (!securityResult.getResult().equals(ValidationResult.Result.OK)) {
                    console.printf(securityResult.getMessage() + '\n');
                    console.printf(validationWarningResult.getMessage() + '\n');
                    console.printf("Enter: " + langUtils.getString(YES) + "/" + langUtils.getString(NO) + '\n');
                    String str = console.readLine();
                    while (true) {
                        if (str.equalsIgnoreCase(langUtils.getString(YES))) {
                            return password1;
                        } else if (str.equalsIgnoreCase(langUtils.getString(NO))) {
                            console.printf(langUtils.getString(TRY_AGAIN) + '\n');
                            break;
                        } else {
                            console.printf("Enter: " + langUtils.getString(YES) + "/" + langUtils.getString(NO) + '\n');
                            str = console.readLine();
                        }
                    }
                } else {
                    return password1;
                }
            } else {
                console.printf(langUtils.getString(VALIDATION_ERROR) + ".%n");
            }
        }
    }

    private void serializeTaskConfig(XMLEventFactory eventFactory,
                                     XMLEventWriter writer,
                                     InstallationData.PostInstallConfig config,
                                     Set<String> variables) throws XMLStreamException, AutomaticInstallationParsingException {
        writer.add(eventFactory.createStartElement(InstallationDataSerializer.PREFIX, InstallationDataSerializer.NS, POST_INSTALL_CONFIG_TAG));
        if (!configNames.containsKey(config.getClass())) {
            throw new AutomaticInstallationParsingException("Can't match config: " + config.getClass() + " to any task");
        }
        final String name = configNames.get(config.getClass());
        writer.add(eventFactory.createAttribute(InstallationDataSerializer.NAME_ATTRIBUTE, name));
        writer.add(config.serialize(eventFactory, variables));
        writer.add(eventFactory.createEndElement(InstallationDataSerializer.PREFIX, InstallationDataSerializer.NS, POST_INSTALL_CONFIG_TAG));
    }

    private void serializeTargetDirectory(InstallationData installationData,
                                          XMLEventFactory eventFactory,
                                          XMLEventWriter writer) throws XMLStreamException {
        final Path targetFolder = installationData.getTargetFolder();
        if (targetFolder != null) {
            writer.add(eventFactory.createStartElement(PREFIX, NS, TARGET_DIRECTORY_TAG));
            writer.add(eventFactory.createCharacters(targetFolder.toString()));
            writer.add(eventFactory.createEndElement(PREFIX, NS, TARGET_DIRECTORY_TAG));
        }
    }

    private XMLEventReader serializeMavenRepository(XMLEventFactory eventFactory, String repoId, String repoUrl) throws XMLStreamException {
        final ArrayList<XMLEvent> events = new ArrayList<>();
        events.add(eventFactory.createStartElement(PREFIX, NS, MAVEN_REPOSITORY_TAG));
        events.add(eventFactory.createAttribute(REPOSITORY_ID_ATTRIBUTE, repoId));
        events.add(eventFactory.createAttribute(REPOSITORY_URL_ATTRIBUTE, repoUrl));
        events.add(eventFactory.createEndElement(PREFIX, NS, MAVEN_REPOSITORY_TAG));

        return new ListXMLEventReader(events);
    }

    private void serializePackages(InstallationData installationData,
                                   XMLEventFactory eventFactory,
                                   XMLEventWriter writer) throws XMLStreamException {
        if (!installationData.getExcludedPackages().isEmpty()) {
            writer.add(eventFactory.createStartElement(PREFIX, NS, EXCLUDED_PACKAGES_TAG));
            for (String pkg : installationData.getExcludedPackages()) {
                writer.add(eventFactory.createStartElement(PREFIX, NS, PACKAGE_TAG));
                writer.add(eventFactory.createAttribute(NAME_ATTR, pkg));
                writer.add(eventFactory.createEndElement(PREFIX, NS, PACKAGE_TAG));
            }
            writer.add(eventFactory.createEndElement(PREFIX, NS, EXCLUDED_PACKAGES_TAG));
        }
        if (!installationData.getSelectedPackages().isEmpty()) {
            writer.add(eventFactory.createStartElement(PREFIX, NS, SELECTED_PACKAGES_TAG));
            for (String pkg : installationData.getSelectedPackages()) {
                writer.add(eventFactory.createStartElement(PREFIX, NS, PACKAGE_TAG));
                writer.add(eventFactory.createAttribute(NAME_ATTR, pkg));
                writer.add(eventFactory.createEndElement(PREFIX, NS, PACKAGE_TAG));
            }
            writer.add(eventFactory.createEndElement(PREFIX, NS, SELECTED_PACKAGES_TAG));
        }
    }

    private void serializeAdminCredentials(InstallationData installationData,
                                           XMLEventFactory eventFactory,
                                           XMLEventWriter writer,
                                           Set<String> variables) throws XMLStreamException {
        final String adminUsername = installationData.getAdminUsername();
        final String adminPassword = installationData.getPassword();
        if (adminUsername != null && adminPassword != null) {
            writer.add(eventFactory.createStartElement(PREFIX, NS, ADMIN_USER_TAG));
            writer.add(eventFactory.createAttribute(USERNAME_ATTRIBUTE, adminUsername));
            variables.add(PASSWORD_ATTRIBUTE);
            writer.add(eventFactory.createEndElement(PREFIX, NS, ADMIN_USER_TAG));
        }
    }

    private XMLEventReader serializePostInstallTask(XMLEventFactory eventFactory, PostInstallTask postInstallTask) {
        final ArrayList<XMLEvent> events = new ArrayList<>();
        events.add(eventFactory.createStartElement(PREFIX, NS, POST_INSTALL_TASK_TAG));
        events.add(eventFactory.createAttribute(NAME_ATTRIBUTE, postInstallTask.getSerializationName()));
        events.add(eventFactory.createEndElement(PREFIX, NS, POST_INSTALL_TASK_TAG));

        return new ListXMLEventReader(events);
    }

    private void printXmlToFile(Path outputFile, ByteArrayOutputStream content) throws IOException, TransformerException {
        TransformerFactory transformerFactory = TransformerFactory.newInstance();

        Transformer transformer = transformerFactory.newTransformer();
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        transformer.setOutputProperty(OutputKeys.STANDALONE, "yes");

        String xml = new String(content.toByteArray(), StandardCharsets.UTF_8);
        StreamSource source = new StreamSource(new StringReader(xml));
        try (FileWriter fw = new FileWriter(outputFile.toFile())) {
            transformer.transform(source, new StreamResult(fw));
        }
    }

    private void printPropertiesToFile(Set<String> variables, Path propertiesFile) throws AutomaticInstallationParsingException {
        try (PrintWriter writer = new PrintWriter(new FileWriter(propertiesFile.toFile()))) {
            for (String variable : variables) {
                writer.println(variable + "=");
            }
        } catch (IOException e) {
            throw new AutomaticInstallationParsingException("Failed to print installation variables", e);
        }
    }

    private MavenRepositoryItem deserializeMavenRepository(StartElement element) throws AutomaticInstallationParsingException {
        final String repoId = element.getAttributeByName(new QName(REPOSITORY_ID_ATTRIBUTE)).getValue();
        final String repoUrl = element.getAttributeByName(new QName(REPOSITORY_URL_ATTRIBUTE)).getValue();
        return new MavenRepositoryItem(repoId, repoUrl);
    }

    private PostInstallTask deserializePostInstallTask(StartElement element) throws AutomaticInstallationParsingException {
        final String name = element.getAttributeByName(new QName(NAME_ATTRIBUTE)).getValue();

        if (!tasks.containsKey(name)) {
            throw new AutomaticInstallationParsingException("Unknown post install operation requested: " + name);
        } else {
            return tasks.get(name);
        }
    }

    private InstallationData.PostInstallConfig deserializeConfig(StartElement element, XMLEventReader reader, Map<String, String> variableMap)
            throws AutomaticInstallationParsingException {
        final String name = element.getAttributeByName(new QName(NAME_ATTRIBUTE)).getValue();

        if (!tasks.containsKey(name)) {
            throw new AutomaticInstallationParsingException("Unknown post install operation requested: " + name);
        }
        final Class<? extends InstallationData.PostInstallConfig> configClass = tasks.get(name).getConfigClass();

        try {
            final InstallationData.PostInstallConfig config = configClass.getDeclaredConstructor().newInstance();
            final Method deserialize = configClass.getMethod("deserialize", XMLEventReader.class, BiFunction.class);
            deserialize.invoke(config, new ConfigElementXmlEventReader(reader),
                    (BiFunction<String, String, String>) (key, prompt) -> getPassword(key, prompt, variableMap));
            return config;
        } catch (ReflectiveOperationException e) {
            throw new AutomaticInstallationParsingException("Failed to read configuration task: " + configClass, e);
        }
    }

    private List<String> deserializePackages(StartElement element, XMLEventReader reader) throws XMLStreamException {
        List<String> res = new ArrayList<>();
        while (reader.hasNext()) {
            if (reader.peek().isEndElement() && reader.peek().asEndElement().getName().getLocalPart().equals(element.getName().getLocalPart())) {
                return res;
            }
            final XMLEvent xmlEvent = reader.nextEvent();
            if (xmlEvent.isStartElement()) {
                final StartElement elem = xmlEvent.asStartElement();
                if (elem.getName().getLocalPart().equals(PACKAGE_TAG)) {
                    res.add(elem.getAttributeByName(new QName(NAME_ATTR)).getValue());
                } else {
                    throw new IllegalStateException("Unexpected element: " + elem.getName());
                }
            }
        }
        return res;
    }

    public static AutomaticInstallationParsingException unableToParse(Exception e) throws AutomaticInstallationParsingException {
        return new AutomaticInstallationParsingException("Failed to parse automatic installation configuration", e);
    }

    public static AutomaticInstallationParsingException unableToParse() throws AutomaticInstallationParsingException {
        return unableToParse(null);
    }

    private static class MavenRepositoryItem {
        private final String repoId;
        private final String repoUrl;

        MavenRepositoryItem(String repoId, String repoUrl) {
            this.repoId = repoId;
            this.repoUrl = repoUrl;
        }

        public String getRepoId() {
            return repoId;
        }

        public String getRepoUrl() {
            return repoUrl;
        }
    }

    public interface InputConsole extends Closeable {

        char[] readPassword();

        void printf(String txt, String... args);

        String readLine();
    }

    private static class AeshInputConsole implements InputConsole {
        private final TerminalConnection terminalConnection = new TerminalConnection();
        final Readline readline = ReadlineBuilder.builder().enableHistory(false).build();

        private AeshInputConsole() throws IOException {
            terminalConnection.setSignalHandler(signal->{
                if (signal == Signal.INT) {
                    terminalConnection.close();
                    System.exit(-1);
                }
            });
        }

        @Override
        public char[] readPassword() {
            final AtomicReference<String> password = new AtomicReference<>();
            readline.readline(terminalConnection, new Prompt("", '*'), input->{
                password.set(input.trim());
                terminalConnection.stopReading();
            });
            terminalConnection.openBlocking();
            return password.get().toCharArray();
        }

        @Override
        public void printf(String txt, String... args) {
            if ( args == null || args.length == 0) {
                terminalConnection.write(String.format(txt));
            } else {
                terminalConnection.write(String.format(txt, (Object[]) args));
            }
        }

        @Override
        public String readLine() {
            final AtomicReference<String> line = new AtomicReference<>();
            readline.readline(terminalConnection, new Prompt(""), input->{
                line.set(input);
                terminalConnection.stopReading();
            });
            terminalConnection.openBlocking();
            return line.get();
        }

        @Override
        public void close() throws IOException {
            terminalConnection.close();
        }
    }
}
