package org.jboss.installer.test.utils;

import static org.jboss.as.controller.client.helpers.ClientConstants.CONTROLLER_PROCESS_STATE_STARTING;
import static org.jboss.as.controller.client.helpers.ClientConstants.CONTROLLER_PROCESS_STATE_STOPPING;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.UnknownHostException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import java.util.function.Supplier;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.sasl.RealmCallback;

import org.jboss.arquillian.container.spi.client.container.DeploymentException;
import org.jboss.as.arquillian.container.ArchiveDeployer;
import org.jboss.as.arquillian.container.ManagementClient;
import org.jboss.as.controller.client.ModelControllerClient;
import org.jboss.as.controller.client.ModelControllerClientConfiguration;
import org.jboss.as.controller.client.helpers.Operations;
import org.jboss.as.controller.client.helpers.domain.DomainClient;
import org.jboss.as.controller.client.helpers.domain.ServerIdentity;
import org.jboss.as.controller.client.helpers.domain.ServerStatus;
import org.jboss.dmr.ModelNode;
import org.jboss.installer.postinstall.server.EmbeddedServer;
import org.jboss.shrinkwrap.api.Archive;
import org.junit.Assert;
import org.wildfly.core.launcher.CommandBuilder;
import org.wildfly.core.launcher.DomainCommandBuilder;
import org.wildfly.core.launcher.Launcher;
import org.wildfly.core.launcher.ProcessHelper;
import org.wildfly.core.launcher.StandaloneCommandBuilder;

public class ServerUtils {

    private static final int START_TIMEOUT = Integer.getInteger("org.jboss.installer.test.server_startup", 60);

    public static boolean isDomainRunning(final ModelControllerClient client) {
        final DomainClient domainClient = (client instanceof DomainClient ? (DomainClient) client : DomainClient.Factory.create(client));
        try {
            // Check for admin-only
            final ModelNode hostAddress = determineHostAddress(domainClient);
            final Operations.CompositeOperationBuilder builder = Operations.CompositeOperationBuilder.create()
                    .addStep(Operations.createReadAttributeOperation(hostAddress, "running-mode"))
                    .addStep(Operations.createReadAttributeOperation(hostAddress, "host-state"));
            ModelNode response = domainClient.execute(builder.build());
            if (Operations.isSuccessfulOutcome(response)) {
                response = Operations.readResult(response);
                if ("ADMIN_ONLY".equals(Operations.readResult(response.get("step-1")).asString())) {
                    if (Operations.isSuccessfulOutcome(response.get("step-2"))) {
                        final String state = Operations.readResult(response).asString();
                        return !CONTROLLER_PROCESS_STATE_STARTING.equals(state)
                                && !CONTROLLER_PROCESS_STATE_STOPPING.equals(state);
                    }
                }
            }
            final Map<ServerIdentity, ServerStatus> servers = new HashMap<>();
            final Map<ServerIdentity, ServerStatus> statuses = domainClient.getServerStatuses();
            for (ServerIdentity id : statuses.keySet()) {
                final ServerStatus status = statuses.get(id);
                switch (status) {
                    case DISABLED:
                    case STARTED: {
                        servers.put(id, status);
                        break;
                    }
                }
            }
            return statuses.size() == servers.size();
        } catch (IllegalStateException | IOException ignore) {
        }
        return false;
    }

    /**
     * Attempts to determine the address for a domain server.
     *
     * @param client the client used to communicate with the server
     *
     * @return the host address
     *
     * @throws IOException if an error occurs determining the host name
     */
    public static ModelNode determineHostAddress(final ModelControllerClient client) throws IOException {
        return Operations.createAddress("host", determineHostName(client));
    }

    /**
     * Attempts to determine the name for a domain server.
     *
     * @param client the client used to communicate with the server
     *
     * @return the host name
     *
     * @throws IOException if an error occurs determining the host name
     */
    public static String determineHostName(final ModelControllerClient client) throws IOException {
        final ModelNode op = Operations.createReadAttributeOperation(new ModelNode().setEmptyList(), "local-host-name");
        ModelNode response = client.execute(op);
        if (Operations.isSuccessfulOutcome(response)) {
            return Operations.readResult(response).asString();
        }
        throw new IOException("Failed to determine host name: " + Operations.readResult(response).asString());
    }

