/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2022 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.apache.commons.lang3.tuple.Pair;
import org.jboss.as.controller.OperationFailedException;
import org.jboss.as.controller.PathAddress;
import org.jboss.dmr.ModelNode;
import org.jboss.installer.core.InstallationData;
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.StandaloneServer;

import java.util.List;
import java.util.Set;

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

@AutoService(PostInstallTask.class)
public class HttpsEnableTask extends AbstractHttpsEnableTask {

    private static final String NAME_KEY = "postinstall.task.enable_ssl.name";

    public static final String DEFAULT_KEYSTORE_FILE_NAME = "management.keystore";
    public static final String DEFAULT_TRUSTSTORE_FILE_NAME = "management.truststore";
    public static final String MANAGEMENT_KEYSTORE_NAME = "management-keystore";
    public static final String MANAGEMENT_TRUSTSTORE_NAME = "management-truststore";
    public static final String MANAGEMENT_KEYMANAGER_NAME = "management-keymanager";
    public static final String MANAGEMENT_TRUSTMANAGER_NAME = "management-trustmanager";
    public static final String MANAGEMENT_SSL_CONTEXT_NAME = "management-ssl-context";
    public static final String CREDENTIAL_ALIAS_KEYSTORE_PASSWORD = "ssl-management-keystore-password";
    public static final String CREDENTIAL_ALIAS_TRUSTSTORE_PASSWORD = "ssl-management-truststore-password";

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

    @Override
    public String getSerializationName() {
        return "enable-https";
    }

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

    public static class Config extends AbstractHttpsEnableTask.Config {
        public Config() {
            // no-op for unmarshalling
        }

        public Config(Set<String> protocols, String cipherSuites, String tls13cipherNames, String keyStorePath, String keyStorePassword, boolean mutual,
                      String trustStorePath, String trustStorePassword, boolean doGenerateStore, String dn,
                      String validity, String clientCertPath, boolean validateCertificate) {
            super(protocols, cipherSuites, tls13cipherNames, keyStorePath, keyStorePassword, mutual, trustStorePath, trustStorePassword,
                    doGenerateStore, dn, validity, clientCertPath, validateCertificate);
        }
    }

    @Override
    public boolean applyToStandalone(InstallationData data, StandaloneServer server, TaskPrinter printer) {
        // create key store, trust store, key manager, trust manager, ssl context
        try {
            printer.print("tasks.management_ssl.started", server.currentConfiguration());
            final List<Pair<ModelNode, String>> ops = getCommonOperations(data, server, MANAGEMENT_SSL_CONTEXT_NAME,
                    CREDENTIAL_ALIAS_KEYSTORE_PASSWORD, CREDENTIAL_ALIAS_TRUSTSTORE_PASSWORD,
                    DEFAULT_KEYSTORE_FILE_NAME, MANAGEMENT_KEYSTORE_NAME, MANAGEMENT_KEYMANAGER_NAME,
                    DEFAULT_TRUSTSTORE_FILE_NAME, MANAGEMENT_TRUSTSTORE_NAME, MANAGEMENT_TRUSTMANAGER_NAME, printer);

            ops.add(Pair.of(setSslContextOnInterface(MANAGEMENT_SSL_CONTEXT_NAME), "Set ssl-context on management-interface"));
            ops.add(Pair.of(setHttpsSocketBinding(), "Set https socket-binding on management-interface"));
            ops.add(Pair.of(removeHttpSocketBinding(), "Remove http socket-binding on management-interface"));

            for (Pair<ModelNode, String> opPair : ops) {
                server.execute(opPair.getLeft(), opPair.getRight());
            }
        } catch (OperationFailedException e) {
            taskLog.error("CLI operation failed", e);
            printer.print("tasks.management_ssl.failed");
            printer.print(e);
            return false;
        }
        printer.print("tasks.management_ssl.finished");
        return true;
    }

