/*
 * 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.postinstall.task.utils.ModelUtils.createEmptyOperation;
import static org.jboss.installer.postinstall.task.utils.ModelUtils.pathAddress;

import java.util.List;
import java.util.stream.Collectors;

import org.jboss.dmr.ModelNode;
import org.jboss.installer.core.InstallationData;
import org.jboss.installer.core.LoggerUtils;
import org.jboss.installer.postinstall.CliPostInstallTaskImpl;
import org.jboss.installer.postinstall.TaskPrinter;
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.PortBindingConfig;

class PortConfigurationTask implements CliPostInstallTaskImpl {
    public static final String DOMAIN_MANAGEMENT_HTTPS_PORT_PROPERTY = "jboss.management.https.port";
    private static final String DOMAIN_MANAGEMENT_PORT_PROPERTY = "jboss.management.http.port";
    public static final int DOMAIN_MANAGEMENT_HTTPS_PORT_DEFAULT = 9993;
    public static final int DOMAIN_MANAGEMENT_PORT_DEFAULT = 9990;
    private static final String JBOSS_PROPERTY_SUB = "${%s:%s}";
    private static final String DOMAIN_PRIMARY_PORT_PROPERTY = "jboss.domain.primary.port";
    private static final String SOCKET_OFFSET_PROPERTY = "jboss.socket.binding.port-offset";
    private static final String STANDARD_SOCKETS_BINDING_GROUP = "standard-sockets";
    private static final String[] BINDING_GROUPS = new String[]{"standard", "ha", "full", "full-ha"};
    private static final String DOMAIN_SCREEN_PREFIX = "domain-";
    public static final String PRIMARY_HOST_NAME = "primary";

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

    @Override
    public boolean applyToStandalone(InstallationData idata, StandaloneServer server, TaskPrinter printer) {
        printer.print("tasks.ports.started", server.currentConfiguration());
        final PortBindingConfig config = idata.getConfig(PortBindingConfig.class);
        try {
            if (config.getOffset() != 0) {
                printer.print("tasks.ports.offset", "" + config.getOffset());
                applyStandaloneOffset(server, config);
            } else {
                final List<PortBindingConfig.SocketBinding> ports = config.getPorts(server.currentConfiguration());
                if (ports != null) {
                    for (PortBindingConfig.SocketBinding port : ports) {
                        printer.print("tasks.ports.binding", port.getKey());
                        applySocketBinding(server, STANDARD_SOCKETS_BINDING_GROUP, port);
                    }
                }
            }
        } catch (ServerOperationException e) {
            LoggerUtils.taskLog.error("Failed to perform operation", e);
            printer.print("tasks.ports.failed");
            printer.print(e);
            return false;
        }

        printer.print("tasks.ports.finished");
        return true;
    }

    @Override
    public boolean applyToDomain(InstallationData idata, DomainServer server, TaskPrinter printer) {
        printer.print("tasks.ports.started", server.currentConfiguration());
        final PortBindingConfig config = idata.getConfig(PortBindingConfig.class);

        try {
            if (config.getOffset() != 0) {
                printer.print("tasks.ports.offset", "" + config.getOffset());
                applyOffsetToDomainConfigs(server, config);
            } else {
                applyIndividualPortsToDomain(server, config, printer);
            }
        } catch (ServerOperationException e) {
            LoggerUtils.taskLog.error("Failed to perform operation", e);
            printer.print("tasks.ports.failed");
            printer.print(e);
            return false;
        }

        printer.print("tasks.ports.finished");
        return true;
    }

    private void applyIndividualPortsToDomain(DomainServer server, PortBindingConfig config, TaskPrinter printer) throws ServerOperationException {
        if (!server.currentConfiguration().equals(DomainServer.HOST_SECONDARY_XML)) {
            for (String bindingGroup : BINDING_GROUPS) {
                final List<PortBindingConfig.SocketBinding> ports = config.getPorts(DOMAIN_SCREEN_PREFIX + bindingGroup);
                if (ports != null) {
                    for (PortBindingConfig.SocketBinding port : ports) {
                        printer.print("tasks.ports.binding", port.getKey());
                        applySocketBinding(server, bindingGroup + "-sockets", port);
                    }
                }
            }
        }
        if (config.getManagementInterface() != null) {
            printer.print("tasks.ports.management_interface", config.getManagementInterface().getPortValue() + "");
            if (!server.currentConfiguration().equals(DomainServer.HOST_SECONDARY_XML)) {
                setHostInterfacePort(server, config.getManagementInterface());
            } else {
                final String hostGroupName = server.readHostGroupName();
                setStaticDiscoveryPort(server, hostGroupName, config.getManagementInterface().getPortValue());
            }
        }
    }

    private void applyOffsetToDomainConfigs(DomainServer server, PortBindingConfig config) throws ServerOperationException {
        int offset = config.getOffset();
        final String hostGroupName = server.readHostGroupName();
        for (String serverConfig : readServerConfigs(server, hostGroupName)) {
            updatePortOffsetForServer(server, offset, serverConfig, hostGroupName);
        }
        if (!server.currentConfiguration().equals(DomainServer.HOST_SECONDARY_XML)) {
            setHostInterfacePort(server, offset);
        } else {
            setStaticDiscoveryPort(server, hostGroupName, DOMAIN_MANAGEMENT_PORT_DEFAULT + offset);
        }
    }

    private void applyStandaloneOffset(StandaloneServer server, PortBindingConfig config) throws ServerOperationException {
        int offset = config.getOffset();
        final ModelNode defaultOffsetOp = createEmptyOperation("write-attribute",
                pathAddress("socket-binding-group", STANDARD_SOCKETS_BINDING_GROUP));
        defaultOffsetOp.get("name").set("port-offset");
        defaultOffsetOp.get("value").set(asExpression(SOCKET_OFFSET_PROPERTY, offset));
        server.execute(defaultOffsetOp, "Set default port offset");
    }

    private void applySocketBinding(EmbeddedServer server, String socketGroup, PortBindingConfig.SocketBinding port) throws ServerOperationException {
        final ModelNode socketBindingOp = createEmptyOperation("write-attribute",
                pathAddress("socket-binding-group", socketGroup).add("socket-binding", port.getKey()));
        socketBindingOp.get("name").set("port");
        if (port.getMcastAddress() == null) {
            socketBindingOp.get("value").set(asExpression(port.getProperty(), port.getPortValue()));
            server.execute(socketBindingOp, "Set default port offset");
        } else if (port.getPortValue() != null) {
            socketBindingOp.get("value").set(port.getPortValue());
            server.execute(socketBindingOp, "Set default port offset");
        }

        if (port.getMcastAddress() != null) {
            socketBindingOp.get("name").set("multicast-address");
            socketBindingOp.get("value").set(asExpression(port.getProperty(), port.getMcastAddress()));
            server.execute(socketBindingOp, "Set default port offset");

            socketBindingOp.get("name").set("multicast-port");
            socketBindingOp.get("value").set(port.getMcastPort());
            server.execute(socketBindingOp, "Set default port offset");
        }
    }

    private void setStaticDiscoveryPort(DomainServer server, String hostGroupName, int portValue) throws ServerOperationException {
        final ModelNode hostInterfacePort = createEmptyOperation("write-attribute",
                pathAddress("host", hostGroupName).add("core-service", "discovery-options")
                        .add("static-discovery", "primary"));
        hostInterfacePort.get("name").set("port");
        hostInterfacePort.get("value").set(asExpression(DOMAIN_PRIMARY_PORT_PROPERTY, portValue));
        server.execute(hostInterfacePort, "Set default port offset");
    }

    private void setHostInterfacePort(DomainServer server, int offset) throws ServerOperationException {
        setHostInterfacePort(server, new PortBindingConfig.SocketBinding(null, DOMAIN_MANAGEMENT_PORT_PROPERTY, DOMAIN_MANAGEMENT_PORT_DEFAULT + offset));
    }

    private void setHostInterfacePort(DomainServer server, PortBindingConfig.SocketBinding socketBinding) throws ServerOperationException {
        final ModelNode hostInterfacePort = createEmptyOperation("write-attribute",
                pathAddress("host", PRIMARY_HOST_NAME).add("core-service", "management")
                        .add("management-interface", "http-interface"));
        hostInterfacePort.get("name").set("port");
        hostInterfacePort.get("value").set(asExpression(socketBinding.getProperty(), socketBinding.getPortValue()));
        server.execute(hostInterfacePort, "Set default port offset");
    }

    private String asExpression(String key, int value) {
        return asExpression(key, "" + value);
    }

    private String asExpression(String key, String value) {
        if (key == null || key.trim().isEmpty()) {
            return value;
        }
        return String.format(JBOSS_PROPERTY_SUB, key, value);
    }

    private List<String> readServerConfigs(DomainServer server, String hostGroupName) throws ServerOperationException {
        final ModelNode readServerConfigs = createEmptyOperation("read-children-names", pathAddress("host", hostGroupName));
        readServerConfigs.get("child-type").set("server-config");
        final ModelNode res = server.execute(readServerConfigs, "Set default port offset");
        return res.asList().stream().map(n->n.asString()).collect(Collectors.toList());
    }

    private void updatePortOffsetForServer(DomainServer server, int offset, String serverName, String hostGroupName) throws ServerOperationException {
        final ModelNode readCurrentOffset = createEmptyOperation("read-attribute", pathAddress("host", hostGroupName).add("server-config", serverName));
        readCurrentOffset.get("name").set("socket-binding-port-offset");
        final ModelNode res = server.execute(readCurrentOffset, "Set default port offset");
        int port = res.asInt() + offset;

        final ModelNode defaultOffsetOp = createEmptyOperation("write-attribute",
                pathAddress("host", hostGroupName).add("server-config", serverName));
        defaultOffsetOp.get("name").set("socket-binding-port-offset");
        defaultOffsetOp.get("value").set(port);
        server.execute(defaultOffsetOp, "Set default port offset");
    }
}
