/* -*- Mode: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/*
 *     Copyright 2017-2020 Couchbase, Inc.
 *
 *   Licensed 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.
 */

#include "config.h"
#include <sys/types.h>
#include <libcouchbase/couchbase.h>
#include <iostream>
#include <map>
#include <cassert>
#include <cstdio>
#include <cerrno>
#include <stdexcept>
#include <sstream>
#include "common/options.h"
#include "common/histogram.h"
#include <libcouchbase/metrics.h>

#include "linenoise/linenoise.h"

static std::string get_resp_key(const lcb_RESPSUBDOC *resp)
{
    const char *p;
    size_t n;

    lcb_respsubdoc_key(resp, &p, &n);
    if (!n) {
        return "";
    }
    return std::string(p, n);
}

extern "C" {
void subdoc_callback(lcb_INSTANCE *, int, const lcb_RESPSUBDOC *resp)
{
    lcb_STATUS rc;
    std::string key = get_resp_key(resp);

    rc = lcb_respsubdoc_status(resp);
    if (rc == LCB_SUCCESS) {
        uint64_t cas;
        lcb_respsubdoc_cas(resp, &cas);
        fprintf(stderr, "%-20s CAS=0x%" PRIx64 "\n", key.c_str(), cas);
    } else {
        fprintf(stderr, "%-20s %s\n", key.c_str(), lcb_strerror_short(rc));
        const lcb_KEY_VALUE_ERROR_CONTEXT *ctx;
        lcb_respsubdoc_error_context(resp, &ctx);
        const char *context, *ref;
        size_t context_len, ref_len;
        lcb_errctx_kv_context(ctx, &context, &context_len);
        fprintf(stderr, "%-20s %.*s\n", "", (int)context_len, context);
        lcb_errctx_kv_ref(ctx, &ref, &ref_len);
        fprintf(stderr, "%-20s Ref: %.*s\n", "", (int)ref_len, ref);
    }
    size_t total = lcb_respsubdoc_result_size(resp);
    for (size_t ii = 0; ii < total; ii++) {
        rc = lcb_respsubdoc_result_status(resp, ii);
        const char *value;
        size_t nvalue;
        lcb_respsubdoc_result_value(resp, ii, &value, &nvalue);
        printf("%d. Size=%lu, RC=%s\n", (int)ii, (unsigned long)nvalue, lcb_strerror_short(rc));
        fflush(stdout);
        if (nvalue > 0) {
            fwrite(value, 1, nvalue, stdout);
            printf("\n");
            fflush(stdout);
        }
    }
}
}

#define CBCSUBDOC_HISTORY_FILENAME ".cbcsubdoc_history"

using namespace cbc;
using namespace cliopts;

static void do_or_die(lcb_STATUS rc, std::string msg = "")
{
    if (rc != LCB_SUCCESS) {
        std::stringstream ss;
        if (!msg.empty()) {
            ss << msg << ". ";
        }
        ss << lcb_strerror_short(rc);
        throw std::runtime_error(ss.str());
    }
}

static lcb_INSTANCE *instance = NULL;
static Histogram hg;

class Configuration
{
  public:
    Configuration() {}

    ~Configuration() {}

    void addToParser(Parser &parser)
    {
        m_params.addToParser(parser);
    }

    void processOptions() {}

    void fillCropts(lcb_CREATEOPTS *&opts)
    {
        m_params.fillCropts(opts);
    }
    lcb_STATUS doCtls()
    {
        return m_params.doCtls(instance);
    }
    bool useTimings()
    {
        return m_params.useTimings();
    }
    bool shouldDump()
    {
        return m_params.shouldDump();
    }

  private:
    ConnParams m_params;
};

static Configuration config;

static const char *handlers_sorted[] = {"help",
                                        "dump",
                                        "get",
                                        "set",
                                        "exists",
                                        "remove",
                                        "replace",
                                        "array-insert",
                                        "array-add-first",
                                        "array-add-last",
                                        "array-add-unique",
                                        "dict-add",
                                        "dict-upsert",
                                        "counter",
                                        "size",
                                        NULL};

