/*
 * Copyright The WildFly Authors
 * SPDX-License-Identifier: Apache-2.0
 */
package org.jboss.as.cli.scriptsupport;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.Callable;

import org.jboss.as.cli.CliInitializationException;
import org.jboss.as.cli.CommandContext;
import org.jboss.as.cli.CommandContextFactory;
import org.jboss.as.cli.CommandLineException;
import org.jboss.as.cli.impl.CommandContextConfiguration;
import org.jboss.as.cli.operation.impl.DefaultCallbackHandler;
import org.jboss.as.cli.parsing.operation.OperationFormat;
import org.jboss.dmr.ModelNode;

/**
 * This class is intended to be used with JVM-based scripting languages. It acts
 * as a facade to the CLI public API, providing a single class that can be used
 * to connect, run CLI commands, and disconnect. It also removes the need to
 * catch checked exceptions.
 *
 * @author Stan Silvert ssilvert@redhat.com (C) 2012 Red Hat Inc.
 */
public class CLI {

    private CommandContext ctx;

    // This parsedCommand is required in order to determinate if the command
    // is an operation or a command
    private final DefaultCallbackHandler handler
            = new DefaultCallbackHandler(true);

    private CLI() { // only allow new instances from newInstance() method.
        initOfflineContext();
    }

    /**
     * Create a new CLI instance.
     *
     * @return The CLI instance.
     */
    public static CLI newInstance() {
        return new CLI();
    }

    /**
     * Return the CLI CommandContext. This allows a script developer full access
     * to CLI facilities if needed. CommandContext can mute during CLI lifetime.
     * An unconnected CLI instance has a context that handles offline commands.
     * When connected to a server, the original context is replaced with a
     * context allowing to interact with the remote server.
     *
     * @return The CommandContext.
     */
    public CommandContext getCommandContext() {
        return ctx;
    }

    /**
     * Connect to the server using the default host and port.
     */
    public void connect() {
        doConnect(() -> {
            return CommandContextFactory.getInstance().
                    newCommandContext(new CommandContextConfiguration.Builder()
                    .setErrorOnInteract(false)
                    .build());
        });
    }

    /**
     * Connect to the server using the default host and port.
     *
     * @param username The user name for logging in.
     * @param password The password for logging in.
     */
    public void connect(String username, char[] password) {
        doConnect(() -> {
            return CommandContextFactory.getInstance().
                    newCommandContext(new CommandContextConfiguration.Builder()
                    .setUsername(username)
                    .setPassword(password)
                    .setErrorOnInteract(false)
                    .build());
        });
    }

    /**
     * Connect to the server using a specified host and port.
     *
     * @param controllerHost The host name.
     * @param controllerPort The port.
     * @param username The user name for logging in.
     * @param password The password for logging in.
     */
    public void connect(String controller, String username, char[] password) {
        connect(controller, username, password, null);
    }

    /**
     * Connect to the server using a specified host and port.
     *
     * @param controller
     * @param username The user name for logging in.
     * @param clientBindAddress
     * @param password The password for logging in.
     */
    public void connect(String controller, String username, char[] password,
            String clientBindAddress) {
        doConnect(() -> {
            return CommandContextFactory.getInstance().
                    newCommandContext(new CommandContextConfiguration.Builder()
                            .setController(controller)
                    .setUsername(username)
                    .setPassword(password)
                    .setErrorOnInteract(false)
                    .setClientBindAddress(clientBindAddress)
                    .build());
        });
    }

    /**
     * Connect to the server using a specified host and port.
     *
     * @param controllerHost The host name.
     * @param controllerPort The port.
     * @param username The user name for logging in.
     * @param password The password for logging in.
     */
    public void connect(String controllerHost, int controllerPort,
            String username, char[] password) {
        connect("remote+http", controllerHost, controllerPort,
                username, password, null);
    }

    /**
     * Connect to the server using a specified host and port.
     *
     * @param controllerHost The host name.
     * @param controllerPort The port.
     * @param username The user name for logging in.
     * @param password The password for logging in.
     * @param clientBindAddress the client bind address.
     */
    public void connect(String controllerHost, int controllerPort,
            String username, char[] password, String clientBindAddress) {
        connect("remote+http", controllerHost, controllerPort,
                username, password, clientBindAddress);
    }

    /**
     * Connect to the server using a specified host and port.
     * @param protocol The protocol
     * @param controllerHost The host name.
     * @param controllerPort The port.
     * @param username The user name for logging in.
     * @param password The password for logging in.
     */
    public void connect(String protocol, String controllerHost,
            int controllerPort, String username, char[] password) {
        connect(protocol, controllerHost, controllerPort, username, password, null);
    }

    /**
     * Connect to the server using a specified host and port.
     * @param protocol The protocol
     * @param controllerHost The host name.
     * @param controllerPort The port.
     * @param username The user name for logging in.
     * @param password The password for logging in.
     */
    public void connect(String protocol, String controllerHost, int controllerPort,
            String username, char[] password, String clientBindAddress) {
        doConnect(() -> {
            return CommandContextFactory.getInstance().newCommandContext(
                    new CommandContextConfiguration.Builder().
                    setController(constructUri(protocol,
                            controllerHost,
                            controllerPort))
                    .setUsername(username)
                    .setPassword(password)
                    .setErrorOnInteract(false)
                    .setClientBindAddress(clientBindAddress)
                    .build());
        });
    }

