/*
 * JBoss, Home of Professional Open Source
 * Copyright 2010, Red Hat Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.as.test.patching;

import static org.jboss.as.patching.Constants.BASE;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.jboss.as.controller.client.helpers.ClientConstants;
import org.jboss.as.controller.descriptions.ModelDescriptionConstants;
import org.jboss.as.test.integration.management.util.CLIOpResult;
import org.jboss.as.test.integration.management.util.CLIWrapper;
import org.jboss.dmr.ModelNode;
import org.jboss.logging.Logger;

/**
 * @author Jan Martiska
 */
public class CliUtilsForPatching {

    private static final Logger logger = Logger.getLogger(CliUtilsForPatching.class);

    public static final String OVERRIDE_ALL = "--override-all";
    public static final String OVERRIDE_MODULES = "--override-modules";
    public static final String OVERRIDE = "--override=%s";
    public static final String PRESERVE = "--preserve=%s";
    public static final String KEEP_CONFIGURATION = "--keep-configuration";
    public static final String ROLLBACK_TO = "--rollback-to";


    /**
     * Use the CLI to apply a patch
     *
     * @param patchFilePath absolute path to the ZIP file containing the patch
     * @param args          conflict resolution arguments or null
     * @throws Exception
     */
    public static boolean applyPatch(String patchFilePath, String... args) throws Exception {
        try (CLIWrapper cli = new CLIWrapper(true)) {
            StringBuilder builder = new StringBuilder("patch apply");
            if (args != null) {
                for (String arg : args) {
                    builder.append(" ").append(arg);
                }
            }
            builder.append(" ").append(patchFilePath);
            String command = builder.toString();
            logger.debug("----- sending command to CLI: " + command + " -----");
            return cli.sendLine(command, true);
        }
    }

    /**
     * Use the CLI to rollback a patch
     *
     * @param oneoffPatchID the ID of the patch that should be rolled back
     * @param args          conflict resolution arguments, rollback arguments
     * @throws Exception
     */
    public static boolean rollbackPatch(String oneoffPatchID, String... args) throws Exception {
        try (CLIWrapper cli = new CLIWrapper(true)) {
            StringBuilder builder = new StringBuilder("patch rollback --reset-configuration=false");
            if (args != null) {
                for (String arg : args) {
                    builder.append(" ").append(arg);
                }
            }
            builder.append(" --patch-id=").append(oneoffPatchID);
            String command = builder.toString();
            logger.debug("----- sending command to CLI: " + command + " -----");
            return cli.sendLine(command, true);
        }
    }

    /**
     * Use the CLI to rollback to a patch
     *
     * @param oneoffPatchID the ID of the patch that should be rolled back to
     * @param args          conflict resolution arguments, rollback arguments
     * @throws Exception
     */
    public static boolean rollbackToPatch(String oneoffPatchID, String... args) throws Exception {
        try (CLIWrapper cli = new CLIWrapper(true)) {
            StringBuilder builder = new StringBuilder("patch rollback --reset-configuration=false --rollback-to=true");
            if (args != null) {
                for (String arg : args) {
                    builder.append(" ").append(arg);
                }
            }
            builder.append(" --patch-id=").append(oneoffPatchID);
            String command = builder.toString();
            logger.debug("----- sending command to CLI: " + command + " -----");
            return cli.sendLine(command, true);
        }
    }

    /**
     * Use the CLI to read information about the installed patches
     *
     * @return output of "patch info" command or null if output is empty
     * @throws Exception
     */
    public static ModelNode info() throws Exception {
        return info(true);
    }

    /**
     * Use the CLI to read information about the installed patches
     *
     * @param connect to the server
     * @return output of "patch info" command or null if output is empty
     * @throws Exception
     */
    public static ModelNode info(boolean connect) throws Exception {
        try (CLIWrapper cli = new CLIWrapper(connect)) {
            String command = "patch info --json-output";
            logger.debug("----- sending command to CLI: " + command + " -----");
            cli.sendLine(command);
            String output = cli.readOutput();
            return ModelNode.fromJSONString(output);
        }
    }