typedef enum {
    SUBDOC_GET = 1,
    SUBDOC_EXISTS,
    SUBDOC_REPLACE,
    SUBDOC_DICT_ADD,
    SUBDOC_DICT_UPSERT,
    SUBDOC_ARRAY_ADD_FIRST,
    SUBDOC_ARRAY_ADD_LAST,
    SUBDOC_ARRAY_ADD_UNIQUE,
    SUBDOC_ARRAY_INSERT,
    SUBDOC_COUNTER,
    SUBDOC_REMOVE,
    SUBDOC_GET_COUNT,
    SUBDOC_GET_FULLDOC,
    SUBDOC_SET_FULLDOC,
    SUBDOC_REMOVE_FULLDOC
} SubdocOperation;

static void command_completion(const char *buf, linenoiseCompletions *lc)
{
    size_t nbuf = strlen(buf);
    for (const char **cur = handlers_sorted; *cur; cur++) {
        if (memcmp(buf, *cur, nbuf) == 0) {
            linenoiseAddCompletion(lc, *cur);
        }
    }
}

namespace subdoc
{
class Handler;
}

static std::map< std::string, subdoc::Handler * > handlers;

namespace subdoc
{
#define HANDLER_DESCRIPTION(s)                                                                                         \
    const char *description() const                                                                                    \
    {                                                                                                                  \
        return s;                                                                                                      \
    }
#define HANDLER_USAGE(s)                                                                                               \
    const char *usagestr() const                                                                                       \
    {                                                                                                                  \
        return s;                                                                                                      \
    }

class Handler
{
  public:
    Handler(const char *name) : parser(name)
    {
        if (name != NULL) {
            cmdname = name;
        }
        parser.default_settings.error_noexit = 1;
        parser.default_settings.help_noexit = 1;
    }

    virtual ~Handler() {}
    virtual const char *description() const
    {
        return NULL;
    }
    virtual const char *usagestr() const
    {
        return NULL;
    }
    void execute(int argc, char **argv)
    {
        parser.reset();
        parser.default_settings.argstring = usagestr();
        parser.default_settings.shortdesc = description();
        addOptions();
        if (parser.parse(argc, argv, true)) {
            run();
        }
    }

  protected:
    virtual const std::string &getLoneArg(bool required = false)
    {
        static std::string empty("");

        const std::vector< std::string > &args = parser.getRestArgs();
        if (args.empty() || args.size() != 1) {
            if (required) {
                throw BadArg("Command requires single argument");
            }
            return empty;
        }
        return args[0];
    }

    virtual const std::string &getRequiredArg()
    {
        return getLoneArg(true);
    }

    virtual void addOptions() {}

    virtual void run() = 0;

    void splitNameValue(std::string &arg, const char **name, size_t *nname, const char **value, size_t *nvalue)
    {
        size_t sep = arg.find("=");
        if (sep == std::string::npos) {
            throw BadArg("Name and value have to be separated with '='");
        }

        const char *k = arg.c_str();
        size_t nk = sep;
        for (size_t j = nk - 1; j > 0; j--, nk--) {
            if (k[j] != ' ' && k[j] != '\t') {
                break;
            }
        }
        if (nk == 0) {
            throw BadArg("Name cannot be empty");
        }

        *name = k;
        *nname = nk;
        *value = arg.c_str() + sep + 1;
        *nvalue = arg.size() - sep - 1;
    }

    cliopts::Parser parser;
    std::string cmdname;
};

class LookupHandler : public Handler
{
  public:
    HANDLER_USAGE("[OPTIONS...] KEY...")

    LookupHandler(const char *name, SubdocOperation opcode, const char *description_)
        : Handler(name), m_opcode(opcode), m_description(description_), o_paths("path"), o_xattrs("xattr"),
          o_deleted("deleted")
    {
        o_paths.abbrev('p').argdesc("PATH").description("JSON path in the document");
        o_xattrs.abbrev('x').argdesc("PATH").description("Access XATTR path (exentnded attributes)");
        o_deleted.abbrev('d').description("Access XATTR attributes of deleted documents");
    }

    const char *description() const
    {
        return m_description;
    }

  protected:
    void addOptions()
    {
        Handler::addOptions();
        parser.addOption(o_paths.reset());
        parser.addOption(o_xattrs.reset());
        parser.addOption(o_deleted.reset());
    }

