/*
 * 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.task;

import com.google.auto.service.AutoService;
import org.jboss.as.controller.OperationFailedException;
import org.jboss.as.controller.PathAddress;
import org.jboss.dmr.ModelNode;
import org.jboss.installer.auto.AutomaticInstallationParsingException;
import org.jboss.installer.core.DatabaseDriver;
import org.jboss.installer.core.FlatListPostInstallConfig;
import org.jboss.installer.core.InstallationData;
import org.jboss.installer.postinstall.CliPostInstallTask;
import org.jboss.installer.postinstall.PostInstallTask;
import org.jboss.installer.postinstall.TaskPrinter;
import org.jboss.installer.postinstall.server.DomainServer;
import org.jboss.installer.postinstall.server.EmbeddedServer;
import org.jboss.installer.postinstall.server.StandaloneServer;
import org.jboss.installer.postinstall.task.utils.CredentialStoreUtil;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.stream.Collectors;

import static org.jboss.as.controller.operations.common.Util.createEmptyOperation;
import static org.jboss.installer.core.LoggerUtils.taskLog;

@AutoService(PostInstallTask.class)
public class DatasourceTask implements CliPostInstallTask {

    public static final String TASK_NAME = "post_install.task.datasource.name";
    public static final String XA_PROPERTY_PREFIX = "xa-property-";
    public static final String XA_RECOVERY_PASSWORD_ALIAS = "xa-recovery-password";
    public static final String DATASOURCE_PASSWORD_ALIAS = "datasource-password";

    @Override
    public boolean applyToStandalone(InstallationData data, StandaloneServer server, TaskPrinter printer) {
        return doApply(data, server, printer);
    }

    @Override
    public boolean applyToDomain(InstallationData data, DomainServer server, TaskPrinter printer) {
        if (!server.currentConfiguration().equals("host.xml")) {
            return true;
        }
        return doApply(data, server, printer);
    }

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

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

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

    private boolean doApply(InstallationData data, EmbeddedServer server, TaskPrinter printer) {
        final Config datasourceConfig = data.getConfig(Config.class);
        final JDBCDriverTask.Config jdbcConfig = data.getConfig(JDBCDriverTask.Config.class);
        printer.print("tasks.datasource.started", datasourceConfig.getDsName(), server.currentConfiguration());

        final Optional<CredentialStoreInstallTask.Config> storeConfig = Optional.ofNullable(data.getConfig(CredentialStoreInstallTask.Config.class));
        taskLog.info(String.format("Starting applying datasource changes to %s", server.currentConfiguration()));
        try {
            taskLog.debug("Applying Datasource change");

            if (storeConfig.isPresent() && !server.aliasExists(DATASOURCE_PASSWORD_ALIAS, storeConfig.get().getStoreName())) {
                if (datasourceConfig.dsPassword != null) {
                    printer.print("tasks.datasource.store_password", storeConfig.get().getStoreName(), DATASOURCE_PASSWORD_ALIAS);
                    server.encryptPassword(DATASOURCE_PASSWORD_ALIAS, storeConfig.get().getStoreName(), datasourceConfig.dsPassword);
                }
                if (datasourceConfig.xaPassword != null) {
                    printer.print("tasks.datasource.store_password", storeConfig.get().getStoreName(), XA_RECOVERY_PASSWORD_ALIAS);
                    server.encryptPassword(XA_RECOVERY_PASSWORD_ALIAS, storeConfig.get().getStoreName(), datasourceConfig.xaPassword);
                }
            }

            List<ModelNode> operations = new ArrayList<>();
            operations.add(getDatasourceOpNode(datasourceConfig, jdbcConfig.getDatabaseDriver(), storeConfig));
            operations.addAll(addXaProperties(datasourceConfig));
            for (int i = 0; i < operations.size(); i++) {
                ModelNode operation = operations.get(i);
                if (server.isDomain()) {
                    ((DomainServer) server).executeOnProfiles(operation, "Setting datasource - step " + i);
                } else {
                    server.execute(operation, "Setting datasource - step " + i);
                }
            }

        } catch (OperationFailedException e) {
            taskLog.error("CLI operation failed", e);
            printer.print("tasks.datasource.failed");
            printer.print(e);
            return false;
        }
        printer.print("tasks.datasource.finished");
        return true;
    }

    private ModelNode getDatasourceOpNode(Config config, DatabaseDriver driver, Optional<CredentialStoreInstallTask.Config> storeConfig) {
        boolean isXa = config.xaProperties != null;
        final ModelNode modelNode;
        if (isXa) {
            modelNode = createEmptyOperation("add",
                    PathAddress.pathAddress("subsystem", "datasources").append("xa-data-source", config.dsName));
            modelNode.get("recovery-username").set(config.xaUsername);
            CredentialStoreUtil.addCredReference(modelNode, storeConfig, XA_RECOVERY_PASSWORD_ALIAS, config.getXaPassword(), "recovery-credential-reference");
        } else {
            modelNode = createEmptyOperation("add",
                    PathAddress.pathAddress("subsystem", "datasources").append("data-source", config.dsName));
            modelNode.get("connection-url").set(config.connectionUrl);
        }

        if (config.securityDomain == null) {
            modelNode.get("user-name").set(config.dsUsername);
            CredentialStoreUtil.addCredReference(modelNode, storeConfig, DATASOURCE_PASSWORD_ALIAS, config.getDsPassword(), "credential-reference");
        } else {
            modelNode.get("security-domain").set(config.securityDomain);
        }

        modelNode.get("driver-name").set(driver.getJdbcName());
        modelNode.get("jndi-name").set(config.jndiName);
        modelNode.get("min-pool-size").set(config.minPool);
        modelNode.get("max-pool-size").set(config.maxPool);

        DatabaseDriver.setUniqueDatasourceElements(modelNode, driver);
        return modelNode;
    }

    private List<ModelNode> addXaProperties(Config config) {
        List<ModelNode> operations = new ArrayList<>();
        if (config.xaProperties == null) {
            return operations;
        }
        for (DatasourceTask.XaProperty property : config.xaProperties) {
            ModelNode addPropertyOp = createEmptyOperation("add",
                    PathAddress.pathAddress("subsystem", "datasources").append("xa-data-source", config.dsName).append("xa-datasource-properties", property.getKey()));
            addPropertyOp.get("value").set(property.getValue());
            operations.add(addPropertyOp);
        }
        return operations;
    }

    public static class Config extends FlatListPostInstallConfig {

        public static final String DS_PASSWORD_PROPERTY_NAME = "datasource.password";
        public static final String XA_RECOVERY_PASSWORD_PROPERTY_NAME = "xa_recovery.password";
        private String dsName;
        private String jndiName;
        private String connectionUrl;
        private String minPool;
        private String maxPool;
        private String securityDomain;
        private String dsUsername;
        private String dsPassword;
        private String xaUsername;
        private String xaPassword;
        private List<XaProperty> xaProperties;

        public Config() {}

        public Config(String dsName, String jndiName, String minPool, String maxPool) {
            this.dsName = dsName;
            this.jndiName = jndiName;
            this.minPool = minPool;
            this.maxPool = maxPool;
        }

        public void setConnectionUrl(String connectionUrl) {
            this.connectionUrl = connectionUrl;
        }

        public void setSecurityDomain(String securityDomain) {
            this.securityDomain = securityDomain;
        }

        public void setDsUsername(String dsUsername) {
            this.dsUsername = dsUsername;
        }

        public void setDsPassword(String dsPassword) {
            this.dsPassword = dsPassword;
        }

        public void setXaUsername(String xaUsername) {
            this.xaUsername = xaUsername;
        }

        public void setXaPassword(String xaPassword) {
            this.xaPassword = xaPassword;
        }

        public void setXaProperties(List<DatasourceTask.XaProperty> xaProperties) {
            this.xaProperties = xaProperties;
        }

        public String getDsName() {
            return dsName;
        }

        public String getJndiName() {
            return jndiName;
        }

        public String getConnectionUrl() {
            return connectionUrl;
        }

        public String getMinPool() {
            return minPool;
        }

        public String getMaxPool() {
            return maxPool;
        }

        public String getSecurityDomain() {
            return securityDomain;
        }

        public String getDsUsername() {
            return dsUsername;
        }

        public String getDsPassword() {
            return dsPassword;
        }

        public String getXaUsername() {
            return xaUsername;
        }

        public String getXaPassword() {
            return xaPassword;
        }

        public List<XaProperty> getXaProperties() {
            return xaProperties;
        }

        @Override
        protected Map<String, String> listAttributes() {
            final HashMap<String, String> attrs = new HashMap<>();
            attrs.put("dsName",dsName);
            attrs.put("jndiName",jndiName);
            attrs.put("connectionUrl",connectionUrl);
            attrs.put("minPool",minPool);
            attrs.put("maxPool",maxPool);
            attrs.put("securityDomain",securityDomain);
            attrs.put("dsUsername",dsUsername);
            attrs.put("xaUsername",xaUsername);
            if (xaProperties != null) {
                for (XaProperty property : xaProperties) {
                    attrs.put(XA_PROPERTY_PREFIX + property.getKey(), property.getValue());
                }
            }
            return attrs;
        }

        @Override
        protected void acceptAttributes(Map<String, String> attributes, BiFunction<String, String, String> variableResolver) throws AutomaticInstallationParsingException {
            dsName = attributes.get("dsName");
            jndiName = attributes.get("jndiName");
            connectionUrl = attributes.get("connectionUrl");
            minPool = attributes.get("minPool");
            maxPool = attributes.get("maxPool");
            securityDomain = attributes.get("securityDomain");
            dsUsername = attributes.get("dsUsername");
            dsPassword = variableResolver.apply(DS_PASSWORD_PROPERTY_NAME, "Datasource password:");
            xaUsername = attributes.get("xaUsername");
            if (xaUsername != null) {
                xaPassword = variableResolver.apply(XA_RECOVERY_PASSWORD_PROPERTY_NAME, "XA recovery password:");
            }
            xaProperties = getXaPropertiesFromAttributes(attributes);
        }

        @Override
        protected Set<String> listVariables() {
            final HashSet<String> vars = new HashSet<>();
            if (dsPassword != null) {
                vars.add(DS_PASSWORD_PROPERTY_NAME);
            }
            if (xaPassword != null) {
                vars.add(XA_RECOVERY_PASSWORD_PROPERTY_NAME);
            }
            return vars;
        }

        @Override
        public int hashCode() {
            return Objects.hash(dsName, jndiName, connectionUrl, minPool, maxPool, securityDomain, dsUsername,
                    dsPassword, xaUsername, xaPassword, xaProperties);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null || getClass() != obj.getClass()) return false;
            Config config = (Config) obj;
            return Objects.equals(dsName, config.dsName) && Objects.equals(jndiName, config.jndiName) &&
                    Objects.equals(connectionUrl, config.connectionUrl) && Objects.equals(minPool, config.minPool) &&
                    Objects.equals(maxPool, config.maxPool) && Objects.equals(securityDomain, config.securityDomain) &&
                    Objects.equals(dsUsername, config.dsUsername) && Objects.equals(dsPassword, config.dsPassword) &&
                    Objects.equals(xaUsername, config.xaUsername) && Objects.equals(xaPassword, config.xaPassword) &&
                    Objects.equals(xaProperties, config.xaProperties);
        }

        private List<XaProperty> getXaPropertiesFromAttributes(Map<String, String> attributes) {
            final List<XaProperty> xaProperties = attributes.entrySet().stream()
                    .filter(entry -> entry.getKey().startsWith(XA_PROPERTY_PREFIX))
                    .map(entry -> new XaProperty(entry.getKey().replace(XA_PROPERTY_PREFIX, ""), entry.getValue()))
                    .collect(Collectors.toList());
            return xaProperties.isEmpty()?null:xaProperties;
        }
    }

    public static class XaProperty {
        private final String key;
        private final String value;

        public XaProperty(String key, String value) {
            this.key = key;
            this.value = value;
        }

        public String getKey() {
            return key;
        }

        public String getValue() {
            return value;
        }
    }
}
