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

import java.io.IOException;
import java.util.ArrayList;
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.LdapModel;
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.EmbeddedServer;
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.SecurityDomainConfig;
import org.jboss.installer.postinstall.task.impl.secdom.CertSecurity;
import org.jboss.installer.postinstall.task.impl.secdom.DatabaseSecurity;
import org.jboss.installer.postinstall.task.impl.secdom.KerberosSecurity;
import org.jboss.installer.postinstall.task.impl.secdom.PropertiesSecurity;
import org.jboss.installer.postinstall.task.utils.ModelDescriptionConstants;

class SecurityDomainTask implements CliPostInstallTaskImpl {

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

    @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(ModelDescriptionConstants.OP).asString().equals("add-identity") || op.get(ModelDescriptionConstants.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 (ServerOperationException 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 (ServerOperationException 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(ModelDescriptionConstants.ADDRESS).get(0).asString().contains("(\"host\" => \"primary\")");
    }

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

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

        final Optional<CredentialStoreConfig> storeConfig
                = Optional.ofNullable(data.getConfig(CredentialStoreConfig.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, LdapModel model, Optional<CredentialStoreConfig> storeConfig) {
        final LdapSecurity ldapSecurity = new LdapSecurity();

        final SecurityDomainModel sdModel = new SecurityDomainModel(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;
    }
}