    void run()
    {
        lcb_STATUS err;

        const std::vector< std::string > &keys = parser.getRestArgs();
        if (keys.empty()) {
            throw BadArg("At least one key has to be specified");
        }
        std::vector< std::string > paths = o_paths.result();
        std::vector< std::string > xattrs = o_xattrs.result();

        if (m_opcode != SUBDOC_GET) {
            if (paths.empty() && xattrs.empty()) {
                throw BadArg("At least one path has to be specified");
            }
        }

        lcb_sched_enter(instance);
        for (size_t ii = 0; ii < keys.size(); ++ii) {
            lcb_SUBDOCSPECS *specs;
            size_t idx = 0, total = xattrs.size() + paths.size();
            if (paths.empty() && m_opcode == SUBDOC_GET) {
                total += 1; /* for fulldoc get */
            }
            lcb_subdocspecs_create(&specs, total);
            for (std::vector< std::string >::const_iterator it = xattrs.begin(); it != xattrs.end(); ++it) {
                uint32_t flags = LCB_SUBDOCSPECS_F_XATTRPATH;
                if (o_deleted.passed()) {
                    flags |= LCB_SUBDOCSPECS_F_XATTR_DELETED_OK;
                }
                switch (m_opcode) {
                    case SUBDOC_GET:
                        lcb_subdocspecs_get(specs, idx++, flags, it->c_str(), it->size());
                        break;
                    case SUBDOC_EXISTS:
                        lcb_subdocspecs_exists(specs, idx++, flags, it->c_str(), it->size());
                        break;
                    case SUBDOC_GET_COUNT:
                        lcb_subdocspecs_get_count(specs, idx++, flags, it->c_str(), it->size());
                        break;
                    default:
                        break;
                }
            }
            for (std::vector< std::string >::const_iterator it = paths.begin(); it != paths.end(); ++it) {
                switch (m_opcode) {
                    case SUBDOC_GET:
                        lcb_subdocspecs_get(specs, idx++, 0, it->c_str(), it->size());
                        break;
                    case SUBDOC_EXISTS:
                        lcb_subdocspecs_exists(specs, idx++, 0, it->c_str(), it->size());
                        break;
                    case SUBDOC_GET_COUNT:
                        lcb_subdocspecs_get_count(specs, idx++, 0, it->c_str(), it->size());
                        break;
                    default:
                        break;
                }
            }
            if (paths.empty() && m_opcode == SUBDOC_GET) {
                lcb_subdocspecs_get(specs, idx++, 0, NULL, 0);
            }

            const std::string &key = keys[ii];
            lcb_CMDSUBDOC *cmd;
            lcb_cmdsubdoc_create(&cmd);
            lcb_cmdsubdoc_key(cmd, key.c_str(), key.size());
            lcb_cmdsubdoc_specs(cmd, specs);
            err = lcb_subdoc(instance, this, cmd);
            lcb_subdocspecs_destroy(specs);
            lcb_cmdsubdoc_destroy(cmd);
            if (err != LCB_SUCCESS) {
                throw LcbError(err, "Failed to schedule " + cmdname + " command");
            }
        }
        lcb_sched_leave(instance);
        err = lcb_wait(instance, LCB_WAIT_DEFAULT);
        if (err != LCB_SUCCESS) {
            throw LcbError(err, "Failed to execute " + cmdname + " command");
        }
    }

  protected:
    SubdocOperation m_opcode;
    const char *m_description;

    cliopts::ListOption o_paths;
    cliopts::ListOption o_xattrs;
    cliopts::BoolOption o_deleted;
};

class RemoveHandler : public Handler
{
  public:
    HANDLER_DESCRIPTION("Remove path in the item on the server")
    HANDLER_USAGE("[OPTIONS...] KEY...")

    RemoveHandler(const char *name = "remove") : Handler(name), o_paths("path"), o_xattrs("xattr")
    {
        o_paths.abbrev('p').argdesc("PATH").description(
            "JSON path in the document. When skipped, the operation applied to full document.");
        o_xattrs.abbrev('x').argdesc("PATH").description("Access XATTR path (exentnded attributes)");
    }

  protected:
    void addOptions()
    {
        Handler::addOptions();
        parser.addOption(o_paths.reset());
        parser.addOption(o_xattrs.reset());
    }

