/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2024 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.impl;

import static org.jboss.installer.core.LoggerUtils.taskLog;
import static org.jboss.installer.postinstall.ldap.LdapSecurity.ALIAS_SUFFIX;
import static org.jboss.installer.postinstall.task.utils.ModelUtils.createEmptyOperation;
import static org.jboss.installer.postinstall.task.utils.ModelUtils.pathAddress;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import org.jboss.dmr.ModelNode;
import org.jboss.installer.core.InstallationData;
import org.jboss.installer.postinstall.CliPostInstallTaskImpl;
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.ldap.SecurityDomainModel;
import org.jboss.installer.postinstall.server.DomainServer;
import org.jboss.installer.postinstall.server.ServerOperationException;
import org.jboss.installer.postinstall.server.StandaloneServer;
import org.jboss.installer.postinstall.task.CredentialStoreConfig;
import org.jboss.installer.postinstall.task.LDAPManagementAuthConfig;

class LDAPSetupTask implements CliPostInstallTaskImpl {

    private static final String DOMAIN_SUFFIX = "-domain";
    private static final String SASL_AUTH_SUFFIX = "-sasl-auth";
    private static final String HTTP_AUTH_SUFFIX = "-http-auth";

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

    @Override
    public boolean applyToStandalone(InstallationData data, StandaloneServer server, TaskPrinter printer) {
        final LDAPManagementAuthConfig config = data.getConfig(LDAPManagementAuthConfig.class);
        final Optional<CredentialStoreConfig> storeConfig = Optional.ofNullable(data.getConfig(CredentialStoreConfig.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 (ServerOperationException 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 LDAPManagementAuthConfig config = data.getConfig(LDAPManagementAuthConfig.class);
        final Optional<CredentialStoreConfig> storeConfig = Optional.ofNullable(data.getConfig(CredentialStoreConfig.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 (ServerOperationException e) {
            taskLog.error("CLI operation failed", e);
            printer.print("tasks.ldap.failed");
            printer.print(e);
            return false;
        }
    }

    private List<ModelNode> prepareOperations(LDAPManagementAuthConfig config, Optional<CredentialStoreConfig> 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(LDAPManagementAuthConfig config) {
        ModelNode op = createEmptyOperation("write-attribute",
                pathAddress("core-service", "management").add("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(LDAPManagementAuthConfig config) {
        ModelNode op = createEmptyOperation("write-attribute",
                pathAddress("core-service", "management").add("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(LDAPManagementAuthConfig config) {
        ModelNode op = createEmptyOperation("add",
                pathAddress("subsystem", "elytron").add("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(LDAPManagementAuthConfig config) {
        ModelNode op = createEmptyOperation("add",
                pathAddress("subsystem", "elytron").add("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(LDAPManagementAuthConfig config) {
        final SecurityDomainModel model = new SecurityDomainModel(config.getRealmName() + DOMAIN_SUFFIX,
                config.getRealmName(), config.toModel().getRoleAttribute() != null);
        return new SecurityDomain().createSecurityDomain(model);
    }

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

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

    private ModelNode createDirContext(LDAPManagementAuthConfig config, Optional<CredentialStoreConfig> storeConfig) {
        final LdapSecurity ldapSecurity = new LdapSecurity();

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