/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.ignite.internal.commandline;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.apache.ignite.cluster.ClusterNode;
import org.apache.ignite.internal.IgniteFeatures;
import org.apache.ignite.internal.client.GridClient;
import org.apache.ignite.internal.client.GridClientConfiguration;
import org.apache.ignite.internal.client.GridClientException;
import org.apache.ignite.internal.commandline.argument.CommandArgUtils;
import org.apache.ignite.internal.processors.cache.version.GridCacheVersion;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.visor.tx.FetchNearXidVersionTask;
import org.apache.ignite.internal.visor.tx.TxKeyLockType;
import org.apache.ignite.internal.visor.tx.TxMappingType;
import org.apache.ignite.internal.visor.tx.TxVerboseId;
import org.apache.ignite.internal.visor.tx.TxVerboseInfo;
import org.apache.ignite.internal.visor.tx.TxVerboseKey;
import org.apache.ignite.internal.visor.tx.VisorTxInfo;
import org.apache.ignite.internal.visor.tx.VisorTxOperation;
import org.apache.ignite.internal.visor.tx.VisorTxProjection;
import org.apache.ignite.internal.visor.tx.VisorTxSortOrder;
import org.apache.ignite.internal.visor.tx.VisorTxTask;
import org.apache.ignite.internal.visor.tx.VisorTxTaskArg;
import org.apache.ignite.internal.visor.tx.VisorTxTaskResult;
import org.apache.ignite.transactions.TransactionState;

import static org.apache.ignite.internal.client.util.GridClientUtils.checkFeatureSupportedByCluster;
import static org.apache.ignite.internal.commandline.CommandList.TX;
import static org.apache.ignite.internal.commandline.CommandLogger.DOUBLE_INDENT;
import static org.apache.ignite.internal.commandline.CommandLogger.optional;
import static org.apache.ignite.internal.commandline.CommandLogger.or;
import static org.apache.ignite.internal.commandline.CommonArgParser.CMD_AUTO_CONFIRMATION;
import static org.apache.ignite.internal.commandline.TaskExecutor.executeTask;
import static org.apache.ignite.internal.commandline.TxCommandArg.TX_INFO;

/**
 * Transaction commands.
 */
public class TxCommands extends AbstractCommand<VisorTxTaskArg> {
    /** Arguments */
    private VisorTxTaskArg args;

    /** Logger. */
    private Logger logger;

    /** {@inheritDoc} */
    @Override public void printUsage(Logger logger) {
        Command.usage(logger, "List or kill transactions:", TX, getTxOptions());
        Command.usage(logger, "Print detailed information (topology and key lock ownership) about specific transaction:",
            TX, TX_INFO.argName(), or("<TX identifier as GridCacheVersion [topVer=..., order=..., nodeOrder=...] " +
                "(can be found in logs)>", "<TX identifier as UUID (can be retrieved via --tx command)>"));

    }

    /**
     * @return Transaction command options.
     */
    private String[] getTxOptions() {
        List<String> list = new ArrayList<>();

        list.add(optional(TxCommandArg.TX_XID, "XID"));
        list.add(optional(TxCommandArg.TX_DURATION, "SECONDS"));
        list.add(optional(TxCommandArg.TX_SIZE, "SIZE"));
        list.add(optional(TxCommandArg.TX_LABEL, "PATTERN_REGEX"));
        list.add(optional(or(TxCommandArg.TX_SERVERS, TxCommandArg.TX_CLIENTS)));
        list.add(optional(TxCommandArg.TX_NODES, "consistentId1[,consistentId2,....,consistentIdN]"));
        list.add(optional(TxCommandArg.TX_LIMIT, "NUMBER"));
        list.add(optional(TxCommandArg.TX_ORDER, or(VisorTxSortOrder.values())));
        list.add(optional(TxCommandArg.TX_KILL));
        list.add(optional(TX_INFO));
        list.add(optional(CMD_AUTO_CONFIRMATION));

        return list.toArray(new String[list.size()]);
    }

    /** {@inheritDoc} */
    @Override public VisorTxTaskArg arg() {
        return args;
    }