    void run()
    {
        lcb_STATUS err;

        const std::vector< std::string > &keys = parser.getRestArgs();
        if (keys.empty()) {
            throw BadArg("At least one key has to be specified");
        }
        std::vector< std::string > paths = o_paths.result();
        std::vector< std::string > xattrs = o_xattrs.result();

        lcb_sched_enter(instance);
        for (size_t ii = 0; ii < keys.size(); ++ii) {
            lcb_SUBDOCSPECS *specs;
            size_t idx = 0, total = xattrs.size() + (paths.empty() ? 1 : paths.size());

            lcb_subdocspecs_create(&specs, total);
            for (std::vector< std::string >::const_iterator it = xattrs.begin(); it != xattrs.end(); ++it) {
                lcb_subdocspecs_remove(specs, idx++, LCB_SUBDOCSPECS_F_XATTRPATH, it->c_str(), it->size());
            }
            for (std::vector< std::string >::const_iterator it = paths.begin(); it != paths.end(); ++it) {
                lcb_subdocspecs_remove(specs, idx++, 0, it->c_str(), it->size());
            }
            if (paths.empty()) {
                lcb_subdocspecs_remove(specs, idx++, 0, NULL, 0);
            }

            const std::string &key = keys[ii];
            lcb_CMDSUBDOC *cmd;
            lcb_cmdsubdoc_create(&cmd);
            lcb_cmdsubdoc_key(cmd, key.c_str(), key.size());
            lcb_cmdsubdoc_specs(cmd, specs);
            err = lcb_subdoc(instance, this, cmd);
            lcb_subdocspecs_destroy(specs);
            lcb_cmdsubdoc_destroy(cmd);
            if (err != LCB_SUCCESS) {
                throw LcbError(err, "Failed to schedule remove command");
            }
        }
        lcb_sched_leave(instance);
        err = lcb_wait(instance, LCB_WAIT_DEFAULT);
        if (err != LCB_SUCCESS) {
            throw LcbError(err, "Failed to execute remove");
        }
    }

  protected:
    cliopts::ListOption o_paths;
    cliopts::ListOption o_xattrs;
};

class UpsertHandler : public Handler
{
  public:
    HANDLER_DESCRIPTION("Store document on the server")
    HANDLER_USAGE("[OPTIONS...] KEY VALUE")

    UpsertHandler(const char *name = "upsert") : Handler(name), o_xattrs("xattr"), o_expiry("expiry")
    {
        o_xattrs.abbrev('x').argdesc("PATH=VALUE").description("Store XATTR path (exentnded attributes)");
        o_expiry.abbrev('e').argdesc("TIME").description(
            "Expiration time in seconds. Relative (up to 30 days) or absolute (as Unix timestamp)");
    }

  protected:
    void addOptions()
    {
        Handler::addOptions();
        parser.addOption(o_xattrs.reset());
        parser.addOption(o_expiry.reset());
    }

    void run()
    {
        lcb_STATUS err;

        const std::vector< std::string > &args = parser.getRestArgs();
        if (args.size() != 2) {
            throw BadArg("Exactly two arguments required: KEY and VALUE");
        }
        std::string key = args[0];
        std::string value = args[1];
        std::vector< std::pair< std::string, std::string > > xattrs = o_xattrs.result();

        size_t idx = 0, total = xattrs.size() + 1;
        if (xattrs.size() == 0) {
            total += 1;
        }
        lcb_SUBDOCSPECS *specs;
        lcb_subdocspecs_create(&specs, total);

        std::string ver = "\"" LCB_CLIENT_ID "\"";
        std::string path = "_cbc.version";

        if (o_xattrs.passed()) {
            for (std::vector< std::pair< std::string, std::string > >::const_iterator it = xattrs.begin();
                 it != xattrs.end(); ++it) {
                lcb_subdocspecs_dict_upsert(specs, idx++,
                                            LCB_SUBDOCSPECS_F_XATTRPATH | LCB_SUBDOCSPECS_F_MKINTERMEDIATES,
                                            it->first.c_str(), it->first.size(), it->second.c_str(), it->second.size());
            }
        } else {
            // currently it is not possible to upsert document without XATTRs
            // so lets allocate "_cbc" object with some useful stuff
            lcb_subdocspecs_dict_upsert(specs, idx++, LCB_SUBDOCSPECS_F_XATTRPATH | LCB_SUBDOCSPECS_F_MKINTERMEDIATES,
                                        path.c_str(), path.size(), ver.c_str(), ver.size());
        }
        lcb_subdocspecs_replace(specs, idx++, 0, NULL, 0, value.c_str(), value.size());

        lcb_CMDSUBDOC *cmd;
        lcb_cmdsubdoc_create(&cmd);
        lcb_cmdsubdoc_key(cmd, key.c_str(), key.size());
        lcb_cmdsubdoc_specs(cmd, specs);
        lcb_cmdsubdoc_store_semantics(cmd, LCB_SUBDOC_STORE_UPSERT);
        if (o_expiry.passed()) {
            lcb_cmdsubdoc_expiry(cmd, o_expiry.result());
        }

        lcb_sched_enter(instance);
        err = lcb_subdoc(instance, this, cmd);
        lcb_subdocspecs_destroy(specs);
        lcb_cmdsubdoc_destroy(cmd);
        if (err != LCB_SUCCESS) {
            throw LcbError(err, "Failed to schedule upsert command");
        }
        lcb_sched_leave(instance);

        err = lcb_wait(instance, LCB_WAIT_DEFAULT);
        if (err != LCB_SUCCESS) {
            throw LcbError(err, "Failed to execute upsert");
        }
    }