    /**
     * Use CLI to get the list of currently installed patches
     *
     * @return the currently installed patches as a collection of patch IDs (strings)
     * @throws Exception
     */
    public static Collection<String> getInstalledPatches() throws Exception {
        try (CLIWrapper cli = new CLIWrapper(true)) {
            String command = "patch info --json-output";
            logger.debug("----- sending command to CLI: " + command + " -----");
            cli.sendLine(command);
            String response = cli.readOutput();
            ModelNode responseNode = ModelNode.fromJSONString(response);
            List<ModelNode> patchesList = responseNode.get("result").get("patches").asList();
            List<String> patchesListString = new ArrayList<String>();
            for (ModelNode n : patchesList) {
                patchesListString.add(n.asString());
            }
            return patchesListString;
        }
    }

    /**
     * Use CLI to get the cumulative patch id
     *
     * @return the current cumulative patch id (string)
     * @throws Exception
     */
    public static String getCumulativePatchId() throws Exception {
        try (CLIWrapper cli = new CLIWrapper(true)) {
            cli.sendLine("patch info --json-output");
            String response = cli.readOutput();
            ModelNode responseNode = ModelNode.fromJSONString(response);
            return responseNode.get("result").get("cumulative-patch-id").asString();
        }
    }

    /**
     * Check if the server is in restart-required state, that means
     * management operation return "response-headers" : {"process-state" : "restart-required"}
     *
     * @return true if the server is in "restart-required" state
     * @throws Exception
     */
    public static boolean doesServerRequireRestart() throws Exception {
        try (CLIWrapper cli = new CLIWrapper(true)) {
            cli.sendLine("patch info --json-output", true);
            String response = cli.readOutput();
            ModelNode responseNode = ModelNode.fromJSONString(response);
            ModelNode respHeaders = responseNode.get("response-headers");
            if (respHeaders != null && respHeaders.isDefined()) {
                ModelNode processState = respHeaders.get("process-state");
                return processState != null && processState.isDefined() && processState.asString()
                        .equals(ClientConstants.CONTROLLER_PROCESS_STATE_RESTART_REQUIRED);
            } else {
                return false;
            }
        }
    }

    /**
     * Rollback all installed oneoffs, offline!!
     *
     * @return true if operation was successful or false if at least one rollback failed
     * @throws Exception
     */
    public static boolean rollbackAllOneOffs() throws Exception {
        boolean success = true;
        final String infoCommand = "patch info --distribution=%s --json-output";
        final String rollbackCommand = "patch rollback --patch-id=%s --distribution=%s --reset-configuration=true --override-all";
        try (CLIWrapper cli = new CLIWrapper(false)) {
            String command = String.format(infoCommand, PatchingTestUtil.AS_DISTRIBUTION);
            logger.debug("----- sending command to CLI: " + command + " -----");
            cli.sendLine(command);
            String response = cli.readOutput();
            ModelNode responseNode = ModelNode.fromJSONString(response);
            List<ModelNode> patchesList = responseNode.get("result").get("patches").asList();
            for (ModelNode n : patchesList) {
                command = String.format(rollbackCommand, n.asString(), PatchingTestUtil.AS_DISTRIBUTION);
                logger.debug("----- sending command to CLI: " + command + " -----");
                success = success && cli.sendLine(command, true);
            }
            return success;
        }
    }

    /**
     * Rollback cumulative patch, online!!
     *
     * @param resetConfiguration
     * @return true if operation was successful or false if at least one rollback failed
     * @throws Exception
     */
    public static boolean rollbackCumulativePatch(boolean resetConfiguration) throws Exception {
        final String infoCommand = "patch info --json-output";
        final String rollbackCommand = "patch rollback --patch-id=%s --reset-configuration=%s";
        try (CLIWrapper cli = new CLIWrapper(true)) {
            String command = infoCommand;
            logger.debug("----- sending command to CLI: " + command + " -----");
            cli.sendLine(command);
            String response = cli.readOutput();
            ModelNode responseNode = ModelNode.fromJSONString(response);
            String cumulativePatchId = responseNode.get("result").get("cumulative-patch-id").asString();
            command = String.format(rollbackCommand, cumulativePatchId, resetConfiguration);
            logger.debug("----- sending command to CLI: " + command + " -----");
            return cli.sendLine(command, true);
        }
    }

