/**
 * 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 com.twitter.distributedlog.tools;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.twitter.distributedlog.BKDistributedLogNamespace;
import com.twitter.distributedlog.Entry;
import com.twitter.distributedlog.logsegment.LogSegmentMetadataStore;
import com.twitter.distributedlog.namespace.DistributedLogNamespace;
import com.twitter.distributedlog.util.Utils;
import org.apache.bookkeeper.client.BKException;
import org.apache.bookkeeper.client.BookKeeper;
import org.apache.bookkeeper.client.BookKeeperAccessor;
import org.apache.bookkeeper.client.BookKeeperAdmin;
import org.apache.bookkeeper.client.LedgerEntry;
import org.apache.bookkeeper.client.LedgerHandle;
import org.apache.bookkeeper.client.LedgerMetadata;
import org.apache.bookkeeper.client.LedgerReader;
import org.apache.bookkeeper.net.BookieSocketAddress;
import org.apache.bookkeeper.proto.BookkeeperInternalCallbacks;
import org.apache.bookkeeper.util.IOUtils;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.RateLimiter;
import com.twitter.distributedlog.AsyncLogReader;
import com.twitter.distributedlog.AsyncLogWriter;
import com.twitter.distributedlog.BookKeeperClient;
import com.twitter.distributedlog.BookKeeperClientBuilder;
import com.twitter.distributedlog.DLSN;
import com.twitter.distributedlog.DistributedLogConfiguration;
import com.twitter.distributedlog.DistributedLogConstants;
import com.twitter.distributedlog.DistributedLogManager;
import com.twitter.distributedlog.exceptions.LogNotFoundException;
import com.twitter.distributedlog.LogReader;
import com.twitter.distributedlog.LogRecord;
import com.twitter.distributedlog.LogRecordWithDLSN;
import com.twitter.distributedlog.LogSegmentMetadata;
import com.twitter.distributedlog.ZooKeeperClient;
import com.twitter.distributedlog.ZooKeeperClientBuilder;
import com.twitter.distributedlog.auditor.DLAuditor;
import com.twitter.distributedlog.bk.LedgerAllocator;
import com.twitter.distributedlog.bk.LedgerAllocatorUtils;
import com.twitter.distributedlog.metadata.BKDLConfig;
import com.twitter.distributedlog.metadata.MetadataUpdater;
import com.twitter.distributedlog.metadata.LogSegmentMetadataStoreUpdater;
import com.twitter.distributedlog.util.SchedulerUtils;
import com.twitter.util.Await;

import static com.google.common.base.Charsets.UTF_8;

@SuppressWarnings("deprecation")
public class DistributedLogTool extends Tool {

    static final Logger logger = LoggerFactory.getLogger(DistributedLogTool.class);

    static final List<String> EMPTY_LIST = Lists.newArrayList();

    static int compareByCompletionTime(long time1, long time2) {
        return time1 > time2 ? 1 : (time1 < time2 ? -1 : 0);
    }

    static final Comparator<LogSegmentMetadata> LOGSEGMENT_COMPARATOR_BY_TIME = new Comparator<LogSegmentMetadata>() {
        @Override
        public int compare(LogSegmentMetadata o1, LogSegmentMetadata o2) {
            if (o1.isInProgress() && o2.isInProgress()) {
                return compareByCompletionTime(o1.getFirstTxId(), o2.getFirstTxId());
            } else if (!o1.isInProgress() && !o2.isInProgress()) {
                return compareByCompletionTime(o1.getCompletionTime(), o2.getCompletionTime());
            } else if (o1.isInProgress() && !o2.isInProgress()) {
                return compareByCompletionTime(o1.getFirstTxId(), o2.getCompletionTime());
            } else {
                return compareByCompletionTime(o1.getCompletionTime(), o2.getFirstTxId());
            }
        }
    };

    static DLSN parseDLSN(String dlsnStr) throws ParseException {
        if (dlsnStr.equals("InitialDLSN")) {
            return DLSN.InitialDLSN;
        }
        String[] parts = dlsnStr.split(",");
        if (parts.length != 3) {
            throw new ParseException("Invalid dlsn : " + dlsnStr);
        }
        try {
            return new DLSN(Long.parseLong(parts[0]), Long.parseLong(parts[1]), Long.parseLong(parts[2]));
        } catch (Exception nfe) {
            throw new ParseException("Invalid dlsn : " + dlsnStr);
        }
    }

    /**
     * Per DL Command, which parses basic options. e.g. uri.
     */
    protected abstract static class PerDLCommand extends OptsCommand {

        protected Options options = new Options();
        protected final DistributedLogConfiguration dlConf;
        protected URI uri;
        protected String zkAclId = null;
        protected boolean force = false;
        protected com.twitter.distributedlog.DistributedLogManagerFactory factory = null;

        protected PerDLCommand(String name, String description) {
            super(name, description);
            dlConf = new DistributedLogConfiguration();
            // Tools are allowed to read old metadata as long as they can interpret it
            dlConf.setDLLedgerMetadataSkipMinVersionCheck(true);
            options.addOption("u", "uri", true, "DistributedLog URI");
            options.addOption("c", "conf", true, "DistributedLog Configuration File");
            options.addOption("a", "zk-acl-id", true, "Zookeeper ACL ID");
            options.addOption("f", "force", false, "Force command (no warnings or prompts)");
        }

        @Override
        protected int runCmd(CommandLine commandLine) throws Exception {
            try {
                parseCommandLine(commandLine);
            } catch (ParseException pe) {
                System.err.println("ERROR: failed to parse commandline : '" + pe.getMessage() + "'");
                printUsage();
                return -1;
            }
            try {
                return runCmd();
            } finally {
                synchronized (this) {
                    if (null != factory) {
                        factory.close();
                    }
                }
            }
        }

        protected abstract int runCmd() throws Exception;

        @Override
        protected Options getOptions() {
            return options;
        }

        protected void parseCommandLine(CommandLine cmdline) throws ParseException {
            if (!cmdline.hasOption("u")) {
                throw new ParseException("No distributedlog uri provided.");
            }
            uri = URI.create(cmdline.getOptionValue("u"));
            if (cmdline.hasOption("c")) {
                String configFile = cmdline.getOptionValue("c");
                try {
                    dlConf.loadConf(new File(configFile).toURI().toURL());
                } catch (ConfigurationException e) {
                    throw new ParseException("Failed to load distributedlog configuration from " + configFile + ".");
                } catch (MalformedURLException e) {
                    throw new ParseException("Failed to load distributedlog configuration from " + configFile + ": malformed uri.");
                }
            }
            if (cmdline.hasOption("a")) {
                zkAclId = cmdline.getOptionValue("a");
            }
            if (cmdline.hasOption("f")) {
                force = true;
            }
        }

        protected DistributedLogConfiguration getConf() {
            return dlConf;
        }

        protected URI getUri() {
            return uri;
        }

        protected void setUri(URI uri) {
            this.uri = uri;
        }

        protected String getZkAclId() {
            return zkAclId;
        }

        protected void setZkAclId(String zkAclId) {
            this.zkAclId = zkAclId;
        }

        protected boolean getForce() {
            return force;
        }

        protected void setForce(boolean force) {
            this.force = force;
        }

        protected synchronized com.twitter.distributedlog.DistributedLogManagerFactory getFactory() throws IOException {
            if (null == this.factory) {
                this.factory = new com.twitter.distributedlog.DistributedLogManagerFactory(getConf(), getUri());
                logger.info("Construct DLM : uri = {}", getUri());
            }
            return this.factory;
        }

        protected LogSegmentMetadataStore getLogSegmentMetadataStore() throws IOException {
            DistributedLogNamespace namespace = getFactory().getNamespace();
            assert(namespace instanceof BKDistributedLogNamespace);
            return ((BKDistributedLogNamespace) namespace).getWriterSegmentMetadataStore();
        }

        protected ZooKeeperClient getZooKeeperClient() throws IOException {
            DistributedLogNamespace namespace = getFactory().getNamespace();
            assert(namespace instanceof BKDistributedLogNamespace);
            return ((BKDistributedLogNamespace) namespace).getSharedWriterZKCForDL();
        }

        protected BookKeeperClient getBookKeeperClient() throws IOException {
            DistributedLogNamespace namespace = getFactory().getNamespace();
            assert(namespace instanceof BKDistributedLogNamespace);
            return ((BKDistributedLogNamespace) namespace).getReaderBKC();
        }
    }

    /**
     * Base class for simple command with no resource setup requirements.
     */
    public abstract static class SimpleCommand extends OptsCommand {

        protected final Options options = new Options();

        SimpleCommand(String name, String description) {
            super(name, description);
        }

        @Override
        protected int runCmd(CommandLine commandLine) throws Exception {
            try {
                parseCommandLine(commandLine);
            } catch (ParseException pe) {
                System.err.println("ERROR: failed to parse commandline : '" + pe.getMessage() + "'");
                printUsage();
                return -1;
            }
            return runSimpleCmd();
        }

        abstract protected int runSimpleCmd() throws Exception;

        abstract protected void parseCommandLine(CommandLine cmdline) throws ParseException;

        @Override
        protected Options getOptions() {
            return options;
        }
    }

    /**
     * Per Stream Command, which parse common options for per stream. e.g. stream name.
     */
    abstract static class PerStreamCommand extends PerDLCommand {

        protected String streamName;

        protected PerStreamCommand(String name, String description) {
            super(name, description);
            options.addOption("s", "stream", true, "Stream Name");
        }

        @Override
        protected void parseCommandLine(CommandLine cmdline) throws ParseException {
            super.parseCommandLine(cmdline);
            if (!cmdline.hasOption("s")) {
                throw new ParseException("No stream name provided.");
            }
            streamName = cmdline.getOptionValue("s");
        }

        protected String getStreamName() {
            return streamName;
        }

        protected void setStreamName(String streamName) {
            this.streamName = streamName;
        }
    }

    protected static class DeleteAllocatorPoolCommand extends PerDLCommand {

        int concurrency = 1;
        String allocationPoolPath = DistributedLogConstants.ALLOCATION_POOL_NODE;

        DeleteAllocatorPoolCommand() {
            super("delete_allocator_pool", "Delete allocator pool for a given distributedlog instance");
            options.addOption("t", "concurrency", true, "Concurrency on deleting allocator pool.");
            options.addOption("ap", "allocation-pool-path", true, "Ledger Allocation Pool Path");
        }

        @Override
        protected void parseCommandLine(CommandLine cmdline) throws ParseException {
            super.parseCommandLine(cmdline);
            if (cmdline.hasOption("t")) {
                concurrency = Integer.parseInt(cmdline.getOptionValue("t"));
                if (concurrency <= 0) {
                    throw new ParseException("Invalid concurrency value : " + concurrency + ": it must be greater or equal to 0.");
                }
            }
            if (cmdline.hasOption("ap")) {
                allocationPoolPath = cmdline.getOptionValue("ap");
                if (!allocationPoolPath.startsWith(".") || !allocationPoolPath.contains("allocation")) {
                    throw new ParseException("Invalid allocation pool path : " + allocationPoolPath + ": it must starts with a '.' and must contains 'allocation'");
                }
            }
        }

        @Override
        protected int runCmd() throws Exception {
            String rootPath = getUri().getPath() + "/" + allocationPoolPath;
            final ScheduledExecutorService allocationExecutor = Executors.newSingleThreadScheduledExecutor();
            ExecutorService executorService = Executors.newFixedThreadPool(concurrency);
            try {
                List<String> pools = getZooKeeperClient().get().getChildren(rootPath, false);
                final LinkedBlockingQueue<String> poolsToDelete = new LinkedBlockingQueue<String>();
                if (getForce() || IOUtils.confirmPrompt("Are you sure you want to delete allocator pools : " + pools)) {
                    for (String pool : pools) {
                        poolsToDelete.add(rootPath + "/" + pool);
                    }
                    final CountDownLatch doneLatch = new CountDownLatch(concurrency);
                    for (int i = 0; i < concurrency; i++) {
                        final int tid = i;
                        executorService.submit(new Runnable() {
                            @Override
                            public void run() {
                                while (!poolsToDelete.isEmpty()) {
                                    String poolPath = poolsToDelete.poll();
                                    if (null == poolPath) {
                                        break;
                                    }
                                    try {
                                        LedgerAllocator allocator =
                                                LedgerAllocatorUtils.createLedgerAllocatorPool(poolPath, 0, getConf(),
                                                        getZooKeeperClient(), getBookKeeperClient(),
                                                        allocationExecutor);
                                        allocator.delete();
                                        System.out.println("Deleted allocator pool : " + poolPath + " .");
                                    } catch (IOException ioe) {
                                        System.err.println("Failed to delete allocator pool " + poolPath + " : " + ioe.getMessage());
                                    }
                                }
                                doneLatch.countDown();
                                System.out.println("Thread " + tid + " is done.");
                            }
                        });
                    }
                    doneLatch.await();
                }
            } finally {
                executorService.shutdown();
                allocationExecutor.shutdown();
            }
            return 0;
        }

        @Override
        protected String getUsage() {
            return "delete_allocator_pool";
        }
    }

    public static class ListCommand extends PerDLCommand {

        boolean printMetadata = false;
        boolean printHex = false;

        ListCommand() {
            super("list", "list streams of a given distributedlog instance");
            options.addOption("m", "meta", false, "Print metadata associated with each stream");
            options.addOption("x", "hex", false, "Print metadata in hex format");
        }

        @Override
        protected void parseCommandLine(CommandLine cmdline) throws ParseException {
            super.parseCommandLine(cmdline);
            printMetadata = cmdline.hasOption("m");
            printHex = cmdline.hasOption("x");
        }

        @Override
        protected String getUsage() {
            return "list [options]";
        }

        @Override
        protected int runCmd() throws Exception {
            if (printMetadata) {
                printStreamsWithMetadata(getFactory());
            } else {
                printStreams(getFactory());
            }
            return 0;
        }

        protected void printStreamsWithMetadata(com.twitter.distributedlog.DistributedLogManagerFactory factory)
                throws Exception {
            Map<String, byte[]> streams = factory.enumerateLogsWithMetadataInNamespace();
            System.out.println("Streams under " + getUri() + " : ");
            System.out.println("--------------------------------");
            for (Map.Entry<String, byte[]> entry : streams.entrySet()) {
                println(entry.getKey());
                if (null == entry.getValue() || entry.getValue().length == 0) {
                    continue;
                }
                if (printHex) {
                    System.out.println(Hex.encodeHexString(entry.getValue()));
                } else {
                    System.out.println(new String(entry.getValue(), UTF_8));
                }
                System.out.println("");
            }
            System.out.println("--------------------------------");
        }

        protected void printStreams(com.twitter.distributedlog.DistributedLogManagerFactory factory) throws Exception {
            Collection<String> streams = factory.enumerateAllLogsInNamespace();
            System.out.println("Streams under " + getUri() + " : ");
            System.out.println("--------------------------------");
            for (String stream : streams) {
                System.out.println(stream);
            }
            System.out.println("--------------------------------");
        }
    }

    protected static class InspectCommand extends PerDLCommand {

        int numThreads = 1;
        String streamPrefix = null;
        boolean printInprogressOnly = false;
        boolean dumpEntries = false;
        boolean orderByTime = false;
        boolean printStreamsOnly = false;
        boolean checkInprogressOnly = false;

        InspectCommand() {
            super("inspect", "Inspect streams under a given dl uri to find any potential corruptions");
            options.addOption("t", "threads", true, "Number threads to do inspection.");
            options.addOption("ft", "filter", true, "Stream filter by prefix");
            options.addOption("i", "inprogress", false, "Print inprogress log segments only");
            options.addOption("d", "dump", false, "Dump entries of inprogress log segments");
            options.addOption("ot", "orderbytime", false, "Order the log segments by completion time");
            options.addOption("pso", "print-stream-only", false, "Print streams only");
            options.addOption("cio", "check-inprogress-only", false, "Check duplicated inprogress only");
        }

        @Override
        protected void parseCommandLine(CommandLine cmdline) throws ParseException {
            super.parseCommandLine(cmdline);
            if (cmdline.hasOption("t")) {
                numThreads = Integer.parseInt(cmdline.getOptionValue("t"));
            }
            if (cmdline.hasOption("ft")) {
                streamPrefix = cmdline.getOptionValue("ft");
            }
            printInprogressOnly = cmdline.hasOption("i");
            dumpEntries = cmdline.hasOption("d");
            orderByTime = cmdline.hasOption("ot");
            printStreamsOnly = cmdline.hasOption("pso");
            checkInprogressOnly = cmdline.hasOption("cio");
        }

        @Override
        protected int runCmd() throws Exception {
            SortedMap<String, List<Pair<LogSegmentMetadata, List<String>>>> corruptedCandidates =
                    new TreeMap<String, List<Pair<LogSegmentMetadata, List<String>>>>();
            inspectStreams(corruptedCandidates);
            System.out.println("Corrupted Candidates : ");
            if (printStreamsOnly) {
                System.out.println(corruptedCandidates.keySet());
                return 0;
            }
            for (Map.Entry<String, List<Pair<LogSegmentMetadata, List<String>>>> entry : corruptedCandidates.entrySet()) {
                System.out.println(entry.getKey() + " : \n");
                for (Pair<LogSegmentMetadata, List<String>> pair : entry.getValue()) {
                    System.out.println("\t - " + pair.getLeft());
                    if (printInprogressOnly && dumpEntries) {
                        int i = 0;
                        for (String entryData : pair.getRight()) {
                            System.out.println("\t" + i + "\t: " + entryData);
                            ++i;
                        }
                    }
                }
                System.out.println();
            }
            return 0;
        }

        private void inspectStreams(final SortedMap<String, List<Pair<LogSegmentMetadata, List<String>>>> corruptedCandidates)
                throws Exception {
            Collection<String> streamCollection = getFactory().enumerateAllLogsInNamespace();
            final List<String> streams = new ArrayList<String>();
            if (null != streamPrefix) {
                for (String s : streamCollection) {
                    if (s.startsWith(streamPrefix)) {
                        streams.add(s);
                    }
                }
            } else {
                streams.addAll(streamCollection);
            }
            if (0 == streams.size()) {
                return;
            }
            println("Streams : " + streams);
            if (!getForce() && !IOUtils.confirmPrompt("Are you sure you want to inspect " + streams.size() + " streams")) {
                return;
            }
            numThreads = Math.min(streams.size(), numThreads);
            final int numStreamsPerThreads = streams.size() / numThreads;
            Thread[] threads = new Thread[numThreads];
            for (int i = 0; i < numThreads; i++) {
                final int tid = i;
                threads[i] = new Thread("Inspect-" + i) {
                    @Override
                    public void run() {
                        try {
                            inspectStreams(streams, tid, numStreamsPerThreads, corruptedCandidates);
                            System.out.println("Thread " + tid + " finished.");
                        } catch (Exception e) {
                            System.err.println("Thread " + tid + " quits with exception : " + e.getMessage());
                        }
                    }
                };
                threads[i].start();
            }
            for (int i = 0; i < numThreads; i++) {
                threads[i].join();
            }
        }

        private void inspectStreams(List<String> streams,
                                    int tid,
                                    int numStreamsPerThreads,
                                    SortedMap<String, List<Pair<LogSegmentMetadata, List<String>>>> corruptedCandidates)
                throws Exception {
            int startIdx = tid * numStreamsPerThreads;
            int endIdx = Math.min(streams.size(), (tid + 1) * numStreamsPerThreads);
            for (int i = startIdx; i < endIdx; i++) {
                String s = streams.get(i);
                BookKeeperClient bkc = getBookKeeperClient();
                DistributedLogManager dlm =
                        getFactory().createDistributedLogManagerWithSharedClients(s);
                try {
                    List<LogSegmentMetadata> segments = dlm.getLogSegments();
                    if (segments.size() <= 1) {
                        continue;
                    }
                    boolean isCandidate = false;
                    if (checkInprogressOnly) {
                        Set<Long> inprogressSeqNos = new HashSet<Long>();
                        for (LogSegmentMetadata segment : segments) {
                            if (segment.isInProgress()) {
                                inprogressSeqNos.add(segment.getLogSegmentSequenceNumber());
                            }
                        }
                        for (LogSegmentMetadata segment : segments) {
                            if (!segment.isInProgress() && inprogressSeqNos.contains(segment.getLogSegmentSequenceNumber())) {
                                isCandidate = true;
                            }
                        }
                    } else {
                        LogSegmentMetadata firstSegment = segments.get(0);
                        long lastSeqNo = firstSegment.getLogSegmentSequenceNumber();

                        for (int j = 1; j < segments.size(); j++) {
                            LogSegmentMetadata nextSegment = segments.get(j);
                            if (lastSeqNo + 1 != nextSegment.getLogSegmentSequenceNumber()) {
                                isCandidate = true;
                                break;
                            }
                            ++lastSeqNo;
                        }
                    }
                    if (isCandidate) {
                        if (orderByTime) {
                            Collections.sort(segments, LOGSEGMENT_COMPARATOR_BY_TIME);
                        }
                        List<Pair<LogSegmentMetadata, List<String>>> ledgers =
                                new ArrayList<Pair<LogSegmentMetadata, List<String>>>();
                        for (LogSegmentMetadata seg : segments) {
                            LogSegmentMetadata segment = seg;
                            List<String> dumpedEntries = new ArrayList<String>();
                            if (segment.isInProgress()) {
                                LedgerHandle lh = bkc.get().openLedgerNoRecovery(segment.getLedgerId(), BookKeeper.DigestType.CRC32,
                                                                                 dlConf.getBKDigestPW().getBytes(UTF_8));
                                try {
                                    long lac = lh.readLastConfirmed();
                                    segment = segment.mutator().setLastEntryId(lac).build();
                                    if (printInprogressOnly && dumpEntries && lac >= 0) {
                                        Enumeration<LedgerEntry> entries = lh.readEntries(0L, lac);
                                        while (entries.hasMoreElements()) {
                                            LedgerEntry entry = entries.nextElement();
                                            dumpedEntries.add(new String(entry.getEntry(), UTF_8));
                                        }
                                    }
                                } finally {
                                    lh.close();
                                }
                            }
                            if (printInprogressOnly) {
                                if (segment.isInProgress()) {
                                    ledgers.add(Pair.of(segment, dumpedEntries));
                                }
                            } else {
                                ledgers.add(Pair.of(segment, EMPTY_LIST));
                            }
                        }
                        synchronized (corruptedCandidates) {
                            corruptedCandidates.put(s, ledgers);
                        }
                    }
                } finally {
                    dlm.close();
                }
            }
        }

        @Override
        protected String getUsage() {
            return "inspect [options]";
        }
    }

    protected static class TruncateCommand extends PerDLCommand {

        int numThreads = 1;
        String streamPrefix = null;
        boolean deleteStream = false;

        TruncateCommand() {
            super("truncate", "truncate streams under a given dl uri");
            options.addOption("t", "threads", true, "Number threads to do truncation");
            options.addOption("ft", "filter", true, "Stream filter by prefix");
            options.addOption("d", "delete", false, "Delete Stream");
        }

        @Override
        protected void parseCommandLine(CommandLine cmdline) throws ParseException {
            super.parseCommandLine(cmdline);
            if (cmdline.hasOption("t")) {
                numThreads = Integer.parseInt(cmdline.getOptionValue("t"));
            }
            if (cmdline.hasOption("ft")) {
                streamPrefix = cmdline.getOptionValue("ft");
            }
            if (cmdline.hasOption("d")) {
                deleteStream = true;
            }
        }

        @Override
        protected String getUsage() {
            return "truncate [options]";
        }

        protected void setFilter(String filter) {
            this.streamPrefix = filter;
        }

        @Override
        protected int runCmd() throws Exception {
            getConf().setZkAclId(getZkAclId());
            return truncateStreams(getFactory());
        }

        private int truncateStreams(final com.twitter.distributedlog.DistributedLogManagerFactory factory) throws Exception {
            Collection<String> streamCollection = factory.enumerateAllLogsInNamespace();
            final List<String> streams = new ArrayList<String>();
            if (null != streamPrefix) {
                for (String s : streamCollection) {
                    if (s.startsWith(streamPrefix)) {
                        streams.add(s);
                    }
                }
            } else {
                streams.addAll(streamCollection);
            }
            if (0 == streams.size()) {
                return 0;
            }
            System.out.println("Streams : " + streams);
            if (!getForce() && !IOUtils.confirmPrompt("Do you want to truncate " + streams.size() + " streams ?")) {
                return 0;
            }
            numThreads = Math.min(streams.size(), numThreads);
            final int numStreamsPerThreads = streams.size() / numThreads;
            Thread[] threads = new Thread[numThreads];
            for (int i = 0; i < numThreads; i++) {
                final int tid = i;
                threads[i] = new Thread("Truncate-" + i) {
                    @Override
                    public void run() {
                        try {
                            truncateStreams(factory, streams, tid, numStreamsPerThreads);
                            System.out.println("Thread " + tid + " finished.");
                        } catch (IOException e) {
                            System.err.println("Thread " + tid + " quits with exception : " + e.getMessage());
                        }
                    }
                };
                threads[i].start();
            }
            for (int i = 0; i < numThreads; i++) {
                threads[i].join();
            }
            return 0;
        }

        private void truncateStreams(com.twitter.distributedlog.DistributedLogManagerFactory factory, List<String> streams,
                                     int tid, int numStreamsPerThreads) throws IOException {
            int startIdx = tid * numStreamsPerThreads;
            int endIdx = Math.min(streams.size(), (tid + 1) * numStreamsPerThreads);
            for (int i = startIdx; i < endIdx; i++) {
                String s = streams.get(i);
                DistributedLogManager dlm =
                        factory.createDistributedLogManagerWithSharedClients(s);
                try {
                    if (deleteStream) {
                        dlm.delete();
                    } else {
                        dlm.purgeLogsOlderThan(Long.MAX_VALUE);
                    }
                } finally {
                    dlm.close();
                }
            }
        }
    }

    public static class SimpleBookKeeperClient {
        BookKeeperClient bkc;
        ZooKeeperClient zkc;

        public SimpleBookKeeperClient(DistributedLogConfiguration conf, URI uri) {
            try {
                zkc = ZooKeeperClientBuilder.newBuilder()
                    .sessionTimeoutMs(conf.getZKSessionTimeoutMilliseconds())
                    .zkAclId(conf.getZkAclId())
                    .uri(uri)
                    .build();
                BKDLConfig bkdlConfig = BKDLConfig.resolveDLConfig(zkc, uri);
                BKDLConfig.propagateConfiguration(bkdlConfig, conf);
                bkc = BookKeeperClientBuilder.newBuilder()
                        .zkc(zkc)
                        .dlConfig(conf)
                        .ledgersPath(bkdlConfig.getBkLedgersPath())
                        .name("dlog")
                        .build();
            } catch (Exception e) {
                close();
            }
        }
        public BookKeeperClient client() {
            return bkc;
        }
        public void close() {
            if (null != bkc) {
                bkc.close();
            }
            if (null != zkc) {
                zkc.close();
            }
        }
    }

    protected static class ShowCommand extends PerStreamCommand {

        SimpleBookKeeperClient bkc = null;
        boolean listSegments = true;
        boolean listEppStats = false;
        long firstLid = 0;
        long lastLid = -1;

        ShowCommand() {
            super("show", "show metadata of a given stream and list segments");
            options.addOption("ns", "no-log-segments", false, "Do not list log segment metadata");
            options.addOption("lp", "placement-stats", false, "Show ensemble placement stats");
            options.addOption("fl", "first-ledger", true, "First log sement no");
            options.addOption("ll", "last-ledger", true, "Last log sement no");
        }

        @Override
        protected void parseCommandLine(CommandLine cmdline) throws ParseException {
            super.parseCommandLine(cmdline);
            if (cmdline.hasOption("fl")) {
                try {
                    firstLid = Long.parseLong(cmdline.getOptionValue("fl"));
                } catch (NumberFormatException nfe) {
                    throw new ParseException("Invalid ledger id " + cmdline.getOptionValue("fl"));
                }
            }
            if (firstLid < 0) {
                throw new IllegalArgumentException("Invalid ledger id " + firstLid);
            }
            if (cmdline.hasOption("ll")) {
                try {
                    lastLid = Long.parseLong(cmdline.getOptionValue("ll"));
                } catch (NumberFormatException nfe) {
                    throw new ParseException("Invalid ledger id " + cmdline.getOptionValue("ll"));
                }
            }
            if (lastLid != -1 && firstLid > lastLid) {
                throw new IllegalArgumentException("Invalid ledger ids " + firstLid + " " + lastLid);
            }
            listSegments = !cmdline.hasOption("ns");
            listEppStats = cmdline.hasOption("lp");
        }

        @Override
        protected int runCmd() throws Exception {
            DistributedLogManager dlm = getFactory().createDistributedLogManagerWithSharedClients(getStreamName());
            try {
                if (listEppStats) {
                    bkc = new SimpleBookKeeperClient(getConf(), getUri());
                }
                printMetadata(dlm);
            } finally {
                dlm.close();
                if (null != bkc) {
                    bkc.close();
                }
            }
            return 0;
        }

        private void printMetadata(DistributedLogManager dlm) throws Exception {
            printHeader(dlm);
            if (listSegments) {
                System.out.println("Ledgers : ");
                List<LogSegmentMetadata> segments = dlm.getLogSegments();
                for (LogSegmentMetadata segment : segments) {
                    if (include(segment)) {
                        printLedgerRow(segment);
                    }
                }
            }
        }

        private void printHeader(DistributedLogManager dlm) throws Exception {
            DLSN firstDlsn = Await.result(dlm.getFirstDLSNAsync());
            boolean endOfStreamMarked = dlm.isEndOfStreamMarked();
            DLSN lastDlsn = dlm.getLastDLSN();
            long firstTxnId = dlm.getFirstTxId();
            long lastTxnId = dlm.getLastTxId();
            long recordCount = dlm.getLogRecordCount();
            String result = String.format("Stream : (firstTxId=%d, lastTxid=%d, firstDlsn=%s, lastDlsn=%s, endOfStreamMarked=%b, recordCount=%d)",
                firstTxnId, lastTxnId, getDlsnName(firstDlsn), getDlsnName(lastDlsn), endOfStreamMarked, recordCount);
            System.out.println(result);
            if (listEppStats) {
                printEppStatsHeader(dlm);
            }
        }

        boolean include(LogSegmentMetadata segment) {
            return (firstLid <= segment.getLogSegmentSequenceNumber() && (lastLid == -1 || lastLid >= segment.getLogSegmentSequenceNumber()));
        }

        private void printEppStatsHeader(DistributedLogManager dlm) throws Exception {
            String label = "Ledger Placement :";
            System.out.println(label);
            Map<BookieSocketAddress, Integer> totals = new HashMap<BookieSocketAddress, Integer>();
            List<LogSegmentMetadata> segments = dlm.getLogSegments();
            for (LogSegmentMetadata segment : segments) {
                if (include(segment)) {
                    merge(totals, getBookieStats(segment));
                }
            }
            List<Map.Entry<BookieSocketAddress, Integer>> entries = new ArrayList<Map.Entry<BookieSocketAddress, Integer>>(totals.entrySet());
            Collections.sort(entries, new Comparator<Map.Entry<BookieSocketAddress, Integer>>() {
                @Override
                public int compare(Map.Entry<BookieSocketAddress, Integer> o1, Map.Entry<BookieSocketAddress, Integer> o2) {
                    return o2.getValue() - o1.getValue();
                }
            });
            int width = 0;
            int totalEntries = 0;
            for (Map.Entry<BookieSocketAddress, Integer> entry : entries) {
                width = Math.max(width, label.length() + 1 + entry.getKey().toString().length());
                totalEntries += entry.getValue();
            }
            for (Map.Entry<BookieSocketAddress, Integer> entry : entries) {
                System.out.println(String.format("%"+width+"s\t%6.2f%%\t\t%d", entry.getKey(), entry.getValue()*1.0/totalEntries, entry.getValue()));
            }
        }

        private void printLedgerRow(LogSegmentMetadata segment) throws Exception {
            System.out.println(segment.getLogSegmentSequenceNumber() + "\t: " + segment);
        }

        private Map<BookieSocketAddress, Integer> getBookieStats(LogSegmentMetadata segment) throws Exception {
            Map<BookieSocketAddress, Integer> stats = new HashMap<BookieSocketAddress, Integer>();
            LedgerHandle lh = bkc.client().get().openLedgerNoRecovery(segment.getLedgerId(), BookKeeper.DigestType.CRC32,
                    getConf().getBKDigestPW().getBytes(UTF_8));
            long eidFirst = 0;
            for (SortedMap.Entry<Long, ArrayList<BookieSocketAddress>> entry : LedgerReader.bookiesForLedger(lh).entrySet()) {
                long eidLast = entry.getKey().longValue();
                long count = eidLast - eidFirst + 1;
                for (BookieSocketAddress bookie : entry.getValue()) {
                    merge(stats, bookie, (int) count);
                }
                eidFirst = eidLast;
            }
            return stats;
        }

        void merge(Map<BookieSocketAddress, Integer> m, BookieSocketAddress bookie, Integer count) {
            if (m.containsKey(bookie)) {
                m.put(bookie, count + m.get(bookie).intValue());
            } else {
                m.put(bookie, count);
            }
        }

        void merge(Map<BookieSocketAddress, Integer> m1, Map<BookieSocketAddress, Integer> m2) {
            for (Map.Entry<BookieSocketAddress, Integer> entry : m2.entrySet()) {
                merge(m1, entry.getKey(), entry.getValue());
            }
        }

        String getDlsnName(DLSN dlsn) {
            if (dlsn.equals(DLSN.InvalidDLSN)) {
                return "InvalidDLSN";
            }
            return dlsn.toString();
        }

        @Override
        protected String getUsage() {
            return "show [options]";
        }
    }

    static class CountCommand extends PerStreamCommand {

        DLSN startDLSN = null;
        DLSN endDLSN = null;

        protected CountCommand() {
            super("count", "count number records between dlsns");
        }

        @Override
        protected void parseCommandLine(CommandLine cmdline) throws ParseException {
            super.parseCommandLine(cmdline);
            String[] args = cmdline.getArgs();
            if (args.length < 1) {
                throw new ParseException("Must specify at least start dlsn.");
            }
            if (args.length >= 1) {
                startDLSN = parseDLSN(args[0]);
            }
            if (args.length >= 2) {
                endDLSN = parseDLSN(args[1]);
            }
        }

        @Override
        protected int runCmd() throws Exception {
            DistributedLogManager dlm = getFactory().createDistributedLogManagerWithSharedClients(getStreamName());
            try {
                long count = 0;
                if (null == endDLSN) {
                    count = countToLastRecord(dlm);
                } else {
                    count = countFromStartToEnd(dlm);
                }
                System.out.println("total is " + count + " records.");
                return 0;
            } finally {
                dlm.close();
            }
        }

        int countFromStartToEnd(DistributedLogManager dlm) throws Exception {
            int count = 0;
            try {
                LogReader reader = dlm.getInputStream(startDLSN);
                try {
                    LogRecordWithDLSN record = reader.readNext(false);
                    LogRecordWithDLSN preRecord = record;
                    System.out.println("first record : " + record);
                    while (null != record) {
                        if (record.getDlsn().compareTo(endDLSN) > 0) {
                            break;
                        }
                        ++count;
                        if (count % 1000 == 0) {
                            logger.info("read {} records from {}...", count, getStreamName());
                        }
                        preRecord = record;
                        record = reader.readNext(false);
                    }
                    System.out.println("last record : " + preRecord);
                } finally {
                    reader.close();
                }
            } finally {
                dlm.close();
            }
            return count;
        }

        long countToLastRecord(DistributedLogManager dlm) throws Exception {
            return Await.result(dlm.getLogRecordCountAsync(startDLSN)).longValue();
        }

        @Override
        protected String getUsage() {
            return "count <start> <end>";
        }
    }

    public static class DeleteCommand extends PerStreamCommand {

        protected DeleteCommand() {
            super("delete", "delete a given stream");
        }

        @Override
        protected int runCmd() throws Exception {
            getConf().setZkAclId(getZkAclId());
            DistributedLogManager dlm = getFactory().createDistributedLogManagerWithSharedClients(getStreamName());
            try {
                dlm.delete();
            } finally {
                dlm.close();
            }
            return 0;
        }

        @Override
        protected String getUsage() {
            return "delete";
        }
    }

    public static class DeleteLedgersCommand extends PerDLCommand {

        private final List<Long> ledgers = new ArrayList<Long>();

        int numThreads = 1;

        protected DeleteLedgersCommand() {
            super("delete_ledgers", "delete given ledgers");
            options.addOption("l", "ledgers", true, "List of ledgers, separated by comma");
            options.addOption("lf", "ledgers-file", true, "File of list of ledgers, each line has a ledger id");
            options.addOption("t", "concurrency", true, "Number of threads to run deletions");
        }

        @Override
        protected void parseCommandLine(CommandLine cmdline) throws ParseException {
            super.parseCommandLine(cmdline);
            if (cmdline.hasOption("l") && cmdline.hasOption("lf")) {
                throw new ParseException("Please specify ledgers: either use list or use file only.");
            }
            if (!cmdline.hasOption("l") && !cmdline.hasOption("lf")) {
                throw new ParseException("No ledgers specified. Please specify ledgers either use list or use file only.");
            }
            if (cmdline.hasOption("l")) {
                String ledgersStr = cmdline.getOptionValue("l");
                String[] ledgerStrs = ledgersStr.split(",");
                for (String ledgerStr : ledgerStrs) {
                    ledgers.add(Long.parseLong(ledgerStr));
                }
            }
            if (cmdline.hasOption("lf")) {
                BufferedReader br = null;
                try {

                    br = new BufferedReader(new InputStreamReader(
                            new FileInputStream(new File(cmdline.getOptionValue("lf"))), UTF_8.name()));
                    String line;
                    while ((line = br.readLine()) != null) {
                        ledgers.add(Long.parseLong(line));
                    }
                } catch (FileNotFoundException e) {
                    throw new ParseException("No ledgers file " + cmdline.getOptionValue("lf") + " found.");
                } catch (IOException e) {
                    throw new ParseException("Invalid ledgers file " + cmdline.getOptionValue("lf") + " found.");
                } finally {
                    if (null != br) {
                        try {
                            br.close();
                        } catch (IOException e) {
                            // no-op
                        }
                    }
                }
            }
            if (cmdline.hasOption("t")) {
                numThreads = Integer.parseInt(cmdline.getOptionValue("t"));
            }
        }

        @Override
        protected String getUsage() {
            return "delete_ledgers [options]";
        }

        @Override
        protected int runCmd() throws Exception {
            ExecutorService executorService = Executors.newFixedThreadPool(numThreads);
            try {
                final AtomicInteger numLedgers = new AtomicInteger(0);
                final CountDownLatch doneLatch = new CountDownLatch(numThreads);
                final AtomicInteger numFailures = new AtomicInteger(0);
                final LinkedBlockingQueue<Long> ledgerQueue =
                        new LinkedBlockingQueue<Long>();
                ledgerQueue.addAll(ledgers);
                for (int i = 0; i < numThreads; i++) {
                    final int tid = i;
                    executorService.submit(new Runnable() {
                        @Override
                        public void run() {
                            while (true) {
                                Long ledger = ledgerQueue.poll();
                                if (null == ledger) {
                                    break;
                                }
                                try {
                                    getBookKeeperClient().get().deleteLedger(ledger);
                                    int numLedgersDeleted = numLedgers.incrementAndGet();
                                    if (numLedgersDeleted % 1000 == 0) {
                                        System.out.println("Deleted " + numLedgersDeleted + " ledgers.");
                                    }
                                } catch (BKException.BKNoSuchLedgerExistsException e) {
                                    int numLedgersDeleted = numLedgers.incrementAndGet();
                                    if (numLedgersDeleted % 1000 == 0) {
                                        System.out.println("Deleted " + numLedgersDeleted + " ledgers.");
                                    }
                                } catch (Exception e) {
                                    numFailures.incrementAndGet();
                                    break;
                                }
                            }
                            doneLatch.countDown();
                            System.out.println("Thread " + tid + " quits");
                        }
                    });
                }
                doneLatch.await();
                if (numFailures.get() > 0) {
                    throw new IOException("Encounter " + numFailures.get() + " failures during deleting ledgers");
                }
            } finally {
                executorService.shutdown();
            }
            return 0;
        }
    }

    public static class CreateCommand extends PerDLCommand {

        final List<String> streams = new ArrayList<String>();

        String streamPrefix = null;
        String streamExpression = null;

        CreateCommand() {
            super("create", "create streams under a given namespace");
            options.addOption("r", "prefix", true, "Prefix of stream name. E.g. 'QuantumLeapTest-'.");
            options.addOption("e", "expression", true, "Expression to generate stream suffix. " +
                              "Currently we support range 'x-y', list 'x,y,z' and name 'xyz'");
        }

        @Override
        protected void parseCommandLine(CommandLine cmdline) throws ParseException {
            super.parseCommandLine(cmdline);
            if (cmdline.hasOption("r")) {
                streamPrefix = cmdline.getOptionValue("r");
            }
            if (cmdline.hasOption("e")) {
                streamExpression = cmdline.getOptionValue("e");
            }
            if (null == streamPrefix || null == streamExpression) {
                throw new ParseException("Please specify stream prefix & expression.");
            }
        }

        protected void generateStreams(String streamPrefix, String streamExpression) throws ParseException {
            // parse the stream expression
            if (streamExpression.contains("-")) {
                // a range expression
                String[] parts = streamExpression.split("-");
                if (parts.length != 2) {
                    throw new ParseException("Invalid stream index range : " + streamExpression);
                }
                try {
                    int start = Integer.parseInt(parts[0]);
                    int end = Integer.parseInt(parts[1]);
                    if (start > end) {
                        throw new ParseException("Invalid stream index range : " + streamExpression);
                    }
                    for (int i = start; i <= end; i++) {
                        streams.add(streamPrefix + i);
                    }
                } catch (NumberFormatException nfe) {
                    throw new ParseException("Invalid stream index range : " + streamExpression);
                }
            } else if (streamExpression.contains(",")) {
                // a list expression
                String[] parts = streamExpression.split(",");
                try {
                    for (String part : parts) {
                        int idx = Integer.parseInt(part);
                        streams.add(streamPrefix + idx);
                    }
                } catch (NumberFormatException nfe) {
                    throw new ParseException("Invalid stream suffix list : " + streamExpression);
                }
            } else {
                streams.add(streamPrefix + streamExpression);
            }
        }

        @Override
        protected int runCmd() throws Exception {
            generateStreams(streamPrefix, streamExpression);
            if (streams.isEmpty()) {
                System.out.println("Nothing to create.");
                return 0;
            }
            if (!getForce() && !IOUtils.confirmPrompt("You are going to create streams : " + streams)) {
                return 0;
            }
            getConf().setZkAclId(getZkAclId());
            for (String stream : streams) {
                getFactory().getNamespace().createLog(stream);
            }
            return 0;
        }

        @Override
        protected String getUsage() {
            return "create [options]";
        }

        protected void setPrefix(String prefix) {
            this.streamPrefix = prefix;
        }

        protected void setExpression(String expression) {
            this.streamExpression = expression;
        }
    }

    protected static class DumpCommand extends PerStreamCommand {

        boolean printHex = false;
        boolean skipPayload = false;
        Long fromTxnId = null;
        DLSN fromDLSN = null;
        int count = 100;

        DumpCommand() {
            super("dump", "dump records of a given stream");
            options.addOption("x", "hex", false, "Print record in hex format");
            options.addOption("sp", "skip-payload", false, "Skip printing the payload of the record");
            options.addOption("o", "offset", true, "Txn ID to start dumping.");
            options.addOption("n", "seqno", true, "Sequence Number to start dumping");
            options.addOption("e", "eid", true, "Entry ID to start dumping");
            options.addOption("t", "slot", true, "Slot to start dumping");
            options.addOption("l", "limit", true, "Number of entries to dump. Default is 100.");
        }

        @Override
        protected void parseCommandLine(CommandLine cmdline) throws ParseException {
            super.parseCommandLine(cmdline);
            printHex = cmdline.hasOption("x");
            skipPayload = cmdline.hasOption("sp");
            if (cmdline.hasOption("o")) {
                try {
                    fromTxnId = Long.parseLong(cmdline.getOptionValue("o"));
                } catch (NumberFormatException nfe) {
                    throw new ParseException("Invalid txn id " + cmdline.getOptionValue("o"));
                }
            }
            if (cmdline.hasOption("l")) {
                try {
                    count = Integer.parseInt(cmdline.getOptionValue("l"));
                } catch (NumberFormatException nfe) {
                    throw new ParseException("Invalid count " + cmdline.getOptionValue("l"));
                }
                if (count <= 0) {
                    throw new ParseException("Negative count found : " + count);
                }
            }
            if (cmdline.hasOption("n")) {
                long seqno;
                try {
                    seqno = Long.parseLong(cmdline.getOptionValue("n"));
                } catch (NumberFormatException nfe) {
                    throw new ParseException("Invalid sequence number " + cmdline.getOptionValue("n"));
                }
                long eid;
                if (cmdline.hasOption("e")) {
                    eid = Long.parseLong(cmdline.getOptionValue("e"));
                } else {
                    eid = 0;
                }
                long slot;
                if (cmdline.hasOption("t")) {
                    slot = Long.parseLong(cmdline.getOptionValue("t"));
                } else {
                    slot = 0;
                }
                fromDLSN = new DLSN(seqno, eid, slot);
            }
            if (null == fromTxnId && null == fromDLSN) {
                throw new ParseException("No start Txn/DLSN is specified.");
            }
        }

        @Override
        protected int runCmd() throws Exception {
            DistributedLogManager dlm = getFactory().createDistributedLogManagerWithSharedClients(getStreamName());
            long totalCount = dlm.getLogRecordCount();
            try {
                AsyncLogReader reader;
                Object startOffset;
                try {
                    DLSN lastDLSN = Await.result(dlm.getLastDLSNAsync());
                    System.out.println("Last DLSN : " + lastDLSN);
                    if (null == fromDLSN) {
                        reader = dlm.getAsyncLogReader(fromTxnId);
                        startOffset = fromTxnId;
                    } else {
                        reader = dlm.getAsyncLogReader(fromDLSN);
                        startOffset = fromDLSN;
                    }
                } catch (LogNotFoundException lee) {
                    System.out.println("No stream found to dump records.");
                    return 0;
                }
                try {
                    System.out.println(String.format("Dump records for %s (from = %s, dump count = %d, total records = %d)",
                            getStreamName(), startOffset, count, totalCount));

                    dumpRecords(reader);
                } finally {
                    Utils.close(reader);
                }
            } finally {
                dlm.close();
            }
            return 0;
        }

        private void dumpRecords(AsyncLogReader reader) throws Exception {
            int numRead = 0;
            LogRecord record = Await.result(reader.readNext());
            while (record != null) {
                // dump the record
                dumpRecord(record);
                ++numRead;
                if (numRead >= count) {
                    break;
                }
                record = Await.result(reader.readNext());
            }
            if (numRead == 0) {
                System.out.println("No records.");
            } else {
                System.out.println("------------------------------------------------");
            }
        }

        private void dumpRecord(LogRecord record) {
            System.out.println("------------------------------------------------");
            if (record instanceof LogRecordWithDLSN) {
                System.out.println("Record (txn = " + record.getTransactionId() + ", bytes = "
                        + record.getPayload().length + ", dlsn = "
                        + ((LogRecordWithDLSN) record).getDlsn() + ", sequence id = "
                        + ((LogRecordWithDLSN) record).getSequenceId() + ")");
            } else {
                System.out.println("Record (txn = " + record.getTransactionId() + ", bytes = "
                        + record.getPayload().length + ")");
            }
            System.out.println("");

            if (skipPayload) {
                return;
            }

            if (printHex) {
                System.out.println(Hex.encodeHexString(record.getPayload()));
            } else {
                System.out.println(new String(record.getPayload(), UTF_8));
            }
        }

        @Override
        protected String getUsage() {
            return "dump [options]";
        }

        protected void setFromTxnId(Long fromTxnId) {
            this.fromTxnId = fromTxnId;
        }
    }

    /**
     * TODO: refactor inspect & inspectstream
     * TODO: support force
     *
     * inspectstream -lac -gap (different options for different operations for a single stream)
     * inspect -lac -gap (inspect the namespace, which will use inspect stream)
     */
    static class InspectStreamCommand extends PerStreamCommand {

        InspectStreamCommand() {
            super("inspectstream", "Inspect a given stream to identify any metadata corruptions");
        }

        @Override
        protected int runCmd() throws Exception {
            DistributedLogManager dlm = getFactory().createDistributedLogManagerWithSharedClients(getStreamName());
            try {
                return inspectAndRepair(dlm.getLogSegments());
            } finally {
                dlm.close();
            }
        }

        protected int inspectAndRepair(List<LogSegmentMetadata> segments) throws Exception {
            LogSegmentMetadataStore metadataStore = getLogSegmentMetadataStore();
            ZooKeeperClient zkc = getZooKeeperClient();
            BKDLConfig bkdlConfig = BKDLConfig.resolveDLConfig(zkc, getUri());
            BKDLConfig.propagateConfiguration(bkdlConfig, getConf());
            BookKeeperClient bkc = BookKeeperClientBuilder.newBuilder()
                    .dlConfig(getConf())
                    .zkServers(bkdlConfig.getBkZkServersForReader())
                    .ledgersPath(bkdlConfig.getBkLedgersPath())
                    .name("dlog")
                    .build();
            try {
                List<LogSegmentMetadata> segmentsToRepair = inspectLogSegments(bkc, segments);
                if (segmentsToRepair.isEmpty()) {
                    System.out.println("The stream is good. No log segments to repair.");
                    return 0;
                }
                System.out.println(segmentsToRepair.size() + " segments to repair : ");
                System.out.println(segmentsToRepair);
                System.out.println();
                if (!IOUtils.confirmPrompt("Do you want to repair them (Y/N): ")) {
                    return 0;
                }
                repairLogSegments(metadataStore, bkc, segmentsToRepair);
                return 0;
            } finally {
                bkc.close();
            }
        }

        protected List<LogSegmentMetadata> inspectLogSegments(
                BookKeeperClient bkc, List<LogSegmentMetadata> segments) throws Exception {
            List<LogSegmentMetadata> segmentsToRepair = new ArrayList<LogSegmentMetadata>();
            for (LogSegmentMetadata segment : segments) {
                if (!segment.isInProgress() && !inspectLogSegment(bkc, segment)) {
                    segmentsToRepair.add(segment);
                }
            }
            return segmentsToRepair;
        }

        /**
         * Inspect a given log segment.
         *
         * @param bkc
         *          bookkeeper client
         * @param metadata
         *          metadata of the log segment to
         * @return true if it is a good stream, false if the stream has inconsistent metadata.
         * @throws Exception
         */
        protected boolean inspectLogSegment(BookKeeperClient bkc,
                                            LogSegmentMetadata metadata) throws Exception {
            if (metadata.isInProgress()) {
                System.out.println("Skip inprogress log segment " + metadata);
                return true;
            }
            long ledgerId = metadata.getLedgerId();
            LedgerHandle lh = bkc.get().openLedger(ledgerId, BookKeeper.DigestType.CRC32,
                    getConf().getBKDigestPW().getBytes(UTF_8));
            LedgerHandle readLh = bkc.get().openLedger(ledgerId, BookKeeper.DigestType.CRC32,
                    getConf().getBKDigestPW().getBytes(UTF_8));
            LedgerReader lr = new LedgerReader(bkc.get());
            final AtomicReference<List<LedgerEntry>> entriesHolder = new AtomicReference<List<LedgerEntry>>(null);
            final AtomicInteger rcHolder = new AtomicInteger(-1234);
            final CountDownLatch doneLatch = new CountDownLatch(1);
            try {
                lr.forwardReadEntriesFromLastConfirmed(readLh, new BookkeeperInternalCallbacks.GenericCallback<List<LedgerEntry>>() {
                    @Override
                    public void operationComplete(int rc, List<LedgerEntry> entries) {
                        rcHolder.set(rc);
                        entriesHolder.set(entries);
                        doneLatch.countDown();
                    }
                });
                doneLatch.await();
                if (BKException.Code.OK != rcHolder.get()) {
                    throw BKException.create(rcHolder.get());
                }
                List<LedgerEntry> entries = entriesHolder.get();
                long lastEntryId;
                if (entries.isEmpty()) {
                    lastEntryId = LedgerHandle.INVALID_ENTRY_ID;
                } else {
                    LedgerEntry lastEntry = entries.get(entries.size() - 1);
                    lastEntryId = lastEntry.getEntryId();
                }
                if (lastEntryId != lh.getLastAddConfirmed()) {
                    System.out.println("Inconsistent Last Add Confirmed Found for LogSegment " + metadata.getLogSegmentSequenceNumber() + ": ");
                    System.out.println("\t metadata: " + metadata);
                    System.out.println("\t lac in ledger metadata is " + lh.getLastAddConfirmed() + ", but lac in bookies is " + lastEntryId);
                    return false;
                } else {
                    return true;
                }
            } finally {
                lh.close();
                readLh.close();
            }
        }

        protected void repairLogSegments(LogSegmentMetadataStore metadataStore,
                                         BookKeeperClient bkc,
                                         List<LogSegmentMetadata> segments) throws Exception {
            BookKeeperAdmin bkAdmin = new BookKeeperAdmin(bkc.get());
            try {
                MetadataUpdater metadataUpdater = LogSegmentMetadataStoreUpdater.createMetadataUpdater(
                        getConf(), metadataStore);
                for (LogSegmentMetadata segment : segments) {
                    repairLogSegment(bkAdmin, metadataUpdater, segment);
                }
            } finally {
                bkAdmin.close();
            }
        }

        protected void repairLogSegment(BookKeeperAdmin bkAdmin,
                                        MetadataUpdater metadataUpdater,
                                        LogSegmentMetadata segment) throws Exception {
            if (segment.isInProgress()) {
                System.out.println("Skip inprogress log segment " + segment);
                return;
            }
            LedgerHandle lh = bkAdmin.openLedger(segment.getLedgerId(), true);
            long lac = lh.getLastAddConfirmed();
            Enumeration<LedgerEntry> entries = lh.readEntries(lac, lac);
            if (!entries.hasMoreElements()) {
                throw new IOException("Entry " + lac + " isn't found for " + segment);
            }
            LedgerEntry lastEntry = entries.nextElement();
            Entry.Reader reader = Entry.newBuilder()
                    .setLogSegmentInfo(segment.getLogSegmentSequenceNumber(), segment.getStartSequenceId())
                    .setEntryId(lastEntry.getEntryId())
                    .setEnvelopeEntry(LogSegmentMetadata.supportsEnvelopedEntries(segment.getVersion()))
                    .setInputStream(lastEntry.getEntryInputStream())
                    .buildReader();
            LogRecordWithDLSN record = reader.nextRecord();
            LogRecordWithDLSN lastRecord = null;
            while (null != record) {
                lastRecord = record;
                record = reader.nextRecord();
            }
            if (null == lastRecord) {
                throw new IOException("No record found in entry " + lac + " for " + segment);
            }
            System.out.println("Updating last record for " + segment + " to " + lastRecord);
            if (!IOUtils.confirmPrompt("Do you want to make this change (Y/N): ")) {
                return;
            }
            metadataUpdater.updateLastRecord(segment, lastRecord);
        }

        @Override
        protected String getUsage() {
            return "inspectstream [options]";
        }
    }

    static interface BKCommandRunner {
        int run(ZooKeeperClient zkc, BookKeeperClient bkc) throws Exception;
    }

    abstract static class PerBKCommand extends PerDLCommand {

        protected PerBKCommand(String name, String description) {
            super(name, description);
        }

        @Override
        protected int runCmd() throws Exception {
            return runBKCommand(new BKCommandRunner() {
                @Override
                public int run(ZooKeeperClient zkc, BookKeeperClient bkc) throws Exception {
                    return runBKCmd(zkc, bkc);
                }
            });
        }

        protected int runBKCommand(BKCommandRunner runner) throws Exception {
            return runner.run(getZooKeeperClient(), getBookKeeperClient());
        }

        abstract protected int runBKCmd(ZooKeeperClient zkc, BookKeeperClient bkc) throws Exception;
    }

    static class RecoverCommand extends PerBKCommand {

        final List<Long> ledgers = new ArrayList<Long>();
        boolean query = false;
        boolean dryrun = false;
        boolean skipOpenLedgers = false;
        boolean fenceOnly = false;
        int fenceRate = 1;
        int concurrency = 1;
        final Set<BookieSocketAddress> bookiesSrc = new HashSet<BookieSocketAddress>();
        int partition = 0;
        int numPartitions = 0;

        RecoverCommand() {
            super("recover", "Recover the ledger data that stored on failed bookies");
            options.addOption("l", "ledger", true, "Specific ledger to recover");
            options.addOption("lf", "ledgerfile", true, "File contains ledgers list");
            options.addOption("q", "query", false, "Query the ledgers that contain given bookies");
            options.addOption("d", "dryrun", false, "Print the recovery plan w/o actually recovering");
            options.addOption("cy", "concurrency", true, "Number of ledgers could be recovered in parallel");
            options.addOption("sk", "skipOpenLedgers", false, "Skip recovering open ledgers");
            options.addOption("p", "partition", true, "partition");
            options.addOption("n", "num-partitions", true, "num partitions");
            options.addOption("fo", "fence-only", true, "fence the ledgers only w/o re-replicating entries");
            options.addOption("fr", "fence-rate", true, "rate on fencing ledgers");
        }

        @Override
        protected void parseCommandLine(CommandLine cmdline) throws ParseException {
            super.parseCommandLine(cmdline);
            query = cmdline.hasOption("q");
            force = cmdline.hasOption("f");
            dryrun = cmdline.hasOption("d");
            skipOpenLedgers = cmdline.hasOption("sk");
            fenceOnly = cmdline.hasOption("fo");
            if (cmdline.hasOption("l")) {
                String[] lidStrs = cmdline.getOptionValue("l").split(",");
                try {
                    for (String lidStr : lidStrs) {
                        ledgers.add(Long.parseLong(lidStr));
                    }
                } catch (NumberFormatException nfe) {
                    throw new ParseException("Invalid ledger id provided : " + cmdline.getOptionValue("l"));
                }
            }
            if (cmdline.hasOption("lf")) {
                String file = cmdline.getOptionValue("lf");
                try {
                    BufferedReader br = new BufferedReader(
                            new InputStreamReader(new FileInputStream(file), UTF_8.name()));
                    try {
                        String line = br.readLine();

                        while (line != null) {
                            ledgers.add(Long.parseLong(line));
                            line = br.readLine();
                        }
                    } finally {
                        br.close();
                    }
                } catch (IOException e) {
                    throw new ParseException("Invalid ledgers file provided : " + file);
                }
            }
            if (cmdline.hasOption("cy")) {
                try {
                    concurrency = Integer.parseInt(cmdline.getOptionValue("cy"));
                } catch (NumberFormatException nfe) {
                    throw new ParseException("Invalid concurrency provided : " + cmdline.getOptionValue("cy"));
                }
            }
            if (cmdline.hasOption("p")) {
                partition = Integer.parseInt(cmdline.getOptionValue("p"));
            }
            if (cmdline.hasOption("n")) {
                numPartitions = Integer.parseInt(cmdline.getOptionValue("n"));
            }
            if (cmdline.hasOption("fr")) {
                fenceRate = Integer.parseInt(cmdline.getOptionValue("fr"));
            }
            // Get bookies list to recover
            String[] args = cmdline.getArgs();
            final String[] bookieStrs = args[0].split(",");
            for (String bookieStr : bookieStrs) {
                final String bookieStrParts[] = bookieStr.split(":");
                if (bookieStrParts.length != 2) {
                    throw new ParseException("BookieSrcs has invalid bookie address format (host:port expected) : "
                            + bookieStr);
                }
                try {
                    bookiesSrc.add(new BookieSocketAddress(bookieStrParts[0],
                            Integer.parseInt(bookieStrParts[1])));
                } catch (NumberFormatException nfe) {
                    throw new ParseException("Invalid ledger id provided : " + cmdline.getOptionValue("l"));
                }
            }
        }

        @Override
        protected int runBKCmd(ZooKeeperClient zkc, BookKeeperClient bkc) throws Exception {
            BookKeeperAdmin bkAdmin = new BookKeeperAdmin(bkc.get());
            try {
                if (query) {
                    return bkQuery(bkAdmin, bookiesSrc);
                }
                if (fenceOnly) {
                    return bkFence(bkc, ledgers, fenceRate);
                }
                if (!force) {
                    System.out.println("Bookies : " + bookiesSrc);
                    if (!IOUtils.confirmPrompt("Do you want to recover them: (Y/N)")) {
                        return -1;
                    }
                }
                if (!ledgers.isEmpty()) {
                    System.out.println("Ledgers : " + ledgers);
                    long numProcessed = 0;
                    Iterator<Long> ledgersIter = ledgers.iterator();
                    LinkedBlockingQueue<Long> ledgersToProcess = new LinkedBlockingQueue<Long>();
                    while (ledgersIter.hasNext()) {
                        long lid = ledgersIter.next();
                        if (numPartitions <=0 || (numPartitions > 0 && lid % numPartitions == partition)) {
                            ledgersToProcess.add(lid);
                            ++numProcessed;
                        }
                        if (ledgersToProcess.size() == 10000) {
                            System.out.println("Processing " + numProcessed + " ledgers");
                            bkRecovery(ledgersToProcess, bookiesSrc, dryrun, skipOpenLedgers);
                            ledgersToProcess.clear();
                            System.out.println("Processed " + numProcessed + " ledgers");
                        }
                    }
                    if (!ledgersToProcess.isEmpty()) {
                        System.out.println("Processing " + numProcessed + " ledgers");
                        bkRecovery(ledgersToProcess, bookiesSrc, dryrun, skipOpenLedgers);
                        System.out.println("Processed " + numProcessed + " ledgers");
                    }
                    System.out.println("Done.");
                    CountDownLatch latch = new CountDownLatch(1);
                    latch.await();
                    return 0;
                }
                return bkRecovery(bkAdmin, bookiesSrc, dryrun, skipOpenLedgers);
            } finally {
                bkAdmin.close();
            }
        }

        private int bkFence(final BookKeeperClient bkc, List<Long> ledgers, int fenceRate) throws Exception {
            if (ledgers.isEmpty()) {
                System.out.println("Nothing to fence. Done.");
                return 0;
            }
            ExecutorService executorService = Executors.newCachedThreadPool();
            final RateLimiter rateLimiter = RateLimiter.create(fenceRate);
            final byte[] passwd = getConf().getBKDigestPW().getBytes(UTF_8);
            final CountDownLatch latch = new CountDownLatch(ledgers.size());
            final AtomicInteger numPendings = new AtomicInteger(ledgers.size());
            final LinkedBlockingQueue<Long> ledgersQueue = new LinkedBlockingQueue<Long>();
            ledgersQueue.addAll(ledgers);

            for (int i = 0; i < concurrency; i++) {
                executorService.submit(new Runnable() {
                    @Override
                    public void run() {
                        while (!ledgersQueue.isEmpty()) {
                            rateLimiter.acquire();
                            Long lid = ledgersQueue.poll();
                            if (null == lid) {
                                break;
                            }
                            System.out.println("Fencing ledger " + lid);
                            int numRetries = 3;
                            while (numRetries > 0) {
                                try {
                                    LedgerHandle lh = bkc.get().openLedger(lid, BookKeeper.DigestType.CRC32, passwd);
                                    lh.close();
                                    System.out.println("Fenced ledger " + lid + ", " + numPendings.decrementAndGet() + " left.");
                                    latch.countDown();
                                } catch (BKException.BKNoSuchLedgerExistsException bke) {
                                    System.out.println("Skipped fence non-exist ledger " + lid + ", " + numPendings.decrementAndGet() + " left.");
                                    latch.countDown();
                                } catch (BKException.BKLedgerRecoveryException lre) {
                                    --numRetries;
                                    continue;
                                } catch (Exception e) {
                                    e.printStackTrace();
                                    break;
                                }
                                numRetries = 0;
                            }
                        }
                        System.out.println("Thread exits");
                    }
                });
            }
            latch.await();
            SchedulerUtils.shutdownScheduler(executorService, 2, TimeUnit.MINUTES);
            return 0;
        }

        private int bkQuery(BookKeeperAdmin bkAdmin, Set<BookieSocketAddress> bookieAddrs)
                throws InterruptedException, BKException {
            SortedMap<Long, LedgerMetadata> ledgersContainBookies =
                    bkAdmin.getLedgersContainBookies(bookieAddrs);
            System.err.println("NOTE: Bookies in inspection list are marked with '*'.");
            for (Map.Entry<Long, LedgerMetadata> ledger : ledgersContainBookies.entrySet()) {
                System.out.println("ledger " + ledger.getKey() + " : " + ledger.getValue().getState());
                Map<Long, Integer> numBookiesToReplacePerEnsemble =
                        inspectLedger(ledger.getValue(), bookieAddrs);
                System.out.print("summary: [");
                for (Map.Entry<Long, Integer> entry : numBookiesToReplacePerEnsemble.entrySet()) {
                    System.out.print(entry.getKey() + "=" + entry.getValue() + ", ");
                }
                System.out.println("]");
                System.out.println();
            }
            System.out.println("Done");
            return 0;
        }

        private Map<Long, Integer> inspectLedger(LedgerMetadata metadata, Set<BookieSocketAddress> bookiesToInspect) {
            Map<Long, Integer> numBookiesToReplacePerEnsemble = new TreeMap<Long, Integer>();
            for (Map.Entry<Long, ArrayList<BookieSocketAddress>> ensemble : metadata.getEnsembles().entrySet()) {
                ArrayList<BookieSocketAddress> bookieList = ensemble.getValue();
                System.out.print(ensemble.getKey() + ":\t");
                int numBookiesToReplace = 0;
                for (BookieSocketAddress bookie: bookieList) {
                    System.out.print(bookie.toString());
                    if (bookiesToInspect.contains(bookie)) {
                        System.out.print("*");
                        ++numBookiesToReplace;
                    } else {
                        System.out.print(" ");
                    }
                    System.out.print(" ");
                }
                System.out.println();
                numBookiesToReplacePerEnsemble.put(ensemble.getKey(), numBookiesToReplace);
            }
            return numBookiesToReplacePerEnsemble;
        }

        private int bkRecovery(final LinkedBlockingQueue<Long> ledgers, final Set<BookieSocketAddress> bookieAddrs,
                               final boolean dryrun, final boolean skipOpenLedgers)
                throws Exception {
            return runBKCommand(new BKCommandRunner() {
                @Override
                public int run(ZooKeeperClient zkc, BookKeeperClient bkc) throws Exception {
                    BookKeeperAdmin bkAdmin = new BookKeeperAdmin(bkc.get());
                    try {
                        bkRecovery(bkAdmin, ledgers, bookieAddrs, dryrun, skipOpenLedgers);
                        return 0;
                    } finally {
                        bkAdmin.close();
                    }
                }
            });
        }

        private int bkRecovery(final BookKeeperAdmin bkAdmin, final LinkedBlockingQueue<Long> ledgers,
                               final Set<BookieSocketAddress> bookieAddrs,
                               final boolean dryrun, final boolean skipOpenLedgers)
                throws InterruptedException, BKException {
            final AtomicInteger numPendings = new AtomicInteger(ledgers.size());
            final ExecutorService executorService = Executors.newCachedThreadPool();
            final CountDownLatch doneLatch = new CountDownLatch(concurrency);
            Runnable r = new Runnable() {
                @Override
                public void run() {
                    while (!ledgers.isEmpty()) {
                        long lid = -1L;
                        try {
                            lid = ledgers.take();
                            System.out.println("Recovering ledger " + lid);
                            bkAdmin.recoverBookieData(lid, bookieAddrs, dryrun, skipOpenLedgers);
                            System.out.println("Recovered ledger completed : " + lid + ", " + numPendings.decrementAndGet() + " left");
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                            doneLatch.countDown();
                            break;
                        } catch (BKException ke) {
                            System.out.println("Recovered ledger failed : " + lid + ", rc = " + BKException.getMessage(ke.getCode()));
                        }
                    }
                    doneLatch.countDown();
                }
            };
            for (int i = 0; i < concurrency; i++) {
                executorService.submit(r);
            }
            doneLatch.await();
            SchedulerUtils.shutdownScheduler(executorService, 2, TimeUnit.MINUTES);
            return 0;
        }

        private int bkRecovery(BookKeeperAdmin bkAdmin, Set<BookieSocketAddress> bookieAddrs,
                               boolean dryrun, boolean skipOpenLedgers)
                throws InterruptedException, BKException {
            bkAdmin.recoverBookieData(bookieAddrs, dryrun, skipOpenLedgers);
            return 0;
        }

        @Override
        protected String getUsage() {
            return "recover [options] <bookiesSrc>";
        }
    }

    /**
     * Per Ledger Command, which parse common options for per ledger. e.g. ledger id.
     */
    abstract static class PerLedgerCommand extends PerDLCommand {

        protected long ledgerId;

        protected PerLedgerCommand(String name, String description) {
            super(name, description);
            options.addOption("l", "ledger", true, "Ledger ID");
        }

        @Override
        protected void parseCommandLine(CommandLine cmdline) throws ParseException {
            super.parseCommandLine(cmdline);
            if (!cmdline.hasOption("l")) {
                throw new ParseException("No ledger provided.");
            }
            ledgerId = Long.parseLong(cmdline.getOptionValue("l"));
        }

        protected long getLedgerID() {
            return ledgerId;
        }

        protected void setLedgerId(long ledgerId) {
            this.ledgerId = ledgerId;
        }
    }

    protected static class RecoverLedgerCommand extends PerLedgerCommand {

        RecoverLedgerCommand() {
            super("recoverledger", "force recover ledger");
        }

        @Override
        protected int runCmd() throws Exception {
            LedgerHandle lh = getBookKeeperClient().get().openLedgerNoRecovery(
                    getLedgerID(), BookKeeper.DigestType.CRC32, dlConf.getBKDigestPW().getBytes(UTF_8));
            final CountDownLatch doneLatch = new CountDownLatch(1);
            final AtomicInteger resultHolder = new AtomicInteger(-1234);
            BookkeeperInternalCallbacks.GenericCallback<Void> recoverCb =
                    new BookkeeperInternalCallbacks.GenericCallback<Void>() {
                @Override
                public void operationComplete(int rc, Void result) {
                    resultHolder.set(rc);
                    doneLatch.countDown();
                }
            };
            try {
                BookKeeperAccessor.forceRecoverLedger(lh, recoverCb);
                doneLatch.await();
                if (BKException.Code.OK != resultHolder.get()) {
                    throw BKException.create(resultHolder.get());
                }
            } finally {
                lh.close();
            }
            return 0;
        }

        @Override
        protected String getUsage() {
            return "recoverledger [options]";
        }
    }

    protected static class ReadLastConfirmedCommand extends PerLedgerCommand {

        ReadLastConfirmedCommand() {
            super("readlac", "read last add confirmed for a given ledger");
        }

        @Override
        protected int runCmd() throws Exception {
            LedgerHandle lh = getBookKeeperClient().get().openLedgerNoRecovery(
                    getLedgerID(), BookKeeper.DigestType.CRC32, dlConf.getBKDigestPW().getBytes(UTF_8));
            try {
                long lac = lh.readLastConfirmed();
                System.out.println("LastAddConfirmed: " + lac);
            } finally {
                lh.close();
            }
            return 0;
        }

        @Override
        protected String getUsage() {
            return "readlac [options]";
        }
    }

    protected static class ReadEntriesCommand extends PerLedgerCommand {

        Long fromEntryId;
        Long untilEntryId;
        boolean printHex = false;
        boolean skipPayload = false;
        boolean readAllBookies = false;
        boolean readLac = false;
        boolean corruptOnly = false;

        int metadataVersion = LogSegmentMetadata.LEDGER_METADATA_CURRENT_LAYOUT_VERSION;

        ReadEntriesCommand() {
            super("readentries", "read entries for a given ledger");
            options.addOption("x", "hex", false, "Print record in hex format");
            options.addOption("sp", "skip-payload", false, "Skip printing the payload of the record");
            options.addOption("fid", "from", true, "Entry id to start reading");
            options.addOption("uid", "until", true, "Entry id to read until");
            options.addOption("bks", "all-bookies", false, "Read entry from all bookies");
            options.addOption("lac", "last-add-confirmed", false, "Return last add confirmed rather than entry payload");
            options.addOption("ver", "metadata-version", true, "The log segment metadata version to use");
            options.addOption("bad", "corrupt-only", false, "Display info for corrupt entries only");
        }

        @Override
        protected void parseCommandLine(CommandLine cmdline) throws ParseException {
            super.parseCommandLine(cmdline);
            printHex = cmdline.hasOption("x");
            skipPayload = cmdline.hasOption("sp");
            if (cmdline.hasOption("fid")) {
                fromEntryId = Long.parseLong(cmdline.getOptionValue("fid"));
            }
            if (cmdline.hasOption("uid")) {
                untilEntryId = Long.parseLong(cmdline.getOptionValue("uid"));
            }
            if (cmdline.hasOption("ver")) {
                metadataVersion = Integer.parseInt(cmdline.getOptionValue("ver"));
            }
            corruptOnly = cmdline.hasOption("bad");
            readAllBookies = cmdline.hasOption("bks");
            readLac = cmdline.hasOption("lac");
        }

        @Override
        protected int runCmd() throws Exception {
            LedgerHandle lh = getBookKeeperClient().get().openLedgerNoRecovery(getLedgerID(), BookKeeper.DigestType.CRC32,
                    dlConf.getBKDigestPW().getBytes(UTF_8));
            try {
                if (null == fromEntryId) {
                    fromEntryId = 0L;
                }
                if (null == untilEntryId) {
                    untilEntryId = lh.readLastConfirmed();
                }
                if (untilEntryId >= fromEntryId) {
                    if (readAllBookies) {
                        LedgerReader lr = new LedgerReader(getBookKeeperClient().get());
                        if (readLac) {
                            readLacsFromAllBookies(lr, lh, fromEntryId, untilEntryId);
                        } else {
                            readEntriesFromAllBookies(lr, lh, fromEntryId, untilEntryId);
                        }
                    } else {
                        simpleReadEntries(lh, fromEntryId, untilEntryId);
                    }
                } else {
                    System.out.println("No entries.");
                }
            } finally {
                lh.close();
            }
            return 0;
        }

        private void readEntriesFromAllBookies(LedgerReader ledgerReader, LedgerHandle lh, long fromEntryId, long untilEntryId)
                throws Exception {
            for (long eid = fromEntryId; eid <= untilEntryId; ++eid) {
                final CountDownLatch doneLatch = new CountDownLatch(1);
                final AtomicReference<Set<LedgerReader.ReadResult<InputStream>>> resultHolder =
                        new AtomicReference<Set<LedgerReader.ReadResult<InputStream>>>();
                ledgerReader.readEntriesFromAllBookies(lh, eid, new BookkeeperInternalCallbacks.GenericCallback<Set<LedgerReader.ReadResult<InputStream>>>() {
                    @Override
                    public void operationComplete(int rc, Set<LedgerReader.ReadResult<InputStream>> readResults) {
                        if (BKException.Code.OK == rc) {
                            resultHolder.set(readResults);
                        } else {
                            resultHolder.set(null);
                        }
                        doneLatch.countDown();
                    }
                });
                doneLatch.await();
                Set<LedgerReader.ReadResult<InputStream>> readResults = resultHolder.get();
                if (null == readResults) {
                    throw new IOException("Failed to read entry " + eid);
                }
                boolean printHeader = true;
                for (LedgerReader.ReadResult<InputStream> rr : readResults) {
                    if (corruptOnly) {
                        if (BKException.Code.DigestMatchException == rr.getResultCode()) {
                            if (printHeader) {
                                System.out.println("\t" + eid + "\t:");
                                printHeader = false;
                            }
                            System.out.println("\tbookie=" + rr.getBookieAddress());
                            System.out.println("\t-------------------------------");
                            System.out.println("status = " + BKException.getMessage(rr.getResultCode()));
                            System.out.println("\t-------------------------------");
                        }
                    } else {
                        if (printHeader) {
                            System.out.println("\t" + eid + "\t:");
                            printHeader = false;
                        }
                        System.out.println("\tbookie=" + rr.getBookieAddress());
                        System.out.println("\t-------------------------------");
                        if (BKException.Code.OK == rr.getResultCode()) {
                            Entry.Reader reader = Entry.newBuilder()
                                    .setLogSegmentInfo(lh.getId(), 0L)
                                    .setEntryId(eid)
                                    .setInputStream(rr.getValue())
                                    .setEnvelopeEntry(LogSegmentMetadata.supportsEnvelopedEntries(metadataVersion))
                                    .buildReader();
                            printEntry(reader);
                        } else {
                            System.out.println("status = " + BKException.getMessage(rr.getResultCode()));
                        }
                        System.out.println("\t-------------------------------");
                    }
                }
            }
        }

        private void readLacsFromAllBookies(LedgerReader ledgerReader, LedgerHandle lh, long fromEntryId, long untilEntryId)
                throws Exception {
            for (long eid = fromEntryId; eid <= untilEntryId; ++eid) {
                final CountDownLatch doneLatch = new CountDownLatch(1);
                final AtomicReference<Set<LedgerReader.ReadResult<Long>>> resultHolder =
                        new AtomicReference<Set<LedgerReader.ReadResult<Long>>>();
                ledgerReader.readLacs(lh, eid, new BookkeeperInternalCallbacks.GenericCallback<Set<LedgerReader.ReadResult<Long>>>() {
                    @Override
                    public void operationComplete(int rc, Set<LedgerReader.ReadResult<Long>> readResults) {
                        if (BKException.Code.OK == rc) {
                            resultHolder.set(readResults);
                        } else {
                            resultHolder.set(null);
                        }
                        doneLatch.countDown();
                    }
                });
                doneLatch.await();
                Set<LedgerReader.ReadResult<Long>> readResults = resultHolder.get();
                if (null == readResults) {
                    throw new IOException("Failed to read entry " + eid);
                }
                System.out.println("\t" + eid + "\t:");
                for (LedgerReader.ReadResult<Long> rr : readResults) {
                    System.out.println("\tbookie=" + rr.getBookieAddress());
                    System.out.println("\t-------------------------------");
                    if (BKException.Code.OK == rr.getResultCode()) {
                        System.out.println("Eid = " + rr.getEntryId() + ", Lac = " + rr.getValue());
                    } else {
                        System.out.println("status = " + BKException.getMessage(rr.getResultCode()));
                    }
                    System.out.println("\t-------------------------------");
                }
            }
        }

        private void simpleReadEntries(LedgerHandle lh, long fromEntryId, long untilEntryId) throws Exception {
            Enumeration<LedgerEntry> entries = lh.readEntries(fromEntryId, untilEntryId);
            long i = fromEntryId;
            System.out.println("Entries:");
            while (entries.hasMoreElements()) {
                LedgerEntry entry = entries.nextElement();
                System.out.println("\t" + i  + "(eid=" + entry.getEntryId() + ")\t: ");
                Entry.Reader reader = Entry.newBuilder()
                        .setLogSegmentInfo(0L, 0L)
                        .setEntryId(entry.getEntryId())
                        .setInputStream(entry.getEntryInputStream())
                        .setEnvelopeEntry(LogSegmentMetadata.supportsEnvelopedEntries(metadataVersion))
                        .buildReader();
                printEntry(reader);
                ++i;
            }
        }

        private void printEntry(Entry.Reader reader) throws Exception {
            LogRecordWithDLSN record = reader.nextRecord();
            while (null != record) {
                System.out.println("\t" + record);
                if (!skipPayload) {
                    if (printHex) {
                        System.out.println(Hex.encodeHexString(record.getPayload()));
                    } else {
                        System.out.println(new String(record.getPayload(), UTF_8));
                    }
                }
                System.out.println("");
                record = reader.nextRecord();
            }
        }

        @Override
        protected String getUsage() {
            return "readentries [options]";
        }
    }

    protected static abstract class AuditCommand extends OptsCommand {

        protected final Options options = new Options();
        protected final DistributedLogConfiguration dlConf;
        protected final List<URI> uris = new ArrayList<URI>();
        protected String zkAclId = null;
        protected boolean force = false;

        protected AuditCommand(String name, String description) {
            super(name, description);
            dlConf = new DistributedLogConfiguration();
            options.addOption("u", "uris", true, "List of distributedlog uris, separated by comma");
            options.addOption("c", "conf", true, "DistributedLog Configuration File");
            options.addOption("a", "zk-acl-id", true, "ZooKeeper ACL ID");
            options.addOption("f", "force", false, "Force command (no warnings or prompts)");
        }

        @Override
        protected int runCmd(CommandLine commandLine) throws Exception {
            try {
                parseCommandLine(commandLine);
            } catch (ParseException pe) {
                System.err.println("ERROR: failed to parse commandline : '" + pe.getMessage() + "'");
                printUsage();
                return -1;
            }
            return runCmd();
        }

        protected abstract int runCmd() throws Exception;

        @Override
        protected Options getOptions() {
            return options;
        }

        protected void parseCommandLine(CommandLine cmdline) throws ParseException {
            if (!cmdline.hasOption("u")) {
                throw new ParseException("No distributedlog uri provided.");
            }
            String urisStr = cmdline.getOptionValue("u");
            for (String uriStr : urisStr.split(",")) {
                uris.add(URI.create(uriStr));
            }
            if (cmdline.hasOption("c")) {
                String configFile = cmdline.getOptionValue("c");
                try {
                    dlConf.loadConf(new File(configFile).toURI().toURL());
                } catch (ConfigurationException e) {
                    throw new ParseException("Failed to load distributedlog configuration from " + configFile + ".");
                } catch (MalformedURLException e) {
                    throw new ParseException("Failed to load distributedlog configuration from malformed "
                            + configFile + ".");
                }
            }
            if (cmdline.hasOption("a")) {
                zkAclId = cmdline.getOptionValue("a");
            }
            if (cmdline.hasOption("f")) {
                force = true;
            }
        }

        protected DistributedLogConfiguration getConf() {
            return dlConf;
        }

        protected List<URI> getUris() {
            return uris;
        }

        protected String getZkAclId() {
            return zkAclId;
        }

        protected boolean getForce() {
            return force;
        }

    }

    static class AuditLedgersCommand extends AuditCommand {

        String ledgersFilePrefix;
        final List<List<String>> allocationPaths =
                new ArrayList<List<String>>();

        AuditLedgersCommand() {
            super("audit_ledgers", "Audit ledgers between bookkeeper and DL uris");
            options.addOption("lf", "ledgers-file", true, "Prefix of filename to store ledgers");
            options.addOption("ap", "allocation-paths", true, "Allocation paths per uri. E.g ap10;ap11,ap20");
        }

        @Override
        protected void parseCommandLine(CommandLine cmdline) throws ParseException {
            super.parseCommandLine(cmdline);
            if (cmdline.hasOption("lf")) {
                ledgersFilePrefix = cmdline.getOptionValue("lf");
            } else {
                throw new ParseException("No file specified to store leak ledgers");
            }
            if (cmdline.hasOption("ap")) {
                String[] aps = cmdline.getOptionValue("ap").split(",");
                for(String ap : aps) {
                    List<String> list = new ArrayList<String>();
                    String[] array = ap.split(";");
                    Collections.addAll(list, array);
                    allocationPaths.add(list);
                }
            } else {
                throw new ParseException("No allocation paths provided.");
            }
        }

        void dumpLedgers(Set<Long> ledgers, File targetFile) throws Exception {
            PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(targetFile), UTF_8.name()));
            try {
                for (Long ledger : ledgers) {
                    pw.println(ledger);
                }
            } finally {
                pw.close();
            }
            System.out.println("Dump " + ledgers.size() + " ledgers to file : " + targetFile);
        }

        @Override
        protected int runCmd() throws Exception {
            if (!getForce() && !IOUtils.confirmPrompt("Do you want to audit uris : "
                    + getUris() + ", allocation paths = " + allocationPaths)) {
                return 0;
            }

            DLAuditor dlAuditor = new DLAuditor(getConf());
            try {
                Pair<Set<Long>, Set<Long>> bkdlLedgers = dlAuditor.collectLedgers(getUris(), allocationPaths);
                dumpLedgers(bkdlLedgers.getLeft(), new File(ledgersFilePrefix + "-bkledgers.txt"));
                dumpLedgers(bkdlLedgers.getRight(), new File(ledgersFilePrefix + "-dlledgers.txt"));
                dumpLedgers(Sets.difference(bkdlLedgers.getLeft(), bkdlLedgers.getRight()),
                            new File(ledgersFilePrefix + "-leakledgers.txt"));
            } finally {
                dlAuditor.close();
            }
            return 0;
        }

        @Override
        protected String getUsage() {
            return "audit_ledgers [options]";
        }
    }

    public static class AuditDLSpaceCommand extends PerDLCommand {

        private String regex = null;

        AuditDLSpaceCommand() {
            super("audit_dl_space", "Audit stream space usage for a given dl uri");
            options.addOption("groupByRegex", true, "Group by the result of applying the regex to stream name");
        }

        @Override
        protected void parseCommandLine(CommandLine cmdline) throws ParseException {
            super.parseCommandLine(cmdline);
            if (cmdline.hasOption("groupByRegex")) {
                regex = cmdline.getOptionValue("groupByRegex");
            }
        }

        @Override
        protected int runCmd() throws Exception {
            DLAuditor dlAuditor = new DLAuditor(getConf());
            try {
                Map<String, Long> streamSpaceMap = dlAuditor.calculateStreamSpaceUsage(getUri());
                if (null != regex) {
                    printGroupByRegexSpaceUsage(streamSpaceMap, regex);
                } else {
                    printSpaceUsage(streamSpaceMap);
                }
            } finally {
                dlAuditor.close();
            }
            return 0;
        }

        @Override
        protected String getUsage() {
            return "audit_dl_space [options]";
        }

        private void printSpaceUsage(Map<String, Long> spaceMap) throws Exception {
            for (Map.Entry<String, Long> entry : spaceMap.entrySet()) {
                System.out.println(entry.getKey() + "\t" + entry.getValue());
            }
        }

        private void printGroupByRegexSpaceUsage(Map<String, Long> streamSpaceMap, String regex) throws Exception {
            Pattern pattern = Pattern.compile(regex);
            Map<String, Long> groupedUsageMap = new HashMap<String, Long>();
            for (Map.Entry<String, Long> entry : streamSpaceMap.entrySet()) {
                Matcher matcher = pattern.matcher(entry.getKey());
                String key = entry.getKey();
                boolean matches = matcher.matches();
                if (matches) {
                    key = matcher.group(1);
                }
                Long value = entry.getValue();
                if (groupedUsageMap.containsKey(key)) {
                    value += groupedUsageMap.get(key);
                }
                groupedUsageMap.put(key, value);
            }
            printSpaceUsage(groupedUsageMap);
        }
    }

    public static class AuditBKSpaceCommand extends PerDLCommand {

        AuditBKSpaceCommand() {
            super("audit_bk_space", "Audit bk space usage for a given dl uri");
        }

        @Override
        protected int runCmd() throws Exception {
            DLAuditor dlAuditor = new DLAuditor(getConf());
            try {
                long spaceUsage = dlAuditor.calculateLedgerSpaceUsage(uri);
                System.out.println("bookkeeper ledgers space usage \t " + spaceUsage);
            } finally {
                dlAuditor.close();
            }
            return 0;
        }

        @Override
        protected String getUsage() {
            return "audit_bk_space [options]";
        }
    }

    protected static class TruncateStreamCommand extends PerStreamCommand {

        DLSN dlsn = DLSN.InvalidDLSN;

        TruncateStreamCommand() {
            super("truncate_stream", "truncate a stream at a specific position");
            options.addOption("dlsn", true, "Truncate all records older than this dlsn");
        }

        public void setDlsn(DLSN dlsn) {
            this.dlsn = dlsn;
        }

        @Override
        protected void parseCommandLine(CommandLine cmdline) throws ParseException {
            super.parseCommandLine(cmdline);
            if (cmdline.hasOption("dlsn")) {
                dlsn = parseDLSN(cmdline.getOptionValue("dlsn"));
            }
        }

        @Override
        protected int runCmd() throws Exception {
            getConf().setZkAclId(getZkAclId());
            return truncateStream(getFactory(), getStreamName(), dlsn);
        }

        private int truncateStream(final com.twitter.distributedlog.DistributedLogManagerFactory factory, String streamName, DLSN dlsn) throws Exception {
            DistributedLogManager dlm = factory.createDistributedLogManagerWithSharedClients(streamName);
            try {
                long totalRecords = dlm.getLogRecordCount();
                long recordsAfterTruncate = Await.result(dlm.getLogRecordCountAsync(dlsn));
                long recordsToTruncate = totalRecords - recordsAfterTruncate;
                if (!getForce() && !IOUtils.confirmPrompt("Do you want to truncate " + streamName + " at dlsn " + dlsn + " (" + recordsToTruncate + " records)?")) {
                    return 0;
                } else {
                    AsyncLogWriter writer = dlm.startAsyncLogSegmentNonPartitioned();
                    try {
                        if (!Await.result(writer.truncate(dlsn))) {
                            System.out.println("Failed to truncate.");
                        }
                        return 0;
                    } finally {
                        Utils.close(writer);
                    }
                }
            } catch (Exception ex) {
                System.err.println("Failed to truncate " + ex);
                return 1;
            } finally {
                dlm.close();
            }
        }
    }

    public static class DeserializeDLSNCommand extends SimpleCommand {

        String base64Dlsn = "";

        DeserializeDLSNCommand() {
            super("deserialize_dlsn", "Deserialize DLSN");
            options.addOption("b64", "base64", true, "Base64 encoded dlsn");
        }

        protected void parseCommandLine(CommandLine cmdline) throws ParseException {
            if (cmdline.hasOption("b64")) {
                base64Dlsn = cmdline.getOptionValue("b64");
            } else {
                throw new IllegalArgumentException("Argument b64 is required");
            }
        }

        @Override
        protected int runSimpleCmd() throws Exception {
            System.out.println(DLSN.deserialize(base64Dlsn).toString());
            return 0;
        }
    }

    public static class SerializeDLSNCommand extends SimpleCommand {

        private DLSN dlsn = DLSN.InitialDLSN;
        private boolean hex = false;

        SerializeDLSNCommand() {
            super("serialize_dlsn", "Serialize DLSN. Default format is base64 string.");
            options.addOption("dlsn", true, "DLSN in comma separated format to serialize");
            options.addOption("x", "hex", false, "Emit hex-encoded string DLSN instead of base 64");
        }

        protected void parseCommandLine(CommandLine cmdline) throws ParseException {
            if (cmdline.hasOption("dlsn")) {
                dlsn = parseDLSN(cmdline.getOptionValue("dlsn"));
            }
            hex = cmdline.hasOption("x");
        }

        @Override
        protected int runSimpleCmd() throws Exception {
            if (hex) {
                byte[] bytes = dlsn.serializeBytes();
                String hexString = Hex.encodeHexString(bytes);
                System.out.println(hexString);
            } else {
                System.out.println(dlsn.serialize());
            }
            return 0;
        }
    }

    public DistributedLogTool() {
        super();
        addCommand(new AuditBKSpaceCommand());
        addCommand(new AuditLedgersCommand());
        addCommand(new AuditDLSpaceCommand());
        addCommand(new CreateCommand());
        addCommand(new CountCommand());
        addCommand(new DeleteCommand());
        addCommand(new DeleteAllocatorPoolCommand());
        addCommand(new DeleteLedgersCommand());
        addCommand(new DumpCommand());
        addCommand(new InspectCommand());
        addCommand(new InspectStreamCommand());
        addCommand(new ListCommand());
        addCommand(new ReadLastConfirmedCommand());
        addCommand(new ReadEntriesCommand());
        addCommand(new RecoverCommand());
        addCommand(new RecoverLedgerCommand());
        addCommand(new ShowCommand());
        addCommand(new TruncateCommand());
        addCommand(new TruncateStreamCommand());
        addCommand(new DeserializeDLSNCommand());
        addCommand(new SerializeDLSNCommand());
    }

    @Override
    protected String getName() {
        return "dlog_tool";
    }

}