  protected:
    cliopts::PairListOption o_xattrs;
    cliopts::UIntOption o_expiry;
};

class MutationHandler : public Handler
{
  public:
    HANDLER_USAGE("[OPTIONS...] KEY...")

    MutationHandler(const char *name, SubdocOperation opcode, const char *description_,
                    bool enable_intermediates = true)
        : Handler(name), m_opcode(opcode), m_description(description_), o_paths("path"), o_xattrs("xattr"),
          o_expiry("expiry"), o_intermediates("intermediates"), o_upsert("upsert"),
          m_enable_intermediates(enable_intermediates)
    {
        o_paths.abbrev('p').argdesc("PATH=VALUE").description("JSON path in the document");
        o_xattrs.abbrev('x').argdesc("PATH=VALUE").description("XATTR path (exentnded attributes)");
        o_expiry.abbrev('e').argdesc("TIME").description(
            "Expiration time in seconds. Relative (up to 30 days) or absolute (as Unix timestamp)");
        o_intermediates.abbrev('i').description("Create intermediate paths");
        o_upsert.abbrev('u').description("Create document if it doesn't exist");
    }

    const char *description() const
    {
        return m_description;
    }

  protected:
    void addOptions()
    {
        Handler::addOptions();
        parser.addOption(o_xattrs.reset());
        parser.addOption(o_paths.reset());
        parser.addOption(o_expiry.reset());
        parser.addOption(o_upsert.reset());
        if (m_enable_intermediates) {
            parser.addOption(o_intermediates.reset());
        }
    }