    /**
     * Disconnect from the server. The current CommandContext is terminated.
     */
    public void disconnect() {
        try {
            if (!isConnected()) {
                throw new IllegalStateException("Not connected to server.");
            }
            terminate();
        } finally {
            // Back to offline context
            initOfflineContext();
        }
    }

    /**
     * Execute a CLI command. This can be any command that you might execute on
     * the CLI command line, including both server-side operations and local
     * commands such as 'cd' or 'cn'.
     *
     * @param cliCommand A CLI command.
     * @return A result object that provides all information about the execution
     * of the command.
     */
    public Result cmd(String cliCommand) {
        try {
            // The intent here is to return a Response when this is doable.
            if (ctx.isWorkflowMode() || ctx.isBatchMode()) {
                ctx.handle(cliCommand);
                return new Result(cliCommand, ctx.getExitCode());
            }
            handler.parse(ctx.getCurrentNodePath(), cliCommand, ctx);
            if (handler.getFormat() == OperationFormat.INSTANCE) {
                ModelNode request = ctx.buildRequest(cliCommand);
                ModelNode response = ctx.execute(request, cliCommand);
                return new Result(cliCommand, request, response);
            } else {
                ctx.handle(cliCommand);
                return new Result(cliCommand, ctx.getExitCode());
            }
        } catch (CommandLineException cfe) {
            throw new IllegalArgumentException("Error handling command: "
                    + cliCommand, cfe);
        } catch (IOException ioe) {
            throw new IllegalStateException("Unable to send command "
                    + cliCommand + " to server.", ioe);
        }
    }

    /**
     * Terminate the current CommandContext.
     */
    public void terminate() {
        if (!ctx.isTerminated()) {
            ctx.terminateSession();
        }
    }

    private String constructUri(final String protocol, final String host,
            final int port) {
        try {
            URI uri = new URI(protocol, null, host, port, null, null, null);
            // String the leading '//' if there is no protocol.
            return protocol == null ? uri.toString().substring(2) : uri.toString();
        } catch (URISyntaxException e) {
            throw new IllegalStateException("Unable to construct URI.", e);
        }
    }

    private void initOfflineContext() {
        try {
            ctx = CommandContextFactory.getInstance().newCommandContext();
        } catch (CliInitializationException e) {
            throw new IllegalStateException("Unable to initialize "
                    + "command context.", e);
        }
    }

    private boolean isConnected() {
        return ctx.getConnectionInfo() != null;
    }

    private void doConnect(Callable<CommandContext> callable) {
        if (isConnected()) {
            throw new IllegalStateException("Already connected to server.");
        }
        CommandContext newContext = null;
        try {
            newContext = callable.call();
            newContext.connectController();
        } catch (Exception ex) {
            if (newContext != null) {
                newContext.terminateSession();
            }
            if (ex instanceof CliInitializationException) {
                throw new IllegalStateException("Unable to initialize "
                        + "command context.", ex);
            }
            if (ex instanceof CommandLineException) {
                throw new IllegalStateException("Unable to connect "
                        + "to controller.", ex);
            }
            throw new IllegalStateException(ex);
        }
        // We have a new connected context, terminate the current one.
        terminate();
        ctx = newContext;
    }

    /**
     * The Result class provides all information about an executed CLI command.
     */
    public class Result {
        private final String cliCommand;
        private ModelNode request;
        private ModelNode response;

        private boolean isSuccess = false;
        private boolean isLocalCommand = false;

        Result(String cliCommand, ModelNode request, ModelNode response) {
            this.cliCommand = cliCommand;
            this.request = request;
            this.response = response;
            this.isSuccess = response.get("outcome").asString().equals("success");
        }

        Result(String cliCommand, int exitCode) {
            this.cliCommand = cliCommand;
            this.isSuccess = exitCode == 0;
            this.isLocalCommand = true;
        }

        /**
         * Return the original command as a String.
         * @return The original CLI command.
         */
        public String getCliCommand() {
            return this.cliCommand;
        }

        /**
         * If the command resulted in a server-side operation, return the
         * ModelNode representation of the operation.
         *
         * @return The request as a ModelNode, or <code>null</code> if this was
         * a local command.
         */
        public ModelNode getRequest() {
            return this.request;
        }

        /**
         * If the command resulted in a server-side operation, return the
         * ModelNode representation of the response.
         *
         * @return The server response as a ModelNode, or <code>null</code> if
         * this was a local command.
         */
        public ModelNode getResponse() {
            return this.response;
        }

        /**
         * Return true if the command was successful. For a server-side
         * operation, this is determined by the outcome of the operation on the
         * server side.
         *
         * @return <code>true</code> if the command was successful,
         * <code>false</code> otherwise.
         */
        public boolean isSuccess() {
            return this.isSuccess;
        }

        /**
         * Return true if the command was only executed locally and did not
         * result in a server-side operation.
         *
         * @return <code>true</code> if the command was only executed locally,
         * <code>false</code> otherwise.
         */
        public boolean isLocalCommand() {
            return this.isLocalCommand;
        }
    }
}