    /**
     * Dump transactions information.
     *
     * @param clientCfg Client configuration.
     */
    @Override public Object execute(GridClientConfiguration clientCfg, Logger logger) throws Exception {
        this.logger = logger;

        try (GridClient client = Command.startClient(clientCfg)) {
            if (args.getOperation() == VisorTxOperation.INFO)
                return transactionInfo(client, clientCfg);

            Map<ClusterNode, VisorTxTaskResult> res = executeTask(client, VisorTxTask.class, args, clientCfg);

            if (res.isEmpty())
                logger.info("Nothing found.");
            else if (args.getOperation() == VisorTxOperation.KILL)
                logger.info("Killed transactions:");
            else
                logger.info("Matching transactions:");

            for (Map.Entry<ClusterNode, VisorTxTaskResult> entry : res.entrySet()) {
                if (entry.getValue().getInfos().isEmpty())
                    continue;

                ClusterNode key = entry.getKey();

                logger.info(key.getClass().getSimpleName() + " [id=" + key.id() +
                    ", addrs=" + key.addresses() +
                    ", order=" + key.order() +
                    ", ver=" + key.version() +
                    ", isClient=" + key.isClient() +
                    ", consistentId=" + key.consistentId() +
                    "]");

                for (VisorTxInfo info : entry.getValue().getInfos())
                    logger.info(info.toUserString());
            }

            return res;
        }
        catch (Throwable e) {
            logger.severe("Failed to perform operation.");
            logger.severe(CommandLogger.errorMessage(e));

            throw e;
        }
    }

    /**
     * Dump transactions information.
     *
     * @param client Client.
     */
    private void transactions(GridClient client, GridClientConfiguration conf) throws GridClientException {
        try {
            if (args.getOperation() == VisorTxOperation.INFO) {
                transactionInfo(client, conf);

                return;
            }

            Map<ClusterNode, VisorTxTaskResult> res = executeTask(client, VisorTxTask.class, args, conf);

            for (Map.Entry<ClusterNode, VisorTxTaskResult> entry : res.entrySet()) {
                if (entry.getValue().getInfos().isEmpty())
                    continue;

                ClusterNode key = entry.getKey();

                logger.info(nodeDescription(key));

                for (VisorTxInfo info : entry.getValue().getInfos())
                    logger.info(info.toUserString());
            }
        }
        catch (Throwable e) {
            logger.severe("Failed to perform operation.");

            throw e;
        }
    }

    /** {@inheritDoc} */
    @Override public String confirmationPrompt() {
        if (args != null && args.getOperation() == VisorTxOperation.KILL)
            return "Warning: the command will kill some transactions.";

        return null;
    }

    /**
     * @param argIter Argument iterator.
     */
    @Override public void parseArguments(CommandArgIterator argIter) {
        VisorTxProjection proj = null;

        Integer limit = null;

        VisorTxSortOrder sortOrder = null;

        Long duration = null;

        Integer size = null;

        String lbRegex = null;

        List<String> consistentIds = null;

        VisorTxOperation op = VisorTxOperation.LIST;

        String xid = null;

        TxVerboseId txVerboseId = null;

        while (true) {
            String str = argIter.peekNextArg();

            if (str == null)
                break;

            TxCommandArg arg = CommandArgUtils.of(str, TxCommandArg.class);

            if (arg == null)
                break;

            switch (arg) {
                case TX_LIMIT:
                    argIter.nextArg("");

                    limit = (int)argIter.nextNonNegativeLongArg(TxCommandArg.TX_LIMIT.toString());

                    break;

                case TX_ORDER:
                    argIter.nextArg("");

                    sortOrder = VisorTxSortOrder.valueOf(argIter.nextArg(TxCommandArg.TX_ORDER.toString()).toUpperCase());

                    break;

                case TX_SERVERS:
                    argIter.nextArg("");

                    proj = VisorTxProjection.SERVER;
                    break;

                case TX_CLIENTS:
                    argIter.nextArg("");

                    proj = VisorTxProjection.CLIENT;
                    break;

                case TX_NODES:
                    argIter.nextArg("");

                    Set<String> ids = argIter.nextStringSet(TxCommandArg.TX_NODES.toString());

                    if (ids.isEmpty()) {
                        throw new IllegalArgumentException("Consistent id list is empty.");
                    }

                    consistentIds = new ArrayList<>(ids);
                    break;

                case TX_DURATION:
                    argIter.nextArg("");

                    duration = argIter.nextNonNegativeLongArg(TxCommandArg.TX_DURATION.toString()) * 1000L;
                    break;

                case TX_SIZE:
                    argIter.nextArg("");

                    size = (int)argIter.nextNonNegativeLongArg(TxCommandArg.TX_SIZE.toString());
                    break;

                case TX_LABEL:
                    argIter.nextArg("");

                    lbRegex = argIter.nextArg(TxCommandArg.TX_LABEL.toString());

                    try {
                        Pattern.compile(lbRegex);
                    }
                    catch (PatternSyntaxException ignored) {
                        throw new IllegalArgumentException("Illegal regex syntax");
                    }

                    break;

                case TX_XID:
                    argIter.nextArg("");

                    xid = argIter.nextArg(TxCommandArg.TX_XID.toString());
                    break;

                case TX_KILL:
                    argIter.nextArg("");

                    op = VisorTxOperation.KILL;
                    break;

                case TX_INFO:
                    argIter.nextArg("");

                    op = VisorTxOperation.INFO;

                    txVerboseId = TxVerboseId.fromString(argIter.nextArg(TX_INFO.argName()));

                    break;

                default:
                    throw new AssertionError();
            }
        }

        if (proj != null && consistentIds != null)
            throw new IllegalArgumentException("Projection can't be used together with list of consistent ids.");

        this.args = new VisorTxTaskArg(op, limit, duration, size, null, proj,
            consistentIds, xid, lbRegex, sortOrder, txVerboseId);
    }