    void run()
    {
        lcb_STATUS err;

        const std::vector< std::string > &keys = parser.getRestArgs();
        if (keys.empty()) {
            throw BadArg("At least one key has to be specified");
        }
        std::vector< std::pair< std::string, std::string > > paths = o_paths.result();
        std::vector< std::pair< std::string, std::string > > xattrs = o_xattrs.result();

        if (xattrs.empty() && paths.empty()) {
            throw BadArg("At least one path has to be specified");
        }

        lcb_sched_enter(instance);

        for (size_t ii = 0; ii < keys.size(); ++ii) {
            size_t idx = 0, total = xattrs.size() + paths.size();
            lcb_SUBDOCSPECS *specs;
            lcb_subdocspecs_create(&specs, total);
            for (std::vector< std::pair< std::string, std::string > >::const_iterator it = xattrs.begin();
                 it != xattrs.end(); ++it) {
                uint32_t flags = LCB_SUBDOCSPECS_F_XATTRPATH;
                if (o_intermediates.passed()) {
                    flags |= LCB_SUBDOCSPECS_F_MKINTERMEDIATES;
                }
                switch (m_opcode) {
                    case SUBDOC_DICT_UPSERT:
                        lcb_subdocspecs_dict_upsert(specs, idx++, flags, it->first.c_str(), it->first.size(),
                                                    it->second.c_str(), it->second.size());
                        break;
                    case SUBDOC_DICT_ADD:
                        lcb_subdocspecs_dict_add(specs, idx++, flags, it->first.c_str(), it->first.size(),
                                                 it->second.c_str(), it->second.size());
                        break;
                    case SUBDOC_REPLACE:
                        lcb_subdocspecs_replace(specs, idx++, flags, it->first.c_str(), it->first.size(),
                                                it->second.c_str(), it->second.size());
                        break;
                    case SUBDOC_ARRAY_ADD_FIRST:
                        lcb_subdocspecs_array_add_first(specs, idx++, flags, it->first.c_str(), it->first.size(),
                                                        it->second.c_str(), it->second.size());
                        break;
                    case SUBDOC_ARRAY_ADD_LAST:
                        lcb_subdocspecs_array_add_last(specs, idx++, flags, it->first.c_str(), it->first.size(),
                                                       it->second.c_str(), it->second.size());
                        break;
                    case SUBDOC_ARRAY_ADD_UNIQUE:
                        lcb_subdocspecs_array_add_unique(specs, idx++, flags, it->first.c_str(), it->first.size(),
                                                         it->second.c_str(), it->second.size());
                        break;
                    case SUBDOC_ARRAY_INSERT:
                        lcb_subdocspecs_array_insert(specs, idx++, flags, it->first.c_str(), it->first.size(),
                                                     it->second.c_str(), it->second.size());
                        break;
                    case SUBDOC_COUNTER:
                        lcb_subdocspecs_counter(specs, idx++, flags, it->first.c_str(), it->first.size(),
                                                atoll(it->second.c_str()));
                        break;
                    default:
                        break;
                }
            }
            for (std::vector< std::pair< std::string, std::string > >::const_iterator it = paths.begin();
                 it != paths.end(); ++it) {
                uint32_t flags = 0;
                if (o_intermediates.passed()) {
                    flags |= LCB_SUBDOCSPECS_F_MKINTERMEDIATES;
                }
                switch (m_opcode) {
                    case SUBDOC_DICT_UPSERT:
                        lcb_subdocspecs_dict_upsert(specs, idx++, flags, it->first.c_str(), it->first.size(),
                                                    it->second.c_str(), it->second.size());
                        break;
                    case SUBDOC_DICT_ADD:
                        lcb_subdocspecs_dict_add(specs, idx++, flags, it->first.c_str(), it->first.size(),
                                                 it->second.c_str(), it->second.size());
                        break;
                    case SUBDOC_REPLACE:
                        lcb_subdocspecs_replace(specs, idx++, flags, it->first.c_str(), it->first.size(),
                                                it->second.c_str(), it->second.size());
                        break;
                    case SUBDOC_ARRAY_ADD_FIRST:
                        lcb_subdocspecs_array_add_first(specs, idx++, flags, it->first.c_str(), it->first.size(),
                                                        it->second.c_str(), it->second.size());
                        break;
                    case SUBDOC_ARRAY_ADD_LAST:
                        lcb_subdocspecs_array_add_last(specs, idx++, flags, it->first.c_str(), it->first.size(),
                                                       it->second.c_str(), it->second.size());
                        break;
                    case SUBDOC_ARRAY_ADD_UNIQUE:
                        lcb_subdocspecs_array_add_unique(specs, idx++, flags, it->first.c_str(), it->first.size(),
                                                         it->second.c_str(), it->second.size());
                        break;
                    case SUBDOC_ARRAY_INSERT:
                        lcb_subdocspecs_array_insert(specs, idx++, flags, it->first.c_str(), it->first.size(),
                                                     it->second.c_str(), it->second.size());
                        break;
                    case SUBDOC_COUNTER:
                        lcb_subdocspecs_counter(specs, idx++, flags, it->first.c_str(), it->first.size(),
                                                atoll(it->second.c_str()));
                        break;
                    default:
                        break;
                }
            }
            const std::string &key = keys[ii];
            lcb_CMDSUBDOC *cmd;
            lcb_cmdsubdoc_create(&cmd);
            lcb_cmdsubdoc_key(cmd, key.c_str(), key.size());
            lcb_cmdsubdoc_specs(cmd, specs);
            if (o_upsert.passed()) {
                lcb_cmdsubdoc_store_semantics(cmd, LCB_SUBDOC_STORE_UPSERT);
            }
            if (o_expiry.passed()) {
                lcb_cmdsubdoc_expiry(cmd, o_expiry.result());
            }
            err = lcb_subdoc(instance, this, cmd);
            lcb_subdocspecs_destroy(specs);
            lcb_cmdsubdoc_destroy(cmd);
            if (err != LCB_SUCCESS) {
                throw LcbError(err, "Failed to schedule " + cmdname + " command");
            }
        }
        lcb_sched_leave(instance);

        err = lcb_wait(instance, LCB_WAIT_DEFAULT);
        if (err != LCB_SUCCESS) {
            throw LcbError(err, "Failed to execute " + cmdname + " command");
        }
    }

