/*
 * 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.cache;

import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Logger;
import org.apache.ignite.internal.client.GridClient;
import org.apache.ignite.internal.client.GridClientConfiguration;
import org.apache.ignite.internal.commandline.AbstractCommand;
import org.apache.ignite.internal.commandline.Command;
import org.apache.ignite.internal.commandline.CommandArgIterator;
import org.apache.ignite.internal.commandline.CommandLogger;
import org.apache.ignite.internal.commandline.argument.CommandArgUtils;
import org.apache.ignite.internal.commandline.cache.argument.ValidateIndexesCommandArg;
import org.apache.ignite.internal.processors.cache.verify.PartitionKey;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.internal.visor.verify.IndexIntegrityCheckIssue;
import org.apache.ignite.internal.visor.verify.IndexValidationIssue;
import org.apache.ignite.internal.visor.verify.ValidateIndexesCheckSizeIssue;
import org.apache.ignite.internal.visor.verify.ValidateIndexesCheckSizeResult;
import org.apache.ignite.internal.visor.verify.ValidateIndexesPartitionResult;
import org.apache.ignite.internal.visor.verify.VisorValidateIndexesJobResult;
import org.apache.ignite.internal.visor.verify.VisorValidateIndexesTaskArg;
import org.apache.ignite.internal.visor.verify.VisorValidateIndexesTaskResult;

import static org.apache.ignite.internal.commandline.CommandLogger.DOUBLE_INDENT;
import static org.apache.ignite.internal.commandline.CommandLogger.INDENT;
import static org.apache.ignite.internal.commandline.CommandLogger.join;
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.TaskExecutor.executeTaskByNameOnNode;
import static org.apache.ignite.internal.commandline.cache.CacheCommands.OP_NODE_ID;
import static org.apache.ignite.internal.commandline.cache.CacheCommands.usageCache;
import static org.apache.ignite.internal.commandline.cache.CacheSubcommands.VALIDATE_INDEXES;
import static org.apache.ignite.internal.commandline.cache.argument.ValidateIndexesCommandArg.CHECK_CRC;
import static org.apache.ignite.internal.commandline.cache.argument.ValidateIndexesCommandArg.CHECK_FIRST;
import static org.apache.ignite.internal.commandline.cache.argument.ValidateIndexesCommandArg.CHECK_SIZES;
import static org.apache.ignite.internal.commandline.cache.argument.ValidateIndexesCommandArg.CHECK_THROUGH;

/**
 * Validate indexes command.
 */
public class CacheValidateIndexes extends AbstractCommand<CacheValidateIndexes.Arguments> {
    /** {@inheritDoc} */
    @Override public void printUsage(Logger logger) {
        String CACHES = "cacheName1,...,cacheNameN";
        String description = "Validates indexes for the specified caches/cache groups on an idle cluster " +
            "on all or specified cluster nodes. " + VALIDATE_INDEXES + " checks consistence between primary/secondary " +
            "indexes against each other and data entries.";

        Map<String, String> map = U.newLinkedHashMap(16);

        map.put(CHECK_FIRST + " N", "validate only the first N keys");
        map.put(CHECK_THROUGH + " K", "validate every Kth key");
        map.put(CHECK_CRC.toString(), "check the CRC-sum of pages stored on disk");
        map.put(CHECK_SIZES.toString(), "check that index size and cache size are the same");

        usageCache(
            logger,
            VALIDATE_INDEXES,
            description,
            map,
            optional(CACHES),
            OP_NODE_ID,
            optional(or(CHECK_FIRST + " N", CHECK_THROUGH + " K", CHECK_CRC, CHECK_SIZES))
        );
    }

    /**
     * Container for command arguments.
     */
    public class Arguments {
         /** Caches. */
        private final Set<String> caches;

        /** Node id. */
        private final UUID nodeId;

        /** Max number of entries to be checked. */
        private final int checkFirst;

        /** Number of entries to check through. */
        private final int checkThrough;

        /** Check CRC. */
        private final boolean checkCrc;

        /** Check that index size and cache size are same. */
        private final boolean checkSizes;

        /**
         * Constructor.
         *
         * @param caches Caches.
         * @param nodeId Node id.
         * @param checkFirst Max number of entries to be checked.
         * @param checkThrough Number of entries to check through.
         * @param checkCrc Check CRC.
         * @param checkSizes Check that index size and cache size are same.
         */
        public Arguments(
            Set<String> caches,
            UUID nodeId,
            int checkFirst,
            int checkThrough,
            boolean checkCrc,
            boolean checkSizes
        ) {
            this.caches = caches;
            this.nodeId = nodeId;
            this.checkFirst = checkFirst;
            this.checkThrough = checkThrough;
            this.checkCrc = checkCrc;
            this.checkSizes = checkSizes;
        }

        /**
         * @return Caches.
         */
        public Set<String> caches() {
            return caches;
        }

        /**
         * @return Max number of entries to be checked.
         */
        public int checkFirst() {
            return checkFirst;
        }

        /**
         * @return Number of entries to check through.
         */
        public int checkThrough() {
            return checkThrough;
        }

        /**
         * @return Node id.
         */
        public UUID nodeId() {
            return nodeId;
        }