    /**
     * Provides text descrition of a cluster node.
     *
     * @param node Node.
     */
    private static String nodeDescription(ClusterNode node) {
        return node.getClass().getSimpleName() + " [id=" + node.id() +
            ", addrs=" + node.addresses() +
            ", order=" + node.order() +
            ", ver=" + node.version() +
            ", isClient=" + node.isClient() +
            ", consistentId=" + node.consistentId() +
            "]";
    }

    /**
     * Executes --tx --info command.
     *
     * @param client Client.
     */
    private Object transactionInfo(GridClient client, GridClientConfiguration conf) throws GridClientException {
        checkFeatureSupportedByCluster(client, IgniteFeatures.TX_INFO_COMMAND, true, true);

        GridCacheVersion nearXidVer = executeTask(client, FetchNearXidVersionTask.class, args.txInfoArgument(), conf);

        boolean histMode = false;

        if (nearXidVer != null) {
            logger.info("Resolved transaction near XID version: " + nearXidVer);

            args.txInfoArgument(new TxVerboseId(null, nearXidVer));
        }
        else {
            logger.info("Active transactions not found.");

            if (args.txInfoArgument().gridCacheVersion() != null) {
                logger.info("Will try to peek history to find out whether transaction was committed / rolled back.");

                histMode = true;
            }
            else {
                logger.info("You can specify transaction in GridCacheVersion format in order to peek history " +
                    "to find out whether transaction was committed / rolled back.");

                return null;
            }
        }

        Map<ClusterNode, VisorTxTaskResult> res = executeTask(client, VisorTxTask.class, args, conf);

        if (histMode)
            printTxInfoHistoricalResult(res);
        else
            printTxInfoResult(res);

        return res;
    }

    /**
     * Prints result of --tx --info command to output.
     *
     * @param res Response.
     */
    private void printTxInfoResult(Map<ClusterNode, VisorTxTaskResult> res) {
        String lb = null;

        Map<Integer, String> usedCaches = new HashMap<>();
        Map<Integer, String> usedCacheGroups = new HashMap<>();
        VisorTxInfo firstInfo = null;
        TxVerboseInfo firstVerboseInfo = null;
        Set<TransactionState> states = new HashSet<>();

        for (Map.Entry<ClusterNode, VisorTxTaskResult> entry : res.entrySet()) {
            for (VisorTxInfo info : entry.getValue().getInfos()) {
                assert info.getTxVerboseInfo() != null;

                if (lb == null)
                    lb = info.getLabel();

                if (firstInfo == null) {
                    firstInfo = info;
                    firstVerboseInfo = info.getTxVerboseInfo();
                }

                usedCaches.putAll(info.getTxVerboseInfo().usedCaches());
                usedCacheGroups.putAll(info.getTxVerboseInfo().usedCacheGroups());
                states.add(info.getState());
            }
        }

        String indent = "";

        logger.info("");
        logger.info(indent + "Transaction detailed info:");

        printTransactionDetailedInfo(
            res, usedCaches, usedCacheGroups, firstInfo, firstVerboseInfo, states, indent + DOUBLE_INDENT);
    }