    @Override
    public boolean applyToDomain(InstallationData data, DomainServer server, TaskPrinter printer) {
        final PortConfigurationTask.Config portConfig = data.getConfig(PortConfigurationTask.Config.class);
        final int offset = portConfig == null?0:portConfig.getOffset();
        try {
            printer.print("tasks.management_ssl.started", server.currentConfiguration());
            // create key store, trust store, key manager, trust manager, ssl context
            final List<Pair<ModelNode, String>> ops = getCommonOperations(data, server, MANAGEMENT_SSL_CONTEXT_NAME,
                    CREDENTIAL_ALIAS_KEYSTORE_PASSWORD, CREDENTIAL_ALIAS_TRUSTSTORE_PASSWORD,
                    DEFAULT_KEYSTORE_FILE_NAME, MANAGEMENT_KEYSTORE_NAME, MANAGEMENT_KEYMANAGER_NAME,
                    DEFAULT_TRUSTSTORE_FILE_NAME, MANAGEMENT_TRUSTSTORE_NAME, MANAGEMENT_TRUSTMANAGER_NAME, printer);

            ops.add(Pair.of(setSslContextOnInterface(MANAGEMENT_SSL_CONTEXT_NAME), "Set ssl-context on management-interface"));

            ops.add(Pair.of(setSecurePort(offset), "Set https socket-binding on management-interface"));
            ops.add(Pair.of(removePort(), "Remove http socket from management interface"));

            for (Pair<ModelNode, String> opPair : ops) {
                server.execute(server.wrapInHost(opPair.getLeft()), opPair.getRight());
            }
        } catch (OperationFailedException e) {
            taskLog.error("CLI operation failed", e);
            printer.print("tasks.management_ssl.failed");
            printer.print(e);
            return false;
        }
        printer.print("tasks.management_ssl.finished");
        return true;
    }

    private ModelNode setSslContextOnInterface(String sslContextName) {
        // /core-service=management/management-interface=http-interface:write-attribute(name=ssl-context, value=foobar)
        final ModelNode writeOp = createEmptyOperation("write-attribute",
                PathAddress.pathAddress("core-service", "management").append("management-interface", "http-interface"));
        writeOp.get("name").set("ssl-context");
        writeOp.get("value").set(sslContextName);
        return writeOp;
    }

    private ModelNode setHttpsSocketBinding() {
        // /core-service=management/management-interface=http-interface:write-attribute(name=secure-socket-binding, value=management-https)
        final ModelNode writeOp = createEmptyOperation("write-attribute",
                PathAddress.pathAddress("core-service", "management").append("management-interface", "http-interface"));
        writeOp.get("name").set("secure-socket-binding");
        writeOp.get("value").set("management-https");
        return writeOp;
    }

    private ModelNode removeHttpSocketBinding() {
        // /core-service=management/management-interface=http-interface:write-attribute(name=secure-socket-binding, value=management-https)
        final ModelNode writeOp = createEmptyOperation("write-attribute",
                PathAddress.pathAddress("core-service", "management").append("management-interface", "http-interface"));
        writeOp.get("name").set("socket-binding");
        return writeOp;
    }

    private ModelNode removePort() {
        final ModelNode removePortOp = createEmptyOperation("undefine-attribute",
                PathAddress.pathAddress("core-service", "management").append("management-interface", "http-interface"));
        removePortOp.get("name").set("port");
        return removePortOp;
    }

    private ModelNode setSecurePort(int offset) {
        // /host=primary/core-service=management/management-interface=http-interface:write-attribute(name=secure-port, value=9993)
        final ModelNode portOp = createEmptyOperation("write-attribute",
                PathAddress.pathAddress("core-service", "management").append("management-interface", "http-interface"));
        portOp.get("name").set("secure-port");
        int port = PortConfigurationTask.DOMAIN_MANAGEMENT_HTTPS_PORT_DEFAULT + offset;
        portOp.get("value").set(String.format("${%s:%s}", PortConfigurationTask.DOMAIN_MANAGEMENT_HTTPS_PORT_PROPERTY, port));
        return portOp;
    }
}
