/**
 * 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
 *
 *     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.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.ldap.LdapSecurity;
import org.jboss.installer.postinstall.ldap.SecurityDomain;
import org.jboss.installer.postinstall.server.DomainServer;
import org.jboss.installer.postinstall.server.StandaloneServer;

import java.util.ArrayList;
import java.util.Arrays;
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 static org.jboss.as.controller.operations.common.Util.createEmptyOperation;
import static org.jboss.installer.core.LoggerUtils.taskLog;
import static org.jboss.installer.postinstall.ldap.LdapSecurity.ALIAS_SUFFIX;

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

    private static final String DOMAIN_SUFFIX = "-domain";
    private static final String LDAP_CONFIG_TASK_NAME = "ldap_config.task.name";
    private static final String SASL_AUTH_SUFFIX = "-sasl-auth";
    private static final String HTTP_AUTH_SUFFIX = "-http-auth";

    @Override
    public boolean applyToStandalone(InstallationData data, StandaloneServer server, TaskPrinter printer) {
        final Config config = data.getConfig(Config.class);
        final Optional<CredentialStoreInstallTask.Config> storeConfig = Optional.ofNullable(data.getConfig(CredentialStoreInstallTask.Config.class));

        printer.print("tasks.ldap.started", server.currentConfiguration());

        List<ModelNode> operations = prepareOperations(config, storeConfig);
        // this cannot be applied in domain as it will break domain creation
        // TODO: figure out if there's a better way
        operations.add(updateManagementInterfaceSasl(config));
        try {
            if (storeConfig.isPresent() && !server.aliasExists(config.getConnection() + ALIAS_SUFFIX, storeConfig.get().getStoreName())) {
                printer.print("tasks.ldap.store_password", config.getConnection() + ALIAS_SUFFIX, storeConfig.get().getStoreName());
                server.encryptPassword(config.getConnection() + ALIAS_SUFFIX, storeConfig.get().getStoreName(), config.getPassword());
            }
            for (int i = 0; i < operations.size(); i++) {
                ModelNode operation = operations.get(i);
                server.execute(operation, "Enable LDAP - step " + i);
            }
            printer.print("tasks.ldap.finished");
            return true;
        } catch (OperationFailedException e) {
            taskLog.error("CLI operation failed", e);
            printer.print("tasks.ldap.failed");
            printer.print(e);
            return false;
        }
    }

    @Override
    public boolean applyToDomain(InstallationData data, DomainServer server, TaskPrinter printer) {
        final Config config = data.getConfig(Config.class);
        final Optional<CredentialStoreInstallTask.Config> storeConfig = Optional.ofNullable(data.getConfig(CredentialStoreInstallTask.Config.class));

        printer.print("tasks.ldap.started", server.currentConfiguration());

        List<ModelNode> operations = prepareOperations(config, storeConfig);

        try {
            if (storeConfig.isPresent() && !server.aliasExists(config.getConnection() + ALIAS_SUFFIX, storeConfig.get().getStoreName())) {
                printer.print("tasks.ldap.store_password", config.getConnection() + ALIAS_SUFFIX, storeConfig.get().getStoreName());
                server.encryptPassword(config.getConnection() + ALIAS_SUFFIX, storeConfig.get().getStoreName(), config.getPassword());
            }
            for (int i = 0; i < operations.size(); i++) {
                ModelNode operation = operations.get(i);
                server.execute(server.wrapInHost(operation), "Enable LDAP - step " + i);
            }
            printer.print("tasks.ldap.finished");
            return true;
        } catch (OperationFailedException e) {
            taskLog.error("CLI operation failed", e);
            printer.print("tasks.ldap.failed");
            printer.print(e);
            return false;
        }
    }

    private List<ModelNode> prepareOperations(Config config, Optional<CredentialStoreInstallTask.Config> storeConfig) {
        List<ModelNode> operations = new ArrayList<>();
        operations.add(createDirContext(config, storeConfig));
        // create ldap-realm
        operations.add(createLdapRealm(config));
        // security-domain
        operations.add(createSecurityDomain(config));
        // http-authentication-factory
        operations.add(createHttpAuthFactory(config));
        // sasl-authentication-factory
        operations.add(createSaslAuthFactory(config));
        // Update the management interfaces to use new factories
        operations.add(updateManagementInterfaceHttp(config));

        return operations;
    }

    private ModelNode updateManagementInterfaceSasl(Config config) {
        ModelNode op = createEmptyOperation("write-attribute",
                PathAddress.pathAddress("core-service", "management").append("management-interface", "http-interface"));
        op.get("name").set("http-upgrade.sasl-authentication-factory");
        op.get("value").set(config.getRealmName() + SASL_AUTH_SUFFIX);
        return op;
    }

    private ModelNode updateManagementInterfaceHttp(Config config) {
        ModelNode op = createEmptyOperation("write-attribute",
                PathAddress.pathAddress("core-service", "management").append("management-interface", "http-interface"));
        op.get("name").set("http-authentication-factory");
        op.get("value").set(config.getRealmName() + HTTP_AUTH_SUFFIX);
        return op;
    }

    private ModelNode createSaslAuthFactory(Config config) {
        ModelNode op = createEmptyOperation("add",
                PathAddress.pathAddress("subsystem", "elytron").append("sasl-authentication-factory", config.getRealmName() + SASL_AUTH_SUFFIX));
        op.get("sasl-server-factory").set("configured");
        op.get("security-domain").set(config.getRealmName() + DOMAIN_SUFFIX);
        ModelNode basicConfNode = new ModelNode();
        basicConfNode.get("mechanism-name").set("PLAIN");
        ModelNode localConfNode = new ModelNode();
        localConfNode.get("mechanism-name").set("JBOSS-LOCAL-USER");
        localConfNode.get("realm-mapper").set("local");
        op.get("mechanism-configurations").set(Arrays.asList(basicConfNode, localConfNode));

        return op;
    }

    private ModelNode createHttpAuthFactory(Config config) {
        ModelNode op = createEmptyOperation("add",
                PathAddress.pathAddress("subsystem", "elytron").append("http-authentication-factory", config.getRealmName() + HTTP_AUTH_SUFFIX));
        op.get("http-server-mechanism-factory").set("global");
        op.get("security-domain").set(config.getRealmName() + DOMAIN_SUFFIX);
        ModelNode mechConfNode = new ModelNode();
        mechConfNode.get("mechanism-name").set("BASIC");
        op.get("mechanism-configurations").set(List.of(mechConfNode));

        return op;
    }

    private ModelNode createSecurityDomain(Config config) {
        final SecurityDomain.Model model = new SecurityDomain.Model(config.getRealmName() + DOMAIN_SUFFIX,
                config.getRealmName(), config.toModel().getRoleAttribute() != null);
        return new SecurityDomain().createSecurityDomain(model);
    }

    private ModelNode createLdapRealm(Config config) {
        final LdapSecurity ldapSecurity = new LdapSecurity();

        return ldapSecurity.createLdapRealm(config.toModel());
    }

    private ModelNode createDirContext(Config config, Optional<CredentialStoreInstallTask.Config> storeConfig) {
        final LdapSecurity ldapSecurity = new LdapSecurity();

        return ldapSecurity.createDirContext(config.toModel(), storeConfig);
    }

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

    @Override
    public String getSerializationName() {
        return "setup-ldap-management-auth";
    }

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

    public enum FilterType {
        USERNAME("ldap_realm_screen.filter_type.username"), ADVANCED("ldap_realm_screen.filter_type.advanced");

        private final String key;

        FilterType(String key) {
            this.key = key;
        }

        public static String[] getLabels() {
            String[] res = new String[FilterType.values().length];
            for (int i = 0; i < FilterType.values().length; i++) {
                res[i] = FilterType.values()[i].key;
            }
            return res;
        }
    }

    public static class Config extends FlatListPostInstallConfig {

        public static final String PASSWORD_PROPERTY_NAME = "ldap_setup.password";
        private String url;
        private String dn;
        private String password;
        private String connection;
        private String filter;
        private boolean recursive;
        private FilterType filterType;
        private String baseDN;
        private String realmName;

        public Config() {
        }

        public Config(String connection, String url, String dn, String password) {
            this.connection = connection;
            this.url = url;
            this.dn = dn;
            this.password = password;
        }

        public LdapSecurity.Model toModel() {
            final LdapSecurity.Model model = new LdapSecurity.Model();
            model.setRealmName(realmName);
            model.setConnection(connection, url, dn, password);
            model.setUserFilter(filterType, filter, baseDN, "userPassword", recursive);

            return model;
        }

        public String getUrl() {
            return url;
        }

        public String getDn() {
            return dn;
        }

        public void setPassword(String password) {
            this.password = password;
        }

        public String getPassword() {
            return password;
        }

        public String getConnection() {
            return connection;
        }

        public void setFilter(String filter) {
            this.filter = filter;
        }

        public String getFilter() {
            return filter;
        }

        public void setRecursive(boolean recursive) {
            this.recursive = recursive;
        }

        public boolean isRecursive() {
            return recursive;
        }

        public void setFilterType(FilterType filterType) {
            this.filterType = filterType;
        }

        public FilterType getFilterType() {
            return filterType;
        }

        public void setBaseDN(String baseDN) {
            this.baseDN = baseDN;
        }

        public String getBaseDN() {
            return baseDN;
        }

        public void setRealmName(String realmName) {
            this.realmName = realmName;
        }

        public String getRealmName() {
            return realmName;
        }

        @Override
        protected Map<String, String> listAttributes() {
            final HashMap<String, String> attrs = new HashMap<>();
            attrs.put("url", url);
            attrs.put("dn", dn);
            attrs.put("connection", connection);
            attrs.put("filter", filter);
            attrs.put("recursive", recursive + "");
            attrs.put("filterType", filterType.toString());
            attrs.put("baseDN", baseDN);
            attrs.put("realmName", realmName);
            return attrs;
        }

        @Override
        protected Set<String> listVariables() {
            final HashSet<String> vars = new HashSet<>();
            vars.add(PASSWORD_PROPERTY_NAME);
            return vars;
        }

        @Override
        protected void acceptAttributes(Map<String, String> attributes, BiFunction<String, String, String> variableResolver) throws AutomaticInstallationParsingException {
            password = variableResolver.apply(PASSWORD_PROPERTY_NAME, "LDAP authentication password");
            url = attributes.get("url");
            dn = attributes.get("dn");
            connection = attributes.get("connection");
            filter = attributes.get("filter");
            recursive = Boolean.parseBoolean(attributes.getOrDefault("recursive", "false"));
            filterType = FilterType.valueOf(attributes.get("filterType"));
            baseDN = attributes.get("baseDN");
            realmName = attributes.get("realmName");
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Config config = (Config) o;
            return recursive == config.recursive && Objects.equals(url, config.url) && Objects.equals(dn, config.dn) && Objects.equals(password, config.password) && Objects.equals(connection, config.connection) && Objects.equals(filter, config.filter) && filterType == config.filterType && Objects.equals(baseDN, config.baseDN) && Objects.equals(realmName, config.realmName);
        }

        @Override
        public int hashCode() {
            return Objects.hash(url, dn, password, connection, filter, recursive, filterType, baseDN, realmName);
        }
    }
}