    public static String readStdout(final Path stdout) {
        final StringBuilder error = new StringBuilder(10240)
                .append("Failed to boot the server: ").append(System.lineSeparator());
        try {
            for (String line : Files.readAllLines(stdout)) {
                error.append(line).append(System.lineSeparator());
            }
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
        return error.toString();
    }

    public static Process startServer(EmbeddedServer embeddedServer, String username, String password) throws Exception {
        final Path stdout = Paths.get("target/test-server", "config-standalone-stdout.txt");
        final CommandBuilder commandBuilder;
        final Function<ModelControllerClient, BooleanSupplier> startCheck;
        final Authentication.CallbackHandler callbackHandler = Authentication.getCallbackHandler(username, password);
        if (embeddedServer.isDomain()) {
            commandBuilder = DomainCommandBuilder.of(TestServer.TARGET_PATH);
            startCheck = (client) -> () -> isDomainRunning(client);
        } else {
            commandBuilder = StandaloneCommandBuilder.of(TestServer.TARGET_PATH);
            startCheck = (client) -> () -> isStandaloneRunning(client);
        }
        final Launcher eapLauncher = Launcher.of(commandBuilder)
                .redirectOutput(stdout);
        final Process eapProcess = eapLauncher.launch();

        try (ModelControllerClient client = getModelControllerClient("127.0.0.1", 9990, callbackHandler)) {
            waitForStart(eapProcess, ()-> readStdout(stdout), startCheck.apply(client));
        }

        return eapProcess;
    }

    public static void deploy(Archive<?> testArchive) throws DeploymentException, UnknownHostException {
        System.out.println("Deploying " + testArchive.getName());
        ModelControllerClient client = ModelControllerClient.Factory.create("localhost", 9990);
        final ArchiveDeployer deployer = new ArchiveDeployer(new ManagementClient(client, "localhost", 9990, "remote+http"));
        deployer.deploy(testArchive);
    }

    public static void undeploy(String archiveName) throws UnknownHostException {
        System.out.println("Undeploying " + archiveName);
        ModelControllerClient client = ModelControllerClient.Factory.create("localhost", 9990);
        final ArchiveDeployer deployer = new ArchiveDeployer(new ManagementClient(client, "localhost", 9990, "remote+http"));
        deployer.undeploy(archiveName, true);
    }

    public static void testOnDeployment(EmbeddedServer server, Archive<?> testArchive, TestFunction test) throws Exception {
        Process serverProcess = null;
        try {
            serverProcess = ServerUtils.startServer(server, "", "");

            ServerUtils.deploy(testArchive);

            // execute test
            test.execute();
        } finally {
            ServerUtils.undeploy(testArchive.getName());
            if (server != null) {
                System.out.println("Shutting down ");
                ProcessHelper.destroyProcess(serverProcess);
                System.out.println("shut down");
            }
        }
    }

    private static ModelControllerClient getModelControllerClient(String address, int port, Authentication.CallbackHandler callbackHandler) {
        final ModelControllerClientConfiguration.Builder builder = new ModelControllerClientConfiguration.Builder()
                .setHostName(address)
                .setPort(port);
        builder.setHandler(callbackHandler);

        return ModelControllerClient.Factory.create(builder.build());
    }

    private static void waitForStart(final Process process, final Supplier<String> failureDescription, final BooleanSupplier check) throws InterruptedException {
        long timeout = START_TIMEOUT * 1000;
        final long sleep = 100L;
        while (timeout > 0) {
            long before = System.currentTimeMillis();
            if (check.getAsBoolean()) {
                break;
            }
            timeout -= (System.currentTimeMillis() - before);
            if (process != null && !process.isAlive()) {
                Assert.fail(failureDescription.get());
            }
            TimeUnit.MILLISECONDS.sleep(sleep);
            timeout -= sleep;
        }
        if (timeout <= 0) {
            if (process != null) {
                process.destroy();
            }
            Assert.fail(String.format("The server did not start within %s seconds: %s", START_TIMEOUT, failureDescription.get()));
        }
    }

    public static boolean isStandaloneRunning(final ModelControllerClient client) {
        try {
            final ModelNode response = client.execute(Operations.createReadAttributeOperation( new ModelNode().setEmptyList(), "server-state"));
            if (Operations.isSuccessfulOutcome(response)) {
                final String state = Operations.readResult(response).asString();
                return !CONTROLLER_PROCESS_STATE_STARTING.equals(state)
                        && !CONTROLLER_PROCESS_STATE_STOPPING.equals(state);
            }
        } catch (RuntimeException | IOException ignore) {
        }
        return false;
    }

    public interface TestFunction {
        void execute() throws Exception;
    }

    public static class Authentication {
        public static CallbackHandler getCallbackHandler(String username, String password) {
            return new CallbackHandler(username, password);
        }

        public static class CallbackHandler implements javax.security.auth.callback.CallbackHandler {

            private final String username;
            private final String password;

            public CallbackHandler(String username, String password) {
                this.username = username;
                this.password = password;
            }

            public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
                for (Callback current : callbacks) {
                    if (current instanceof NameCallback) {
                        NameCallback ncb = (NameCallback) current;
                        ncb.setName(this.username);
                    } else if (current instanceof PasswordCallback) {
                        PasswordCallback pcb = (PasswordCallback) current;
                        pcb.setPassword(this.password.toCharArray());
                    } else if (current instanceof RealmCallback) {
                        RealmCallback rcb = (RealmCallback) current;
                        rcb.setText(rcb.getDefaultText());
                    } else {
                        throw new UnsupportedCallbackException(current);
                    }
                }
            }
        }
    }
}
