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

import com.google.auto.service.AutoService;
import org.jboss.as.controller.OperationFailedException;
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.EmbeddedServer;
import org.jboss.installer.postinstall.server.StandaloneServer;
import org.jboss.installer.postinstall.task.secdom.CertSecurity;
import org.jboss.installer.postinstall.task.secdom.DatabaseSecurity;
import org.jboss.installer.postinstall.task.secdom.KerberosSecurity;
import org.jboss.installer.postinstall.task.secdom.PropertiesSecurity;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;

import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADDRESS;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP;
import static org.jboss.installer.core.LoggerUtils.taskLog;
import static org.jboss.installer.postinstall.ldap.LdapSecurity.Model.LDAP_PASSWORD_KEY;
import static org.jboss.installer.postinstall.task.secdom.CertSecurity.CertificateConfig.TRUST_STORE_PASSWORD_KEY;

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

    public static final String REALM_SUFFIX = "-realm";

    @Override
    public boolean applyToStandalone(InstallationData data, StandaloneServer server, TaskPrinter printer) {
        printer.print("tasks.sec_dom.started", server.currentConfiguration());
        ArrayList<ModelNode> ops;
        try {
            ops = prepareOperations(data, server, printer);
        } catch (Exception e) {
            taskLog.error("Error when preparing security domain operations", e);
            printer.print("tasks.sec_dom.failed");
            printer.print(e);
            return false;
        }

        for (ModelNode op : ops) {
            try {
                if ((op.get(OP).asString().equals("add-identity") || op.get(OP).asString().equals("add-identity-attribute")) && !server.currentConfiguration().equals("standalone.xml")){
                    // For add-identity and add-identity-attribute operations, only execute once with standalone.xml, because it writes to the same file containing the realm.
                    // Otherwise, subsequent operation fails with "WFLYELY01000: Identity with name [user1@REALM] already exists."
                    continue;
                }
                server.execute(op, "Add Security Domain");
            } catch (OperationFailedException e) {
                taskLog.error("Failed operation", e);
                printer.print("tasks.sec_dom.failed");
                printer.print(e);
                return false;
            }
        }
        printer.print("tasks.sec_dom.finished");
        return true;
    }

    @Override
    public boolean applyToDomain(InstallationData data, DomainServer server, TaskPrinter printer) {
        if (!server.currentConfiguration().equals("host.xml")) {
            // we create the security domains on profiles, so we need only a single run
            return true;
        }
        printer.print("tasks.sec_dom.started", server.currentConfiguration());

        ArrayList<ModelNode> ops;
        try {
            ops = prepareOperations(data, server, printer);
        } catch (Exception e) {
            taskLog.error("Error when preparing security domain operations", e);
            printer.print("tasks.sec_dom.failed");
            printer.print(e);
            return false;
        }

        for (ModelNode op : ops) {
            try {
                if (isOperationOnPrimaryHC(op)) {
                    server.execute(op, "SecurityDomainTask executes operations on primary HC.");
                } else {
                    server.executeOnProfiles(op, "Add Security Domain");
                }
            } catch (OperationFailedException e) {
                taskLog.error("Failed operation", e);
                printer.print("tasks.sec_dom.failed");
                printer.print(e);
                return false;
            }
        }
        printer.print("tasks.sec_dom.finished");
        return true;
    }

    private boolean isOperationOnPrimaryHC(ModelNode op) {
        return op.get(ADDRESS).get(0).asString().contains("(\"host\" => \"primary\")");
    }

    private ArrayList<ModelNode> prepareOperations(InstallationData data, EmbeddedServer server, TaskPrinter printer) throws IOException, OperationFailedException {
        final Config config = data.getConfig(Config.class);
        String domainName = config.getDomainName();

        ArrayList<ModelNode> ops = new ArrayList<>();

        final Optional<CredentialStoreInstallTask.Config> storeConfig
                = Optional.ofNullable(data.getConfig(CredentialStoreInstallTask.Config.class));
        switch (config.getConfigType()) {
            case Properties:
                ops.addAll(new PropertiesSecurity(printer).toOperations(server, domainName, config.getPropertiesFileConfig()));
                break;
            case Jdbc:
                ops.addAll(new DatabaseSecurity().toOperations(domainName, config.getJdbcConfig()));
                break;
            case Ldap: {
                ops.addAll(addLdapDomain(domainName, config.getLdapConfig(), storeConfig));
                break;
            }
            case Cert: {
                ops.addAll(new CertSecurity(printer).toOperations(server, domainName, config.getCertConfig(), storeConfig));
                break;
            }
            case Kerberos: {
                ops.addAll(new KerberosSecurity().toOperations(server, domainName, config.getKerberosConfig()));
                break;
            }
            default:
                throw new IllegalArgumentException("Unknown security domain type: " + config.getConfigType());
        }
        return ops;
    }

    private List<ModelNode> addLdapDomain(String domainName, LdapSecurity.Model model, Optional<CredentialStoreInstallTask.Config> storeConfig) {
        final LdapSecurity ldapSecurity = new LdapSecurity();

        final SecurityDomain.Model sdModel = new SecurityDomain.Model(domainName,
                domainName + REALM_SUFFIX, model.getRoleAttribute() != null);

        List<ModelNode> ops = new ArrayList<>();
        ops.add(ldapSecurity.createDirContext(model, storeConfig));
        ops.add(ldapSecurity.createLdapRealm(model));
        ops.add(new SecurityDomain().createSecurityDomain(sdModel));
        return ops;
    }

    @Override
    public String getName() {
        return "security_domain.task.name";
    }

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

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

    public static class Config extends FlatListPostInstallConfig {
        private String domainName;
        private PropertiesSecurity.PropertiesFileConfig propertiesFileConfig;
        private DatabaseSecurity.JdbcConfig jdbcConfig;
        private ConfigType configType = ConfigType.None;
        private LdapSecurity.Model ldapConfig;
        private CertSecurity.CertificateConfig certConfig;
        private KerberosSecurity.KerberosConfig kerberosConfig;

        public void setCertConfig(CertSecurity.CertificateConfig certConfig) {
            this.certConfig = certConfig;
            this.configType = ConfigType.Cert;
        }

        public CertSecurity.CertificateConfig getCertConfig() {
            return certConfig;
        }

        enum ConfigType {
            None, Properties, Jdbc, Ldap, Cert, Kerberos
        }
        public ConfigType getConfigType() {
            return configType;
        }

        public String getDomainName() {
            return domainName;
        }

        public void setDomainName(String domainName) {
            this.domainName = domainName;
        }

        public PropertiesSecurity.PropertiesFileConfig getPropertiesFileConfig() {
            return propertiesFileConfig;
        }

        public void setPropertiesFileConfig(PropertiesSecurity.PropertiesFileConfig propertiesFileConfig) {
            this.propertiesFileConfig = propertiesFileConfig;
            this.configType = ConfigType.Properties;
        }

        public DatabaseSecurity.JdbcConfig getJdbcConfig() {
            return jdbcConfig;
        }

        public void setJdbcFileConfig(DatabaseSecurity.JdbcConfig jdbcConfig) {
            this.jdbcConfig = jdbcConfig;
            this.configType = ConfigType.Jdbc;
        }

        public void setLdapConfig(LdapSecurity.Model config) {
            this.ldapConfig = config;
            this.configType = ConfigType.Ldap;
        }

        public LdapSecurity.Model getLdapConfig() {
            return ldapConfig;
        }

        public KerberosSecurity.KerberosConfig getKerberosConfig() {
            return kerberosConfig;
        }

        public void setKerberosConfig(KerberosSecurity.KerberosConfig kerberosConfig) {
            this.kerberosConfig = kerberosConfig;
            this.configType = ConfigType.Kerberos;
        }

        @Override
        protected Map<String, String> listAttributes() {
            final HashMap<String, String> attrs = new HashMap<>();
            attrs.put("domainName", domainName);
            attrs.put("configType", configType.toString());
            switch (configType) {
                case Properties:
                    attrs.putAll(propertiesFileConfig.toAttributes());
                    break;
                case Jdbc:
                    attrs.putAll(jdbcConfig.toAttributes());
                    break;
                case Ldap:
                    attrs.putAll(ldapConfig.toAttributes());
                    break;
                case Cert:
                    attrs.putAll(certConfig.toAttributes());
                    break;
                case Kerberos:
                    attrs.putAll(kerberosConfig.toAttributes());
                    break;
                default:
                    throw new IllegalStateException("Unknown config type: " + configType);
            }
            return attrs;
        }

        @Override
        protected Set<String> listVariables() {
            HashSet<String> vars = new HashSet<>();
            if (configType == ConfigType.Ldap) {
                vars.add(LDAP_PASSWORD_KEY);
            }
            if (configType == ConfigType.Cert) {
                vars.add(TRUST_STORE_PASSWORD_KEY);
            }
            return vars;
        }

        @Override
        protected void acceptAttributes(Map<String, String> attributes, BiFunction<String, String, String> variableResolver) throws AutomaticInstallationParsingException {
            if (!attributes.containsKey("domainName") || ! attributes.containsKey("configType")) {
                throw new AutomaticInstallationParsingException("Stored configuration doesn't have required fields ['configType', 'domainName']");
            }

            this.domainName = attributes.get("domainName");
            this.configType = ConfigType.valueOf(attributes.get("configType"));

            switch (configType) {
                case Properties:
                    this.propertiesFileConfig = new PropertiesSecurity.PropertiesFileConfig(attributes);
                    break;
                case Jdbc:
                    this.jdbcConfig = new DatabaseSecurity.JdbcConfig(attributes);
                    break;
                case Ldap:
                    attributes.put(LDAP_PASSWORD_KEY, variableResolver.apply(LDAP_PASSWORD_KEY, "LDAP server password:"));
                    this.ldapConfig = new LdapSecurity.Model(attributes);
                    break;
                case Cert:
                    attributes.put(TRUST_STORE_PASSWORD_KEY, variableResolver.apply(TRUST_STORE_PASSWORD_KEY, "Trust store password:"));
                    this.certConfig = new CertSecurity.CertificateConfig(attributes);
                    break;
                case Kerberos:
                    this.kerberosConfig = new KerberosSecurity.KerberosConfig(attributes);
                    break;
                default:
                    throw new AutomaticInstallationParsingException("Unknown config type: " + configType);
            }
        }
    }
}