    /**
     * Rollback cumulative patch and all picked up one-offs, offline
     *
     * @return true if operation was successful or false if at least one rollback failed
     * @throws Exception
     */
    public static boolean rollbackAll() throws Exception {
        CLIWrapper cli = null;
        boolean success = true;
        try {
            cli = new CLIWrapper(false);
            cli.sendLine("patch info --streams --json-output");
            final ModelNode response = ModelNode.fromJSONString(cli.readOutput());
            for(ModelNode stream : response.get(ModelDescriptionConstants.RESULT).asList()) {
                success = success && rollbackAll(cli, stream.asString());
            }
            return success;
        } finally {
            if (cli != null) {
                cli.quit();
            }
        }
    }

    protected static boolean rollbackAll(CLIWrapper cli, String patchStream) {
        final String infoCommand = "patch info --patch-stream=%s --distribution=%s --json-output";
        final String rollbackCommand = "patch rollback --patch-stream=%s --patch-id=%s --distribution=%s --reset-configuration=true";
        boolean success = true;
        boolean doRollback = true;
        while (doRollback) {
            doRollback = false;
            String command = String.format(infoCommand, patchStream, PatchingTestUtil.AS_DISTRIBUTION);
            logger.debug("----- sending command to CLI: " + command + " -----");
            cli.sendLine(command);
            String response = cli.readOutput();
            ModelNode responseNode = ModelNode.fromJSONString(response);
            ModelNode result = responseNode.get("result");
            if (result.has("patches")) {
                final List<ModelNode> patchesList = result.get("patches").asList();
                if (!patchesList.isEmpty()) {
                    doRollback = true;
                    for (ModelNode n : patchesList) {
                        command = String.format(rollbackCommand, patchStream, n.asString(), PatchingTestUtil.AS_DISTRIBUTION);
                        logger.debug("----- sending command to CLI: " + command + " -----");
                        success = success && cli.sendLine(command, true);
                    }
                }
            }
            if (result.has("cumulative-patch-id")) {
                final String cumulativePatchId = result.get("cumulative-patch-id").asString();
                if (!cumulativePatchId.equalsIgnoreCase(BASE)) {
                    doRollback = true;
                    command = String.format(rollbackCommand, patchStream, cumulativePatchId, PatchingTestUtil.AS_DISTRIBUTION);
                    logger.debug("----- sending command to CLI: " + command + " -----");
                    success = success && cli.sendLine(command, true);
                }
            }
        }
        return success;
    }

    public static List<String> getResourceLoaderPathsForModule(String module, boolean ignoreError)
            throws Exception {
        try(CLIWrapper cli = new CLIWrapper(true)) {
            String command = "/core-service=module-loading:list-resource-loader-paths(module=" + module + ")";
            logger.debug("CLI command: " + command);
            if (!cli.sendLine(command, ignoreError)) {
                throw new RuntimeException(cli.readOutput());
            }
            ModelNode response = ModelNode.fromString(cli.readOutput());
            List<ModelNode> pathList = response.get("result").asList();
            List<String> patchesListString = new ArrayList<String>();
            for (ModelNode n : pathList) {
                patchesListString.add(n.asString());
            }
            return patchesListString;
        }
    }

    public static List<ModelNode> getPatchingHistory() throws Exception {
        try(CLIWrapper cli = new CLIWrapper(true)) {
            String command = "/core-service=patching:show-history";
            logger.debug("CLI command: " + command);
            cli.sendLine(command, false);
            ModelNode response = ModelNode.fromString(cli.readOutput());
            return response.get("result").asList();
        }
    }

    public static boolean doesModuleExist(String module) throws Exception {
        try(CLIWrapper cli = new CLIWrapper(true)) {
            cli.sendLine("/core-service=module-loading:list-resource-loader-paths(module=" + module + ")",
                    true);
            final CLIOpResult result = cli.readAllAsOpResult();
            return result.isIsOutcomeSuccess();
        }
    }

}