        /**
         * @return Check CRC.
         */
        public boolean checkCrc() {
            return checkCrc;
        }

        /**
         * Returns whether to check that index size and cache size are same.
         *
         * @return {@code true} if need check that index size and cache size
         *      are same.
         */
        public boolean checkSizes() {
            return checkSizes;
        }
    }

    /** Command parsed arguments. */
    private Arguments args;

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

    /** {@inheritDoc} */
    @Override public Object execute(GridClientConfiguration clientCfg, Logger logger) throws Exception {
        VisorValidateIndexesTaskArg taskArg = new VisorValidateIndexesTaskArg(
            args.caches(),
            args.nodeId() != null ? Collections.singleton(args.nodeId()) : null,
            args.checkFirst(),
            args.checkThrough(),
            args.checkCrc(),
            args.checkSizes()
        );

        try (GridClient client = Command.startClient(clientCfg)) {
            VisorValidateIndexesTaskResult taskRes = executeTaskByNameOnNode(
                client, "org.apache.ignite.internal.visor.verify.VisorValidateIndexesTask", taskArg, null, clientCfg);

            boolean errors = CommandLogger.printErrors(taskRes.exceptions(), "Index validation failed on nodes:", logger);

            for (Entry<UUID, VisorValidateIndexesJobResult> nodeEntry : taskRes.results().entrySet()) {
                VisorValidateIndexesJobResult jobRes = nodeEntry.getValue();

                if (!jobRes.hasIssues())
                    continue;

                errors = true;

                logger.info("Index issues found on node " + nodeEntry.getKey() + ":");

                for (IndexIntegrityCheckIssue is : jobRes.integrityCheckFailures())
                    logger.info(INDENT + is);

                for (Entry<PartitionKey, ValidateIndexesPartitionResult> e : jobRes.partitionResult().entrySet()) {
                    ValidateIndexesPartitionResult res = e.getValue();

                    if (!res.issues().isEmpty()) {
                        logger.info(INDENT + join(" ", e.getKey(), e.getValue()));

                        for (IndexValidationIssue is : res.issues())
                            logger.info(DOUBLE_INDENT + is);
                    }
                }

                for (Entry<String, ValidateIndexesPartitionResult> e : jobRes.indexResult().entrySet()) {
                    ValidateIndexesPartitionResult res = e.getValue();

                    if (!res.issues().isEmpty()) {
                        logger.info(INDENT + join(" ", "SQL Index", e.getKey(), e.getValue()));

                        for (IndexValidationIssue is : res.issues())
                            logger.info(DOUBLE_INDENT + is);
                    }
                }

                for (Entry<String, ValidateIndexesCheckSizeResult> e : jobRes.checkSizeResult().entrySet()) {
                    ValidateIndexesCheckSizeResult res = e.getValue();
                    Collection<ValidateIndexesCheckSizeIssue> issues = res.issues();

                    if (issues.isEmpty())
                        continue;

                    logger.info(INDENT + join(" ", "Size check", e.getKey(), res));

                    for (ValidateIndexesCheckSizeIssue issue : issues)
                        logger.info(DOUBLE_INDENT + issue);
                }
            }

            if (!errors)
                logger.info("no issues found.");
            else
                logger.severe("issues found (listed above).");

            logger.info("");

            return taskRes;
        }
    }

    /** {@inheritDoc} */
    @Override public void parseArguments(CommandArgIterator argIter) {
        int checkFirst = -1;
        int checkThrough = -1;
        UUID nodeId = null;
        Set<String> caches = null;
        boolean checkCrc = false;
        boolean checkSizes = false;

        while (argIter.hasNextSubArg()) {
            String nextArg = argIter.nextArg("");

            ValidateIndexesCommandArg arg = CommandArgUtils.of(nextArg, ValidateIndexesCommandArg.class);

            if (arg == CHECK_FIRST || arg == CHECK_THROUGH) {
                if (!argIter.hasNextSubArg())
                    throw new IllegalArgumentException("Numeric value for '" + nextArg + "' parameter expected.");

                int numVal;

                String numStr = argIter.nextArg("");

                try {
                    numVal = Integer.parseInt(numStr);
                }
                catch (IllegalArgumentException e) {
                    throw new IllegalArgumentException(
                        "Not numeric value was passed for '" + nextArg + "' parameter: " + numStr
                    );
                }

                if (numVal <= 0)
                    throw new IllegalArgumentException("Value for '" + nextArg + "' property should be positive.");

                if (arg == CHECK_FIRST)
                    checkFirst = numVal;
                else
                    checkThrough = numVal;

                continue;
            }
            else if (arg == CHECK_CRC) {
                checkCrc = true;
                continue;
            }
            else if (CHECK_SIZES == arg) {
                checkSizes = true;

                continue;
            }

            try {
                nodeId = UUID.fromString(nextArg);

                continue;
            }
            catch (IllegalArgumentException ignored) {
                //No-op.
            }

            caches = argIter.parseStringSet(nextArg);
        }

        args = new Arguments(caches, nodeId, checkFirst, checkThrough, checkCrc, checkSizes);
    }

    /** {@inheritDoc} */
    @Override public String name() {
        return VALIDATE_INDEXES.text().toUpperCase();
    }
}