  protected:
    SubdocOperation m_opcode;
    const char *m_description;

    cliopts::PairListOption o_paths;
    cliopts::PairListOption o_xattrs;
    cliopts::UIntOption o_expiry;
    cliopts::BoolOption o_intermediates;
    cliopts::BoolOption o_upsert;

    bool m_enable_intermediates;
};

class HelpHandler : public Handler
{
  public:
    HANDLER_DESCRIPTION("Show help")
    HelpHandler() : Handler("help") {}

  protected:
    void run()
    {
        fprintf(stderr, "Usage: <command> [options]\n");
        fprintf(stderr, "command may be:\n");
        for (const char **cur = handlers_sorted; *cur; cur++) {
            const Handler *handler = handlers[*cur];
            fprintf(stderr, "   %-20s", *cur);
            fprintf(stderr, "%s\n", handler->description());
        }
    }
};

class DumpHandler : public Handler
{
  public:
    HANDLER_DESCRIPTION("Dump metrics and internal state of library")
    DumpHandler() : Handler("dump") {}

  protected:
    void run()
    {
        lcb_METRICS *metrics = NULL;
        size_t ii;

        lcb_dump(instance, stderr, LCB_DUMP_ALL);
        lcb_cntl(instance, LCB_CNTL_GET, LCB_CNTL_METRICS, &metrics);

        if (metrics) {
            fprintf(stderr, "%p: nsrv: %d, retried: %lu\n", (void *)instance, (int)metrics->nservers,
                    (unsigned long)metrics->packets_retried);
            for (ii = 0; ii < metrics->nservers; ii++) {
                fprintf(stderr, "  [srv-%d] snt: %lu, rcv: %lu, q: %lu, err: %lu, tmo: %lu, nmv: %lu, orph: %lu\n",
                        (int)ii, (unsigned long)metrics->servers[ii]->packets_sent,
                        (unsigned long)metrics->servers[ii]->packets_read,
                        (unsigned long)metrics->servers[ii]->packets_queued,
                        (unsigned long)metrics->servers[ii]->packets_errored,
                        (unsigned long)metrics->servers[ii]->packets_timeout,
                        (unsigned long)metrics->servers[ii]->packets_nmv,
                        (unsigned long)metrics->servers[ii]->packets_ownerless);
            }
        }
    }
};
} // namespace subdoc

static void setupHandlers()
{
    handlers["help"] = new subdoc::HelpHandler();
    handlers["dump"] = new subdoc::DumpHandler();
    handlers["get"] = new subdoc::LookupHandler("get", SUBDOC_GET, "Retrieve path from the item on the server");
    handlers["exists"] =
        new subdoc::LookupHandler("exists", SUBDOC_EXISTS, "Check if path exists in the item on the server");
    handlers["exist"] = handlers["exists"];
    handlers["remove"] = new subdoc::RemoveHandler();
    handlers["delete"] = handlers["remove"];
    handlers["upsert"] = new subdoc::UpsertHandler();
    handlers["set"] = handlers["upsert"];
    handlers["dict-upsert"] =
        new subdoc::MutationHandler("dict-upsert", SUBDOC_DICT_UPSERT, "Unconditionally set the value at the path");
    handlers["dict-add"] = new subdoc::MutationHandler(
        "dict-add", SUBDOC_DICT_ADD, "Add the value at the given path, if the given path does not exist");
    handlers["replace"] =
        new subdoc::MutationHandler("replace", SUBDOC_REPLACE, "Replace the value at the specified path", false);
    handlers["array-add-first"] =
        new subdoc::MutationHandler("array-add-first", SUBDOC_ARRAY_ADD_FIRST, "Prepend the value(s) to the array");
    handlers["array-add-last"] =
        new subdoc::MutationHandler("array-add-last", SUBDOC_ARRAY_ADD_LAST, "Append the value(s) to the array");
    handlers["array-add-unique"] = new subdoc::MutationHandler(
        "array-add-unique", SUBDOC_ARRAY_ADD_UNIQUE,
        "Add the value to the array indicated by the path, if the value is not already in the array");
    handlers["array-insert"] = new subdoc::MutationHandler(
        "array-insert", SUBDOC_ARRAY_INSERT,
        "Add the value at the given array index. Path must include index, e.g. `my.list[4]`");
    handlers["counter"] = new subdoc::MutationHandler(
        "counter", SUBDOC_COUNTER, "Increment or decrement an existing numeric path. The value must be 64-bit integer");
    handlers["size"] =
        new subdoc::LookupHandler("size", SUBDOC_GET_COUNT, "Count the number of elements in an array or dictionary");
    handlers["get-count"] = handlers["size"];
}