    /**
     * Prints detailed info about transaction to output.
     *
     * @param res Response.
     * @param usedCaches Used caches.
     * @param usedCacheGroups Used cache groups.
     * @param firstInfo First info.
     * @param firstVerboseInfo First verbose info.
     * @param states States.
     * @param indent Indent.
     */
    private void printTransactionDetailedInfo(Map<ClusterNode, VisorTxTaskResult> res, Map<Integer, String> usedCaches,
        Map<Integer, String> usedCacheGroups, VisorTxInfo firstInfo, TxVerboseInfo firstVerboseInfo,
        Set<TransactionState> states, String indent) {
        logger.info(indent + "Near XID version: " + firstVerboseInfo.nearXidVersion());
        logger.info(indent + "Near XID version (UUID): " + firstInfo.getNearXid());
        logger.info(indent + "Isolation: " + firstInfo.getIsolation());
        logger.info(indent + "Concurrency: " + firstInfo.getConcurrency());
        logger.info(indent + "Timeout: " + firstInfo.getTimeout());
        logger.info(indent + "Initiator node: " + firstVerboseInfo.nearNodeId());
        logger.info(indent + "Initiator node (consistent ID): " + firstVerboseInfo.nearNodeConsistentId());
        logger.info(indent + "Label: " + firstInfo.getLabel());
        logger.info(indent + "Topology version: " + firstInfo.getTopologyVersion());
        logger.info(indent + "Used caches (ID to name): " + usedCaches);
        logger.info(indent + "Used cache groups (ID to name): " + usedCacheGroups);
        logger.info(indent + "States across the cluster: " + states);
        logger.info(indent + "Transaction topology: ");

        printTransactionTopology(res, indent + DOUBLE_INDENT);
    }

    /**
     * Prints transaction topology to output.
     *
     * @param res Response.
     * @param indent Indent.
     */
    private void printTransactionTopology(Map<ClusterNode, VisorTxTaskResult> res, String indent) {
        for (Map.Entry<ClusterNode, VisorTxTaskResult> entry : res.entrySet()) {
            logger.info(indent + nodeDescription(entry.getKey()) + ':');

            printTransactionMappings(indent + DOUBLE_INDENT, entry);
        }
    }

    /**
     * Prints transaction mappings for specific cluster node to output.
     *
     * @param indent Indent.
     * @param entry Entry.
     */
    private void printTransactionMappings(String indent, Map.Entry<ClusterNode, VisorTxTaskResult> entry) {
        for (VisorTxInfo info : entry.getValue().getInfos()) {
            TxVerboseInfo verboseInfo = info.getTxVerboseInfo();

            if (verboseInfo != null) {
                logger.info(indent + "Mapping [type=" + verboseInfo.txMappingType() + "]:");

                printTransactionMapping(indent + DOUBLE_INDENT, info, verboseInfo);
            }
            else {
                logger.info(indent + "Mapping [type=HISTORICAL]:");

                logger.info(indent + DOUBLE_INDENT + "State: " + info.getState());
            }
        }
    }

    /**
     * Prints specific transaction mapping to output.
     *
     * @param indent Indent.
     * @param info Info.
     * @param verboseInfo Verbose info.
     */
    private void printTransactionMapping(String indent, VisorTxInfo info, TxVerboseInfo verboseInfo) {
        logger.info(indent + "XID version (UUID): " + info.getXid());
        logger.info(indent + "State: " + info.getState());

        if (verboseInfo.txMappingType() == TxMappingType.REMOTE) {
            logger.info(indent + "Primary node: " + verboseInfo.dhtNodeId());
            logger.info(indent + "Primary node (consistent ID): " + verboseInfo.dhtNodeConsistentId());
        }

        if (!F.isEmpty(verboseInfo.localTxKeys())) {
            logger.info(indent + "Mapped keys:");

            printTransactionKeys(indent + DOUBLE_INDENT, verboseInfo);
        }
    }

    /**
     * Prints keys of specific transaction mapping to output.
     *
     * @param indent Indent.
     * @param verboseInfo Verbose info.
     */
    private void printTransactionKeys(String indent, TxVerboseInfo verboseInfo) {
        for (TxVerboseKey txVerboseKey : verboseInfo.localTxKeys()) {
            logger.info(indent + (txVerboseKey.read() ? "Read" : "Write") +
                " [lock=" + txVerboseKey.lockType() + "]: " + txVerboseKey.txKey());

            if (txVerboseKey.lockType() == TxKeyLockType.AWAITS_LOCK)
                logger.info(indent + DOUBLE_INDENT + "Lock owner XID: " + txVerboseKey.ownerVersion());
        }
    }

    /**
     * Prints results of --tx --info to output in case requested transaction is not active.
     *
     * @param res Response.
     */
    private void printTxInfoHistoricalResult(Map<ClusterNode, VisorTxTaskResult> res) {
        if (F.isEmpty(res))
            logger.info("Transaction was not found in history across the cluster.");
        else {
            logger.info("Transaction was found in completed versions history of the following nodes:");

            for (Map.Entry<ClusterNode, VisorTxTaskResult> entry : res.entrySet()) {
                logger.info(DOUBLE_INDENT + nodeDescription(entry.getKey()) + ':');
                logger.info(DOUBLE_INDENT + DOUBLE_INDENT + "State: " + entry.getValue().getInfos().get(0).getState());
            }
        }
    }

    /** {@inheritDoc} */
    @Override public String name() {
        return TX.toCommandName();
    }
}