static void cleanup()
{
    std::map< std::string, subdoc::Handler * >::iterator iter = handlers.begin();

    handlers["exists"] = NULL;
    handlers["delete"] = NULL;
    handlers["set"] = NULL;
    handlers["get-count"] = NULL;

    for (; iter != handlers.end(); iter++) {
        if (iter->second) {
            delete iter->second;
        }
    }

    if (instance) {
        if (config.shouldDump()) {
            lcb_dump(instance, stderr, LCB_DUMP_ALL);
        }
        if (config.useTimings()) {
            hg.write();
        }
        if (instance) {
            lcb_destroy(instance);
        }
    }
}

static void real_main(int argc, char **argv)
{
    std::string history_path = ConnParams::getUserHome() + CBCSUBDOC_HISTORY_FILENAME;
    Parser parser;

    config.addToParser(parser);
    parser.parse(argc, argv);
    config.processOptions();

    lcb_CREATEOPTS *cropts = NULL;
    config.fillCropts(cropts);
    do_or_die(lcb_create(&instance, cropts), "Failed to create connection");
    lcb_createopts_destroy(cropts);
    config.doCtls();
    do_or_die(lcb_connect(instance), "Failed to connect to cluster");
    do_or_die(lcb_wait(instance, LCB_WAIT_DEFAULT), "Failed to wait for connection bootstrap");
    do_or_die(lcb_get_bootstrap_status(instance), "Failed to bootstrap");
    if (config.useTimings()) {
        hg.install(instance, stdout);
    }
    {
        int activate = 1;
        lcb_cntl(instance, LCB_CNTL_SET, LCB_CNTL_METRICS, &activate);
    }
    setupHandlers();
    std::atexit(cleanup);
    lcb_install_callback(instance, LCB_CALLBACK_SDLOOKUP, (lcb_RESPCALLBACK)subdoc_callback);
    lcb_install_callback(instance, LCB_CALLBACK_SDMUTATE, (lcb_RESPCALLBACK)subdoc_callback);

    linenoiseSetCompletionCallback(command_completion);
    linenoiseSetMultiLine(1);
    linenoiseHistoryLoad(history_path.c_str());

    do {
        char *line = linenoise("subdoc> ");
        if (line == NULL) {
            break;
        }
        if (line[0] != '\0') {
            linenoiseHistoryAdd(line);
            linenoiseHistorySave(history_path.c_str());

            int cmd_argc = 0;
            char **cmd_argv = NULL;
            int rv = cliopts_split_args(line, &cmd_argc, &cmd_argv);
            if (rv) {
                fprintf(stderr, "Invalid input: unterminated single quote\n");
            } else {
                if (cmd_argc > 0) {
                    char *cmd_name = cmd_argv[0];
                    subdoc::Handler *handler = handlers[cmd_name];
                    if (handler == NULL) {
                        fprintf(stderr, "Unknown command %s\n", cmd_name);
                        subdoc::HelpHandler().execute(cmd_argc, cmd_argv);
                    } else {
                        try {
                            handler->execute(cmd_argc, cmd_argv);
                        } catch (std::exception &err) {
                            fprintf(stderr, "%s\n", err.what());
                        }
                    }
                    free(cmd_argv);
                }
            }
        }
        free(line);
    } while (true);
}

int main(int argc, char **argv)
{
    try {
        real_main(argc, argv);
        return 0;
    } catch (std::exception &exc) {
        std::cerr << exc.what() << std::endl;
        exit(EXIT_FAILURE);
    }
}
